diff --git a/mobile/android/base/fxa/sync/FxAccountSyncAdapter.java b/mobile/android/base/fxa/sync/FxAccountSyncAdapter.java index 01a5be4b5106..bbf85f14409c 100644 --- a/mobile/android/base/fxa/sync/FxAccountSyncAdapter.java +++ b/mobile/android/base/fxa/sync/FxAccountSyncAdapter.java @@ -265,6 +265,10 @@ public class FxAccountSyncAdapter extends AbstractThreadedSyncAdapter { Logger.error(LOG_TAG, "Failed to get token.", e); callback.handleError(null, e); } + + @Override + public void handleBackoff(int backoffSeconds) { + } }); } diff --git a/mobile/android/base/resources/drawable-hdpi/fxaccount_checkbox.png b/mobile/android/base/resources/drawable-hdpi/fxaccount_checkbox.png index 11e89d34aba2..f6ef37ba9405 100755 Binary files a/mobile/android/base/resources/drawable-hdpi/fxaccount_checkbox.png and b/mobile/android/base/resources/drawable-hdpi/fxaccount_checkbox.png differ diff --git a/mobile/android/base/resources/drawable-hdpi/fxaccount_ddarrow_active.png b/mobile/android/base/resources/drawable-hdpi/fxaccount_ddarrow_active.png index 4abb71ee3cdb..a8782e2d6656 100644 Binary files a/mobile/android/base/resources/drawable-hdpi/fxaccount_ddarrow_active.png and b/mobile/android/base/resources/drawable-hdpi/fxaccount_ddarrow_active.png differ diff --git a/mobile/android/base/resources/drawable-hdpi/fxaccount_ddarrow_inactive.png b/mobile/android/base/resources/drawable-hdpi/fxaccount_ddarrow_inactive.png index 204562d39f09..8314fc45b04f 100644 Binary files a/mobile/android/base/resources/drawable-hdpi/fxaccount_ddarrow_inactive.png and b/mobile/android/base/resources/drawable-hdpi/fxaccount_ddarrow_inactive.png differ diff --git a/mobile/android/base/resources/drawable-hdpi/fxaccount_icon.png b/mobile/android/base/resources/drawable-hdpi/fxaccount_icon.png index 131f05dd288a..03a16d427eae 100755 Binary files a/mobile/android/base/resources/drawable-hdpi/fxaccount_icon.png and b/mobile/android/base/resources/drawable-hdpi/fxaccount_icon.png differ diff --git a/mobile/android/base/resources/drawable-hdpi/fxaccount_mail.png b/mobile/android/base/resources/drawable-hdpi/fxaccount_mail.png index 3a2e33adfcc3..23ebe23741ed 100755 Binary files a/mobile/android/base/resources/drawable-hdpi/fxaccount_mail.png and b/mobile/android/base/resources/drawable-hdpi/fxaccount_mail.png differ diff --git a/mobile/android/base/resources/drawable-hdpi/fxaccount_sync_icon.png b/mobile/android/base/resources/drawable-hdpi/fxaccount_sync_icon.png index 1b6e67572fb2..fe2e174886c9 100644 Binary files a/mobile/android/base/resources/drawable-hdpi/fxaccount_sync_icon.png and b/mobile/android/base/resources/drawable-hdpi/fxaccount_sync_icon.png differ diff --git a/mobile/android/base/resources/drawable-mdpi/fxaccount_checkbox.png b/mobile/android/base/resources/drawable-mdpi/fxaccount_checkbox.png index 1a50daf6de8d..1c1d14c0c9fe 100644 Binary files a/mobile/android/base/resources/drawable-mdpi/fxaccount_checkbox.png and b/mobile/android/base/resources/drawable-mdpi/fxaccount_checkbox.png differ diff --git a/mobile/android/base/resources/drawable-mdpi/fxaccount_ddarrow_active.png b/mobile/android/base/resources/drawable-mdpi/fxaccount_ddarrow_active.png index bb978372f0ea..d1fd254cd5f2 100644 Binary files a/mobile/android/base/resources/drawable-mdpi/fxaccount_ddarrow_active.png and b/mobile/android/base/resources/drawable-mdpi/fxaccount_ddarrow_active.png differ diff --git a/mobile/android/base/resources/drawable-mdpi/fxaccount_ddarrow_inactive.png b/mobile/android/base/resources/drawable-mdpi/fxaccount_ddarrow_inactive.png index 7d82a89cb183..82304783977b 100644 Binary files a/mobile/android/base/resources/drawable-mdpi/fxaccount_ddarrow_inactive.png and b/mobile/android/base/resources/drawable-mdpi/fxaccount_ddarrow_inactive.png differ diff --git a/mobile/android/base/resources/drawable-mdpi/fxaccount_icon.png b/mobile/android/base/resources/drawable-mdpi/fxaccount_icon.png index b6e7595e0bf7..b328c7b44144 100644 Binary files a/mobile/android/base/resources/drawable-mdpi/fxaccount_icon.png and b/mobile/android/base/resources/drawable-mdpi/fxaccount_icon.png differ diff --git a/mobile/android/base/resources/drawable-mdpi/fxaccount_mail.png b/mobile/android/base/resources/drawable-mdpi/fxaccount_mail.png index bcb5f1596358..2f7d92b706c5 100644 Binary files a/mobile/android/base/resources/drawable-mdpi/fxaccount_mail.png and b/mobile/android/base/resources/drawable-mdpi/fxaccount_mail.png differ diff --git a/mobile/android/base/resources/drawable-mdpi/fxaccount_sync_error.png b/mobile/android/base/resources/drawable-mdpi/fxaccount_sync_error.png index 71f60fb224ee..5b084016df13 100644 Binary files a/mobile/android/base/resources/drawable-mdpi/fxaccount_sync_error.png and b/mobile/android/base/resources/drawable-mdpi/fxaccount_sync_error.png differ diff --git a/mobile/android/base/resources/drawable-mdpi/sync_desktop.png b/mobile/android/base/resources/drawable-mdpi/sync_desktop.png index 83f98d3c0b11..e470ebc6d671 100644 Binary files a/mobile/android/base/resources/drawable-mdpi/sync_desktop.png and b/mobile/android/base/resources/drawable-mdpi/sync_desktop.png differ diff --git a/mobile/android/base/resources/drawable-mdpi/sync_mobile.png b/mobile/android/base/resources/drawable-mdpi/sync_mobile.png index c237c29559c2..51efb99a2175 100644 Binary files a/mobile/android/base/resources/drawable-mdpi/sync_mobile.png and b/mobile/android/base/resources/drawable-mdpi/sync_mobile.png differ diff --git a/mobile/android/base/sync/net/SyncResponse.java b/mobile/android/base/sync/net/SyncResponse.java index 2409f7c72585..b0e676a7e8fe 100644 --- a/mobile/android/base/sync/net/SyncResponse.java +++ b/mobile/android/base/sync/net/SyncResponse.java @@ -49,6 +49,20 @@ public class SyncResponse { return this.getStatusCode() == 200; } + /** + * Fetch the content type of the HTTP response body. + * + * @return a Header instance, or null if there was + * no body or no valid Content-Type. + */ + public Header getContentType() { + HttpEntity entity = this.response.getEntity(); + if (entity == null) { + return null; + } + return entity.getContentType(); + } + private String body = null; public String body() throws IllegalStateException, IOException { if (body != null) { @@ -145,6 +159,14 @@ public class SyncResponse { } } + /** + * @return A number of seconds, or -1 if the 'X-Backoff' header was not + * present. + */ + public int backoffInSeconds() throws NumberFormatException { + return this.getIntegerHeader("x-backoff"); + } + /** * @return A number of seconds, or -1 if the 'X-Weave-Backoff' header was not * present. @@ -154,14 +176,21 @@ public class SyncResponse { } /** - * @return A number of milliseconds, or -1 if neither the 'Retry-After' or - * 'X-Weave-Backoff' header was present. + * Extract a number of seconds, or -1 if none of the specified headers were present. + * + * @param includeRetryAfter + * if true, the Retry-After header is excluded. This is + * useful for processing non-error responses where a Retry-After + * header would be unexpected. + * @return the maximum of the three possible backoff headers, in seconds. */ - public long totalBackoffInMilliseconds() { + public int totalBackoffInSeconds(boolean includeRetryAfter) { int retryAfterInSeconds = -1; - try { - retryAfterInSeconds = retryAfterInSeconds(); - } catch (NumberFormatException e) { + if (includeRetryAfter) { + try { + retryAfterInSeconds = retryAfterInSeconds(); + } catch (NumberFormatException e) { + } } int weaveBackoffInSeconds = -1; @@ -170,7 +199,26 @@ public class SyncResponse { } catch (NumberFormatException e) { } - long totalBackoff = (long) Math.max(retryAfterInSeconds, weaveBackoffInSeconds); + int backoffInSeconds = -1; + try { + backoffInSeconds = backoffInSeconds(); + } catch (NumberFormatException e) { + } + + int totalBackoff = Math.max(retryAfterInSeconds, Math.max(backoffInSeconds, weaveBackoffInSeconds)); + if (totalBackoff < 0) { + return -1; + } else { + return totalBackoff; + } + } + + /** + * @return A number of milliseconds, or -1 if neither the 'Retry-After', + * 'X-Backoff', or 'X-Weave-Backoff' header were present. + */ + public long totalBackoffInMilliseconds() { + long totalBackoff = totalBackoffInSeconds(true); if (totalBackoff < 0) { return -1; } else { diff --git a/mobile/android/base/sync/net/SyncStorageRecordRequest.java b/mobile/android/base/sync/net/SyncStorageRecordRequest.java index 88bf81ab73ab..5e87ce8c8ef9 100644 --- a/mobile/android/base/sync/net/SyncStorageRecordRequest.java +++ b/mobile/android/base/sync/net/SyncStorageRecordRequest.java @@ -25,6 +25,7 @@ import org.mozilla.gecko.sync.ThreadPool; * * Headers: * * Retry-After * * X-Weave-Backoff + * * X-Backoff * * X-Weave-Records? * * ... * * Timeouts diff --git a/mobile/android/base/tokenserver/TokenServerClient.java b/mobile/android/base/tokenserver/TokenServerClient.java index e13d0fe3c31d..dd8229fa1d79 100644 --- a/mobile/android/base/tokenserver/TokenServerClient.java +++ b/mobile/android/base/tokenserver/TokenServerClient.java @@ -29,6 +29,7 @@ import org.mozilla.gecko.tokenserver.TokenServerException.TokenServerMalformedRe import org.mozilla.gecko.tokenserver.TokenServerException.TokenServerMalformedResponseException; import org.mozilla.gecko.tokenserver.TokenServerException.TokenServerUnknownServiceException; +import ch.boye.httpclientandroidlib.Header; import ch.boye.httpclientandroidlib.HttpHeaders; import ch.boye.httpclientandroidlib.HttpResponse; import ch.boye.httpclientandroidlib.client.ClientProtocolException; @@ -93,6 +94,28 @@ public class TokenServerClient { }); } + /** + * Notify the delegate that some kind of backoff header (X-Backoff, + * X-Weave-Backoff, Retry-After) was received and should be acted upon. + * + * This method is non-terminal, and will be followed by a separate + * invoke* call. + * + * @param delegate + * the delegate to inform. + * @param backoffSeconds + * the number of seconds for which the system should wait before + * making another token server request to this server. + */ + protected void notifyBackoff(final TokenServerClientDelegate delegate, final int backoffSeconds) { + executor.execute(new Runnable() { + @Override + public void run() { + delegate.handleBackoff(backoffSeconds); + } + }); + } + protected void invokeHandleError(final TokenServerClientDelegate delegate, final Exception e) { executor.execute(new Runnable() { @Override @@ -102,17 +125,21 @@ public class TokenServerClient { }); } - public TokenServerToken processResponse(HttpResponse response) - throws TokenServerException { - SyncResponse res = new SyncResponse(response); + public TokenServerToken processResponse(SyncResponse res) throws TokenServerException { int statusCode = res.getStatusCode(); Logger.debug(LOG_TAG, "Got token response with status code " + statusCode + "."); // Responses should *always* be JSON, even in the case of 4xx and 5xx // errors. If we don't see JSON, the server is likely very unhappy. - String contentType = response.getEntity().getContentType().getValue(); - if (!contentType.equals("application/json") && !contentType.startsWith("application/json;")) { + final Header contentType = res.getContentType(); + if (contentType == null) { + throw new TokenServerMalformedResponseException(null, "Non-JSON response Content-Type."); + } + + final String type = contentType.getValue(); + if (!type.equals("application/json") && + !type.startsWith("application/json;")) { Logger.warn(LOG_TAG, "Got non-JSON response with Content-Type " + contentType + ". Misconfigured server?"); throw new TokenServerMalformedResponseException(null, "Non-JSON response Content-Type."); @@ -230,10 +257,21 @@ public class TokenServerClient { @Override public void handleHttpResponse(HttpResponse response) { + // Skew. SkewHandler skewHandler = SkewHandler.getSkewHandlerForResource(resource); skewHandler.updateSkew(response, System.currentTimeMillis()); + + // Extract backoff regardless of whether this was an error response, and + // Retry-After for 503 responses. The error will be handled elsewhere.) + SyncResponse res = new SyncResponse(response); + final boolean includeRetryAfter = res.getStatusCode() == 503; + int backoffInSeconds = res.totalBackoffInSeconds(includeRetryAfter); + if (backoffInSeconds > -1) { + client.notifyBackoff(delegate, backoffInSeconds); + } + try { - TokenServerToken token = client.processResponse(response); + TokenServerToken token = client.processResponse(res); client.invokeHandleSuccess(delegate, token); } catch (TokenServerException e) { client.invokeHandleFailure(delegate, e); diff --git a/mobile/android/base/tokenserver/TokenServerClientDelegate.java b/mobile/android/base/tokenserver/TokenServerClientDelegate.java index 2fc48b08ef99..d34d04c73ffa 100644 --- a/mobile/android/base/tokenserver/TokenServerClientDelegate.java +++ b/mobile/android/base/tokenserver/TokenServerClientDelegate.java @@ -4,9 +4,13 @@ package org.mozilla.gecko.tokenserver; - public interface TokenServerClientDelegate { void handleSuccess(TokenServerToken token); void handleFailure(TokenServerException e); void handleError(Exception e); + + /** + * Might be called multiple times, in addition to the other terminating handler methods. + */ + void handleBackoff(int backoffSeconds); } \ No newline at end of file