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