1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/mobile/android/base/background/announcements/AnnouncementsFetchResourceDelegate.java Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,183 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +package org.mozilla.gecko.background.announcements; 1.9 + 1.10 +import java.io.IOException; 1.11 +import java.security.GeneralSecurityException; 1.12 +import java.util.ArrayList; 1.13 +import java.util.Date; 1.14 +import java.util.List; 1.15 + 1.16 +import org.json.simple.JSONArray; 1.17 +import org.json.simple.JSONObject; 1.18 +import org.mozilla.gecko.background.common.log.Logger; 1.19 +import org.mozilla.gecko.sync.ExtendedJSONObject; 1.20 +import org.mozilla.gecko.sync.NonArrayJSONException; 1.21 +import org.mozilla.gecko.sync.net.AuthHeaderProvider; 1.22 +import org.mozilla.gecko.sync.net.BaseResource; 1.23 +import org.mozilla.gecko.sync.net.BaseResourceDelegate; 1.24 +import org.mozilla.gecko.sync.net.Resource; 1.25 +import org.mozilla.gecko.sync.net.SyncResponse; 1.26 + 1.27 +import ch.boye.httpclientandroidlib.Header; 1.28 +import ch.boye.httpclientandroidlib.HttpResponse; 1.29 +import ch.boye.httpclientandroidlib.client.ClientProtocolException; 1.30 +import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase; 1.31 +import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient; 1.32 +import ch.boye.httpclientandroidlib.impl.cookie.DateUtils; 1.33 +import ch.boye.httpclientandroidlib.protocol.HTTP; 1.34 + 1.35 +/** 1.36 + * Converts HTTP resource callbacks into AnnouncementsFetchDelegate callbacks. 1.37 + */ 1.38 +public class AnnouncementsFetchResourceDelegate extends BaseResourceDelegate { 1.39 + private static final String ACCEPT_HEADER = "application/json;charset=utf-8"; 1.40 + 1.41 + private static final String LOG_TAG = "AnnounceFetchRD"; 1.42 + 1.43 + protected final long startTime; 1.44 + protected AnnouncementsFetchDelegate delegate; 1.45 + 1.46 + public AnnouncementsFetchResourceDelegate(Resource resource, AnnouncementsFetchDelegate delegate) { 1.47 + super(resource); 1.48 + this.startTime = System.currentTimeMillis(); 1.49 + this.delegate = delegate; 1.50 + } 1.51 + 1.52 + @Override 1.53 + public String getUserAgent() { 1.54 + return delegate.getUserAgent(); 1.55 + } 1.56 + 1.57 + @Override 1.58 + public void addHeaders(HttpRequestBase request, DefaultHttpClient client) { 1.59 + super.addHeaders(request, client); 1.60 + 1.61 + // The basics. 1.62 + request.addHeader("Accept-Language", delegate.getLocale().toString()); 1.63 + request.addHeader("Accept", ACCEPT_HEADER); 1.64 + 1.65 + // We never want to keep connections alive. 1.66 + request.addHeader("Connection", "close"); 1.67 + 1.68 + // Set If-Modified-Since to avoid re-fetching content. 1.69 + final String ifModifiedSince = delegate.getLastDate(); 1.70 + if (ifModifiedSince != null) { 1.71 + Logger.info(LOG_TAG, "If-Modified-Since: " + ifModifiedSince); 1.72 + request.addHeader("If-Modified-Since", ifModifiedSince); 1.73 + } 1.74 + 1.75 + // Just in case. 1.76 + request.removeHeaders("Cookie"); 1.77 + } 1.78 + 1.79 + private List<Announcement> parseBody(ExtendedJSONObject body) throws NonArrayJSONException { 1.80 + List<Announcement> out = new ArrayList<Announcement>(1); 1.81 + JSONArray snippets = body.getArray("announcements"); 1.82 + if (snippets == null) { 1.83 + Logger.warn(LOG_TAG, "Missing announcements body. Returning empty."); 1.84 + return out; 1.85 + } 1.86 + 1.87 + for (Object s : snippets) { 1.88 + try { 1.89 + out.add(Announcement.parseAnnouncement(new ExtendedJSONObject((JSONObject) s))); 1.90 + } catch (Exception e) { 1.91 + Logger.warn(LOG_TAG, "Malformed announcement or display failed. Skipping.", e); 1.92 + } 1.93 + } 1.94 + return out; 1.95 + } 1.96 + 1.97 + @Override 1.98 + public void handleHttpResponse(HttpResponse response) { 1.99 + final Header dateHeader = response.getFirstHeader(HTTP.DATE_HEADER); 1.100 + String date = null; 1.101 + if (dateHeader != null) { 1.102 + // Note that we are deliberately not validating the server time here. 1.103 + // We pass it directly back to the server; we don't care about the 1.104 + // contents, and if we reject a value we essentially re-initialize 1.105 + // the client, which will cause stale announcements to be re-fetched. 1.106 + date = dateHeader.getValue(); 1.107 + } 1.108 + if (date == null) { 1.109 + // Use local clock, because skipping is better than re-fetching. 1.110 + date = DateUtils.formatDate(new Date()); 1.111 + Logger.warn(LOG_TAG, "No fetch date; using local time " + date); 1.112 + } 1.113 + 1.114 + final SyncResponse r = new SyncResponse(response); // For convenience. 1.115 + try { 1.116 + final int statusCode = r.getStatusCode(); 1.117 + Logger.debug(LOG_TAG, "Got announcements response: " + statusCode); 1.118 + 1.119 + if (statusCode == 204 || statusCode == 304) { 1.120 + BaseResource.consumeEntity(response); 1.121 + delegate.onNoNewAnnouncements(startTime, date); 1.122 + return; 1.123 + } 1.124 + 1.125 + if (statusCode == 200) { 1.126 + final List<Announcement> snippets; 1.127 + try { 1.128 + snippets = parseBody(r.jsonObjectBody()); 1.129 + } catch (Exception e) { 1.130 + delegate.onRemoteError(e); 1.131 + return; 1.132 + } 1.133 + delegate.onNewAnnouncements(snippets, startTime, date); 1.134 + return; 1.135 + } 1.136 + 1.137 + if (statusCode == 400 || statusCode == 405) { 1.138 + // We did something wrong. 1.139 + Logger.warn(LOG_TAG, "We did something wrong. Oh dear."); 1.140 + // Fall through. 1.141 + } 1.142 + 1.143 + if (statusCode == 503 || statusCode == 500) { 1.144 + Logger.warn(LOG_TAG, "Server issue: " + r.body()); 1.145 + delegate.onBackoff(r.retryAfterInSeconds()); 1.146 + return; 1.147 + } 1.148 + 1.149 + // Otherwise, clean up. 1.150 + delegate.onRemoteFailure(statusCode); 1.151 + 1.152 + } catch (Exception e) { 1.153 + Logger.warn(LOG_TAG, "Failed to extract body.", e); 1.154 + delegate.onRemoteError(e); 1.155 + } 1.156 + } 1.157 + 1.158 + @Override 1.159 + public void handleHttpProtocolException(ClientProtocolException e) { 1.160 + Logger.warn(LOG_TAG, "Protocol exception.", e); 1.161 + delegate.onLocalError(e); 1.162 + } 1.163 + 1.164 + @Override 1.165 + public void handleHttpIOException(IOException e) { 1.166 + Logger.warn(LOG_TAG, "IO exception.", e); 1.167 + delegate.onLocalError(e); 1.168 + } 1.169 + 1.170 + @Override 1.171 + public void handleTransportException(GeneralSecurityException e) { 1.172 + Logger.warn(LOG_TAG, "Transport exception.", e); 1.173 + // Class this as a remote error, because it's probably something odd 1.174 + // with SSL negotiation. 1.175 + delegate.onRemoteError(e); 1.176 + } 1.177 + 1.178 + /** 1.179 + * Be very thorough in case the superclass implementation changes. 1.180 + * We never want this to be an authenticated request. 1.181 + */ 1.182 + @Override 1.183 + public AuthHeaderProvider getAuthHeaderProvider() { 1.184 + return null; 1.185 + } 1.186 +}