QuickConversationsService.java

  1package eu.siacs.conversations.services;
  2
  3
  4import android.os.SystemClock;
  5import android.util.Log;
  6
  7import java.io.BufferedWriter;
  8import java.io.OutputStream;
  9import java.io.OutputStreamWriter;
 10import java.net.ConnectException;
 11import java.net.HttpURLConnection;
 12import java.net.URL;
 13import java.net.URLConnection;
 14import java.net.UnknownHostException;
 15import java.security.SecureRandom;
 16import java.util.Collections;
 17import java.util.Set;
 18import java.util.WeakHashMap;
 19import java.util.concurrent.atomic.AtomicBoolean;
 20
 21import javax.net.ssl.SSLHandshakeException;
 22
 23import eu.siacs.conversations.Config;
 24import eu.siacs.conversations.crypto.sasl.Plain;
 25import eu.siacs.conversations.entities.Account;
 26import eu.siacs.conversations.utils.AccountUtils;
 27import eu.siacs.conversations.utils.CryptoHelper;
 28import eu.siacs.conversations.utils.PhoneNumberUtilWrapper;
 29import io.michaelrocks.libphonenumber.android.Phonenumber;
 30import rocks.xmpp.addr.Jid;
 31
 32public class QuickConversationsService {
 33
 34
 35    public static final int API_ERROR_OTHER = -1;
 36    public static final int API_ERROR_UNKNOWN_HOST = -2;
 37    public static final int API_ERROR_CONNECT = -3;
 38    public static final int API_ERROR_SSL_HANDSHAKE = -4;
 39    public static final int API_ERROR_AIRPLANE_MODE = -5;
 40
 41    private static final String BASE_URL = "http://venus.fritz.box:4567";
 42
 43    private final XmppConnectionService service;
 44
 45    private final Set<OnVerificationRequested> mOnVerificationRequested = Collections.newSetFromMap(new WeakHashMap<>());
 46    private final Set<OnVerification> mOnVerification = Collections.newSetFromMap(new WeakHashMap<>());
 47
 48    private final AtomicBoolean mVerificationInProgress = new AtomicBoolean(false);
 49    private final AtomicBoolean mVerificationRequestInProgress = new AtomicBoolean(false);
 50
 51    QuickConversationsService(XmppConnectionService xmppConnectionService) {
 52        this.service = xmppConnectionService;
 53    }
 54
 55    public void addOnVerificationRequestedListener(OnVerificationRequested onVerificationRequested) {
 56        synchronized (mOnVerificationRequested) {
 57            mOnVerificationRequested.add(onVerificationRequested);
 58        }
 59    }
 60
 61    public void removeOnVerificationRequestedListener(OnVerificationRequested onVerificationRequested) {
 62        synchronized (mOnVerificationRequested) {
 63            mOnVerificationRequested.remove(onVerificationRequested);
 64        }
 65    }
 66
 67    public void addOnVerificationListener(OnVerification onVerification) {
 68        synchronized (mOnVerification) {
 69            mOnVerification.add(onVerification);
 70        }
 71    }
 72
 73    public void removeOnVerificationListener(OnVerification onVerification) {
 74        synchronized (mOnVerification) {
 75            mOnVerification.remove(onVerification);
 76        }
 77    }
 78
 79    public void requestVerification(Phonenumber.PhoneNumber phoneNumber) {
 80        final String e164 = PhoneNumberUtilWrapper.normalize(service, phoneNumber);
 81
 82        /**
 83         * GET /authentication/+phoneNumber
 84         *
 85         * - returns too many requests, (sms ist unterwegs), retry after seconden -- auf jeden fall in nächste activity (verify activity) weiter leiten weil es sein kann das sms noch ankommt
 86         * - returns OK; success (auch in activity weiter lassen. aber ohne error paramater; dh send again button is activ; und vielleicht kurzer toast bzw snackbar
 87         * - returns invalid request user error wenn die phone number falsch ist
 88         */
 89        if (mVerificationRequestInProgress.compareAndSet(false, true)) {
 90            new Thread(() -> {
 91                try {
 92
 93                    Thread.sleep(5000);
 94
 95                    final URL url = new URL(BASE_URL + "/authentication/" + e164);
 96                    HttpURLConnection connection = (HttpURLConnection) url.openConnection();
 97                    connection.setConnectTimeout(Config.SOCKET_TIMEOUT * 1000);
 98                    connection.setReadTimeout(Config.SOCKET_TIMEOUT * 1000);
 99                    final int code = connection.getResponseCode();
100                    if (code == 200) {
101                        createAccountAndWait(phoneNumber, 0L);
102                    } else if (code == 429) {
103                        createAccountAndWait(phoneNumber, retryAfter(connection));
104                    } else {
105                        synchronized (mOnVerificationRequested) {
106                            for (OnVerificationRequested onVerificationRequested : mOnVerificationRequested) {
107                                onVerificationRequested.onVerificationRequestFailed(code);
108                            }
109                        }
110                    }
111                } catch (Exception e) {
112                    final int code = getApiErrorCode(e);
113                    synchronized (mOnVerificationRequested) {
114                        for (OnVerificationRequested onVerificationRequested : mOnVerificationRequested) {
115                            onVerificationRequested.onVerificationRequestFailed(code);
116                        }
117                    }
118                } finally {
119                    mVerificationRequestInProgress.set(false);
120                }
121            }).start();
122        }
123
124
125    }
126
127    private void createAccountAndWait(Phonenumber.PhoneNumber phoneNumber, final long timestamp) {
128        String local = PhoneNumberUtilWrapper.normalize(service, phoneNumber);
129        Log.d(Config.LOGTAG, "requesting verification for " + PhoneNumberUtilWrapper.normalize(service, phoneNumber));
130        Jid jid = Jid.of(local, "quick.conversations.im", null);
131        Account account = AccountUtils.getFirst(service);
132        if (account == null || !account.getJid().asBareJid().equals(jid.asBareJid())) {
133            if (account != null) {
134                service.deleteAccount(account);
135            }
136            account = new Account(jid, CryptoHelper.createPassword(new SecureRandom()));
137            account.setOption(Account.OPTION_DISABLED, true);
138            account.setOption(Account.OPTION_UNVERIFIED, true);
139            service.createAccount(account);
140        }
141        synchronized (mOnVerificationRequested) {
142            for (OnVerificationRequested onVerificationRequested : mOnVerificationRequested) {
143                if (timestamp <= 0) {
144                    onVerificationRequested.onVerificationRequested();
145                } else {
146                    onVerificationRequested.onVerificationRequestedRetryAt(timestamp);
147                }
148            }
149        }
150    }
151
152    public void verify(final Account account, String pin) {
153        /**
154         * POST /password
155         * authentication gesetzt mit telephone nummber und verification code
156         * body = password
157         *
158         * retry after, too many requests
159         * code wrong
160         * OK (weiterleiten auf publish avatar activity
161         *
162         */
163        if (mVerificationInProgress.compareAndSet(false, true)) {
164            new Thread(() -> {
165                try {
166
167                    Thread.sleep(5000);
168
169                    final URL url = new URL(BASE_URL + "/password");
170                    final HttpURLConnection connection = (HttpURLConnection) url.openConnection();
171                    connection.setConnectTimeout(Config.SOCKET_TIMEOUT * 1000);
172                    connection.setReadTimeout(Config.SOCKET_TIMEOUT * 1000);
173                    connection.setRequestMethod("POST");
174                    connection.setRequestProperty("Authorization", Plain.getMessage(account.getUsername(), pin));
175                    final OutputStream os = connection.getOutputStream();
176                    final BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(os, "UTF-8"));
177                    writer.write(account.getPassword());
178                    writer.flush();
179                    writer.close();
180                    os.close();
181                    connection.connect();
182                    final int code = connection.getResponseCode();
183                    if (code == 200) {
184                        account.setOption(Account.OPTION_UNVERIFIED, false);
185                        account.setOption(Account.OPTION_DISABLED, false);
186                        service.updateAccount(account);
187                        synchronized (mOnVerification) {
188                            for (OnVerification onVerification : mOnVerification) {
189                                onVerification.onVerificationSucceeded();
190                            }
191                        }
192                    } else if (code == 429) {
193                        final long retryAfter = retryAfter(connection);
194                        synchronized (mOnVerification) {
195                            for (OnVerification onVerification : mOnVerification) {
196                                onVerification.onVerificationRetryAt(retryAfter);
197                            }
198                        }
199                    } else {
200                        synchronized (mOnVerification) {
201                            for (OnVerification onVerification : mOnVerification) {
202                                onVerification.onVerificationFailed(code);
203                            }
204                        }
205                    }
206                } catch (Exception e) {
207                    final int code = getApiErrorCode(e);
208                    synchronized (mOnVerification) {
209                        for (OnVerification onVerification : mOnVerification) {
210                            onVerification.onVerificationFailed(code);
211                        }
212                    }
213                } finally {
214                    mVerificationInProgress.set(false);
215                }
216            }).start();
217        }
218    }
219
220    private int getApiErrorCode(Exception e) {
221        if (!service.hasInternetConnection()) {
222            return API_ERROR_AIRPLANE_MODE;
223        } else if (e instanceof UnknownHostException) {
224            return API_ERROR_UNKNOWN_HOST;
225        } else if (e instanceof ConnectException) {
226            return API_ERROR_CONNECT;
227        } else if (e instanceof SSLHandshakeException) {
228            return API_ERROR_SSL_HANDSHAKE;
229        } else {
230            Log.d(Config.LOGTAG,e.getClass().getName());
231            return API_ERROR_OTHER;
232        }
233    }
234
235    private static long retryAfter(HttpURLConnection connection) {
236        try {
237            return SystemClock.elapsedRealtime() + (Long.parseLong(connection.getHeaderField("Retry-After")) * 1000L);
238        } catch (Exception e) {
239            return 0;
240        }
241    }
242
243    public boolean isVerifying() {
244        return mVerificationInProgress.get();
245    }
246
247    public boolean isRequestingVerification() {
248        return mVerificationRequestInProgress.get();
249    }
250
251    public interface OnVerificationRequested {
252        void onVerificationRequestFailed(int code);
253
254        void onVerificationRequested();
255
256        void onVerificationRequestedRetryAt(long timestamp);
257    }
258
259    public interface OnVerification {
260        void onVerificationFailed(int code);
261
262        void onVerificationSucceeded();
263
264        void onVerificationRetryAt(long timestamp);
265    }
266}