added dialogs for rate limiting and out of date version

Daniel Gultsch created

Change summary

build.gradle                                                                  |  2 
src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java         |  6 
src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java         |  4 
src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java           |  2 
src/main/res/values/strings.xml                                               |  7 
src/quick/java/eu/siacs/conversations/services/QuickConversationsService.java | 48 
src/quick/java/eu/siacs/conversations/ui/EnterPhoneNumberActivity.java        |  7 
src/quick/java/eu/siacs/conversations/ui/VerifyActivity.java                  | 16 
src/quick/java/eu/siacs/conversations/ui/util/ApiDialogHelper.java            | 83 
src/quick/java/eu/siacs/conversations/ui/util/ApiErrorDialogHelper.java       | 47 
10 files changed, 139 insertions(+), 83 deletions(-)

Detailed changes

build.gradle πŸ”—

@@ -104,7 +104,7 @@ android {
         quick {
             dimension "mode"
             applicationId = "im.conversations.quick"
-            resValue "string", "app_name", "Quick Conversations"
+            resValue "string", "app_name", "Quicksy"
             resValue "string", "applicationId", applicationId
         }
 

src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java πŸ”—

@@ -69,7 +69,11 @@ public abstract class AbstractGenerator {
 	}
 
 	public String getIdentityName() {
-		return mXmppConnectionService.getString(R.string.app_name) + " " + getIdentityVersion();
+		return mXmppConnectionService.getString(R.string.app_name) + ' ' + getIdentityVersion();
+	}
+
+	public String getUserAgent() {
+		return mXmppConnectionService.getString(R.string.app_name) + '/' + getIdentityVersion();
 	}
 
 	String getIdentityType() {

src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java πŸ”—

@@ -288,7 +288,7 @@ public class HttpDownloadConnection implements Transferable {
 				}
 				connection.setUseCaches(false);
 				Log.d(Config.LOGTAG, "url: " + connection.getURL().toString());
-				connection.setRequestProperty("User-Agent", mXmppConnectionService.getIqGenerator().getIdentityName());
+				connection.setRequestProperty("User-Agent", mXmppConnectionService.getIqGenerator().getUserAgent());
 				if (connection instanceof HttpsURLConnection) {
 					mHttpConnectionManager.setupTrustManager((HttpsURLConnection) connection, interactive);
 				}
@@ -367,7 +367,7 @@ public class HttpDownloadConnection implements Transferable {
 					mHttpConnectionManager.setupTrustManager((HttpsURLConnection) connection, interactive);
 				}
 				connection.setUseCaches(false);
-				connection.setRequestProperty("User-Agent", mXmppConnectionService.getIqGenerator().getIdentityName());
+				connection.setRequestProperty("User-Agent", mXmppConnectionService.getIqGenerator().getUserAgent());
 				final boolean tryResume = file.exists() && file.getKey() == null && file.getSize() > 0;
 				long resumeSize = 0;
 				long expected = file.getExpectedSize();

src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java πŸ”—

@@ -166,7 +166,7 @@ public class HttpUploadConnection implements Transferable {
 			connection.setUseCaches(false);
 			connection.setRequestMethod("PUT");
 			connection.setFixedLengthStreamingMode(expectedFileSize);
-			connection.setRequestProperty("User-Agent",mXmppConnectionService.getIqGenerator().getIdentityName());
+			connection.setRequestProperty("User-Agent",mXmppConnectionService.getIqGenerator().getUserAgent());
 			if(slot.getHeaders() != null) {
 				for(HashMap.Entry<String,String> entry : slot.getHeaders().entrySet()) {
 					connection.setRequestProperty(entry.getKey(),entry.getValue());

src/main/res/values/strings.xml πŸ”—

@@ -754,7 +754,7 @@
     <string name="choose_a_country">Choose a country</string>
     <string name="phone_number">phone number</string>
     <string name="verify_your_phone_number">Verify your phone number</string>
-    <string name="enter_country_code_and_phone_number">Quick Conversations will send an SMS message (carrier charges may apply) to verify your phone number. Enter your country code and phone number:</string>
+    <string name="enter_country_code_and_phone_number">Quicksy will send an SMS message (carrier charges may apply) to verify your phone number. Enter your country code and phone number:</string>
     <string name="we_will_be_verifying"><![CDATA[We will be verifying the phone number<br/><br/><b>%s</b><br/><br/>Is this OK, or would you like to edit the number?]]></string>
     <string name="not_a_valid_phone_number">%s is not a valid phone number.</string>
     <string name="please_enter_your_phone_number">Please enter your phone number.</string>
@@ -783,4 +783,9 @@
     <string name="invalid_user_input">Invalid user input</string>
     <string name="temporarily_unavailable">Temporarily unavailable. Try again later.</string>
     <string name="no_network_connection">No network connection.</string>
+    <string name="try_again_in_x">Please try again in %s</string>
+    <string name="rate_limited">You are rate limited</string>
+    <string name="too_many_attempts">Too many attempts</string>
+    <string name="the_app_is_out_of_date">You are using an out of date version of this app.</string>
+    <string name="update">Update</string>
 </resources>

src/quick/java/eu/siacs/conversations/services/QuickConversationsService.java πŸ”—

@@ -1,7 +1,9 @@
 package eu.siacs.conversations.services;
 
 
+import android.content.SharedPreferences;
 import android.os.SystemClock;
+import android.preference.PreferenceManager;
 import android.util.Log;
 
 import java.io.BufferedWriter;
@@ -10,11 +12,12 @@ import java.io.OutputStreamWriter;
 import java.net.ConnectException;
 import java.net.HttpURLConnection;
 import java.net.URL;
-import java.net.URLConnection;
 import java.net.UnknownHostException;
 import java.security.SecureRandom;
 import java.util.Collections;
+import java.util.Locale;
 import java.util.Set;
+import java.util.UUID;
 import java.util.WeakHashMap;
 import java.util.concurrent.atomic.AtomicBoolean;
 
@@ -40,6 +43,8 @@ public class QuickConversationsService {
 
     private static final String BASE_URL = "http://venus.fritz.box:4567";
 
+    private static final String INSTALLATION_ID = "eu.siacs.conversations.installation-id";
+
     private final XmppConnectionService service;
 
     private final Set<OnVerificationRequested> mOnVerificationRequested = Collections.newSetFromMap(new WeakHashMap<>());
@@ -78,14 +83,6 @@ public class QuickConversationsService {
 
     public void requestVerification(Phonenumber.PhoneNumber phoneNumber) {
         final String e164 = PhoneNumberUtilWrapper.normalize(service, phoneNumber);
-
-        /**
-         * GET /authentication/+phoneNumber
-         *
-         * - 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
-         * - returns OK; success (auch in activity weiter lassen. aber ohne error paramater; dh send again button is activ; und vielleicht kurzer toast bzw snackbar
-         * - returns invalid request user error wenn die phone number falsch ist
-         */
         if (mVerificationRequestInProgress.compareAndSet(false, true)) {
             new Thread(() -> {
                 try {
@@ -96,6 +93,7 @@ public class QuickConversationsService {
                     HttpURLConnection connection = (HttpURLConnection) url.openConnection();
                     connection.setConnectTimeout(Config.SOCKET_TIMEOUT * 1000);
                     connection.setReadTimeout(Config.SOCKET_TIMEOUT * 1000);
+                    setHeader(connection);
                     final int code = connection.getResponseCode();
                     if (code == 200) {
                         createAccountAndWait(phoneNumber, 0L);
@@ -150,16 +148,6 @@ public class QuickConversationsService {
     }
 
     public void verify(final Account account, String pin) {
-        /**
-         * POST /password
-         * authentication gesetzt mit telephone nummber und verification code
-         * body = password
-         *
-         * retry after, too many requests
-         * code wrong
-         * OK (weiterleiten auf publish avatar activity
-         *
-         */
         if (mVerificationInProgress.compareAndSet(false, true)) {
             new Thread(() -> {
                 try {
@@ -172,6 +160,7 @@ public class QuickConversationsService {
                     connection.setReadTimeout(Config.SOCKET_TIMEOUT * 1000);
                     connection.setRequestMethod("POST");
                     connection.setRequestProperty("Authorization", Plain.getMessage(account.getUsername(), pin));
+                    setHeader(connection);
                     final OutputStream os = connection.getOutputStream();
                     final BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(os, "UTF-8"));
                     writer.write(account.getPassword());
@@ -217,6 +206,25 @@ public class QuickConversationsService {
         }
     }
 
+    private void setHeader(HttpURLConnection connection) {
+        connection.setRequestProperty("User-Agent", service.getIqGenerator().getUserAgent());
+        connection.setRequestProperty("Installation-Id", getInstallationId());
+        connection.setRequestProperty("Accept-Language", Locale.getDefault().getLanguage());
+    }
+
+    private String getInstallationId() {
+        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(service);
+        String id = preferences.getString(INSTALLATION_ID, null);
+        if (id != null) {
+            return id;
+        } else {
+            id = UUID.randomUUID().toString();
+            preferences.edit().putString(INSTALLATION_ID, id).apply();
+            return id;
+        }
+
+    }
+
     private int getApiErrorCode(Exception e) {
         if (!service.hasInternetConnection()) {
             return API_ERROR_AIRPLANE_MODE;
@@ -227,7 +235,7 @@ public class QuickConversationsService {
         } else if (e instanceof SSLHandshakeException) {
             return API_ERROR_SSL_HANDSHAKE;
         } else {
-            Log.d(Config.LOGTAG,e.getClass().getName());
+            Log.d(Config.LOGTAG, e.getClass().getName());
             return API_ERROR_OTHER;
         }
     }

src/quick/java/eu/siacs/conversations/ui/EnterPhoneNumberActivity.java πŸ”—

@@ -4,23 +4,20 @@ import android.app.AlertDialog;
 import android.content.Intent;
 import android.databinding.DataBindingUtil;
 import android.os.Bundle;
-import android.support.v7.app.AppCompatActivity;
 import android.support.v7.widget.Toolbar;
 import android.text.Editable;
 import android.text.Html;
 import android.text.TextUtils;
 import android.text.TextWatcher;
 import android.util.Log;
-import android.view.KeyEvent;
 import android.view.View;
-import android.widget.TextView;
 
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
 import eu.siacs.conversations.databinding.ActivityEnterNumberBinding;
 import eu.siacs.conversations.services.QuickConversationsService;
 import eu.siacs.conversations.ui.drawable.TextDrawable;
-import eu.siacs.conversations.ui.util.ApiErrorDialogHelper;
+import eu.siacs.conversations.ui.util.ApiDialogHelper;
 import eu.siacs.conversations.utils.PhoneNumberUtilWrapper;
 import io.michaelrocks.libphonenumber.android.NumberParseException;
 import io.michaelrocks.libphonenumber.android.PhoneNumberUtil;
@@ -180,7 +177,7 @@ public class EnterPhoneNumberActivity extends XmppActivity implements QuickConve
     public void onVerificationRequestFailed(int code) {
         runOnUiThread(() -> {
             setRequestingVerificationState(false);
-            ApiErrorDialogHelper.create(this, code).show();
+            ApiDialogHelper.createError(this, code).show();
         });
     }
 

src/quick/java/eu/siacs/conversations/ui/VerifyActivity.java πŸ”—

@@ -19,7 +19,7 @@ import eu.siacs.conversations.R;
 import eu.siacs.conversations.databinding.ActivityVerifyBinding;
 import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.services.QuickConversationsService;
-import eu.siacs.conversations.ui.util.ApiErrorDialogHelper;
+import eu.siacs.conversations.ui.util.ApiDialogHelper;
 import eu.siacs.conversations.ui.util.PinEntryWrapper;
 import eu.siacs.conversations.utils.AccountUtils;
 import eu.siacs.conversations.utils.PhoneNumberUtilWrapper;
@@ -286,7 +286,7 @@ public class VerifyActivity extends XmppActivity implements ClipboardManager.OnP
                 builder.setPositiveButton(R.string.ok, null);
                 builder.create().show();
             } else {
-                ApiErrorDialogHelper.create(this, code).show();
+                ApiDialogHelper.createError(this, code).show();
             }
         });
     }
@@ -299,7 +299,10 @@ public class VerifyActivity extends XmppActivity implements ClipboardManager.OnP
     @Override
     public void onVerificationRetryAt(long timestamp) {
         this.retryVerificationAfter = timestamp;
-        runOnUiThread(() -> setVerifyingState(false));
+        runOnUiThread(() -> {
+            ApiDialogHelper.createTooManyAttempts(this).show();
+            setVerifyingState(false);
+        });
         mHandler.removeCallbacks(VERIFICATION_TIMEOUT_UPDATER);
         runOnUiThread(VERIFICATION_TIMEOUT_UPDATER);
     }
@@ -309,7 +312,7 @@ public class VerifyActivity extends XmppActivity implements ClipboardManager.OnP
     public void onVerificationRequestFailed(int code) {
         runOnUiThread(() -> {
             setRequestingVerificationState(false);
-            ApiErrorDialogHelper.create(this, code).show();
+            ApiDialogHelper.createError(this, code).show();
         });
     }
 
@@ -328,7 +331,10 @@ public class VerifyActivity extends XmppActivity implements ClipboardManager.OnP
     @Override
     public void onVerificationRequestedRetryAt(long timestamp) {
         this.retrySmsAfter = timestamp;
-        runOnUiThread(() -> setRequestingVerificationState(false));
+        runOnUiThread(() -> {
+            ApiDialogHelper.createRateLimited(this, timestamp).show();
+            setRequestingVerificationState(false);
+        });
         mHandler.removeCallbacks(SMS_TIMEOUT_UPDATER);
         runOnUiThread(SMS_TIMEOUT_UPDATER);
     }

src/quick/java/eu/siacs/conversations/ui/util/ApiDialogHelper.java πŸ”—

@@ -0,0 +1,83 @@
+package eu.siacs.conversations.ui.util;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.SystemClock;
+import android.support.annotation.StringRes;
+
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.services.QuickConversationsService;
+import eu.siacs.conversations.utils.TimeframeUtils;
+
+public class ApiDialogHelper {
+
+    public static Dialog createError(final Context context, final int code) {
+        @StringRes final int res;
+        switch (code) {
+            case QuickConversationsService.API_ERROR_AIRPLANE_MODE:
+                res = R.string.no_network_connection;
+                break;
+            case QuickConversationsService.API_ERROR_OTHER:
+                res = R.string.unknown_api_error_network;
+                break;
+            case QuickConversationsService.API_ERROR_CONNECT:
+                res = R.string.unable_to_connect_to_server;
+                break;
+            case QuickConversationsService.API_ERROR_SSL_HANDSHAKE:
+                res = R.string.unable_to_establish_secure_connection;
+                break;
+            case QuickConversationsService.API_ERROR_UNKNOWN_HOST:
+                res = R.string.unable_to_find_server;
+                break;
+            case 400:
+                res = R.string.invalid_user_input;
+                break;
+            case 403:
+                res = R.string.the_app_is_out_of_date;
+                break;
+            case 502:
+            case 503:
+            case 504:
+                res = R.string.temporarily_unavailable;
+                break;
+            default:
+                res = R.string.unknown_api_error_response;
+        }
+        final AlertDialog.Builder builder = new AlertDialog.Builder(context);
+        builder.setMessage(res);
+        if (code == 403 && resolvable(context, getMarketViewIntent(context))) {
+            builder.setNegativeButton(R.string.cancel, null);
+            builder.setPositiveButton(R.string.update, (dialog, which) -> context.startActivity(getMarketViewIntent(context)));
+        } else {
+            builder.setPositiveButton(R.string.ok, null);
+        }
+        return builder.create();
+    }
+
+    public static Dialog createRateLimited(final Context context, final long timestamp) {
+        final AlertDialog.Builder builder = new AlertDialog.Builder(context);
+        builder.setTitle(R.string.rate_limited);
+        builder.setMessage(context.getString(R.string.try_again_in_x, TimeframeUtils.resolve(context, timestamp - SystemClock.elapsedRealtime())));
+        builder.setPositiveButton(R.string.ok, null);
+        return builder.create();
+    }
+
+    public static Dialog createTooManyAttempts(final Context context) {
+        final AlertDialog.Builder builder = new AlertDialog.Builder(context);
+        builder.setMessage(R.string.too_many_attempts);
+        builder.setPositiveButton(R.string.ok, null);
+        return builder.create();
+    }
+
+    private static Intent getMarketViewIntent(Context context) {
+        return new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=" + context.getPackageName()));
+    }
+
+    private static boolean resolvable(Context context, Intent intent) {
+        return context.getPackageManager().queryIntentActivities(intent, 0).size() > 0;
+    }
+}

src/quick/java/eu/siacs/conversations/ui/util/ApiErrorDialogHelper.java πŸ”—

@@ -1,47 +0,0 @@
-package eu.siacs.conversations.ui.util;
-
-import android.app.AlertDialog;
-import android.app.Dialog;
-import android.content.Context;
-import android.support.annotation.StringRes;
-
-import eu.siacs.conversations.R;
-import eu.siacs.conversations.services.QuickConversationsService;
-
-public class ApiErrorDialogHelper {
-
-    public static Dialog create(Context context, int code) {
-        @StringRes final int res;
-        switch (code) {
-            case QuickConversationsService.API_ERROR_AIRPLANE_MODE:
-                res = R.string.no_network_connection;
-                break;
-            case QuickConversationsService.API_ERROR_OTHER:
-                res = R.string.unknown_api_error_network;
-                break;
-            case QuickConversationsService.API_ERROR_CONNECT:
-                res = R.string.unable_to_connect_to_server;
-                break;
-            case QuickConversationsService.API_ERROR_SSL_HANDSHAKE:
-                res = R.string.unable_to_establish_secure_connection;
-                break;
-            case QuickConversationsService.API_ERROR_UNKNOWN_HOST:
-                res = R.string.unable_to_find_server;
-                break;
-            case 400:
-                res = R.string.invalid_user_input;
-                break;
-            case 502:
-            case 503:
-            case 504:
-                res = R.string.temporarily_unavailable;
-                break;
-            default:
-                res = R.string.unknown_api_error_response;
-        }
-        AlertDialog.Builder builder = new AlertDialog.Builder(context);
-        builder.setMessage(res);
-        builder.setPositiveButton(R.string.ok, null);
-        return builder.create();
-    }
-}