--- a/mobile/android/base/java/org/mozilla/gecko/dlc/DownloadAction.java
+++ b/mobile/android/base/java/org/mozilla/gecko/dlc/DownloadAction.java
@@ -21,35 +21,34 @@ import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
import java.util.zip.GZIPInputStream;
-import ch.boye.httpclientandroidlib.HttpEntity;
-import ch.boye.httpclientandroidlib.HttpResponse;
-import ch.boye.httpclientandroidlib.HttpStatus;
-import ch.boye.httpclientandroidlib.client.HttpClient;
-import ch.boye.httpclientandroidlib.client.methods.HttpGet;
-import ch.boye.httpclientandroidlib.impl.client.DefaultHttpRequestRetryHandler;
-import ch.boye.httpclientandroidlib.impl.client.HttpClientBuilder;
-
/**
* Download content that has been scheduled during "study" or "verify".
*/
public class DownloadAction extends BaseAction {
private static final String LOGTAG = "DLCDownloadAction";
private static final String CACHE_DIRECTORY = "downloadContent";
+
private static final String CDN_BASE_URL = "https://mobile.cdn.mozilla.net/";
+ private static final int STATUS_OK = 200;
+ private static final int STATUS_PARTIAL_CONTENT = 206;
+
public interface Callback {
void onContentDownloaded(DownloadContent content);
}
private Callback callback;
public DownloadAction(Callback callback) {
this.callback = callback;
@@ -65,18 +64,16 @@ public class DownloadAction extends Base
}
if (isActiveNetworkMetered(context)) {
Log.d(LOGTAG, "Network is metered. Postponing download.");
// TODO: Reschedule download (bug 1209498)
return;
}
- final HttpClient client = buildHttpClient();
-
for (DownloadContent content : catalog.getScheduledDownloads()) {
Log.d(LOGTAG, "Downloading: " + content);
File temporaryFile = null;
try {
File destinationFile = getDestinationFile(context, content);
if (destinationFile.exists() && verify(destinationFile, content.getChecksum())) {
@@ -91,17 +88,17 @@ public class DownloadAction extends Base
Log.d(LOGTAG, "Not enough disk space to save content. Skipping download.");
continue;
}
// TODO: Check space on disk before downloading content (bug 1220145)
final String url = createDownloadURL(content);
if (!temporaryFile.exists() || temporaryFile.length() < content.getSize()) {
- download(client, url, temporaryFile);
+ download(url, temporaryFile);
}
if (!verify(temporaryFile, content.getDownloadChecksum())) {
Log.w(LOGTAG, "Wrong checksum after download, content=" + content.getId());
temporaryFile.delete();
continue;
}
@@ -141,68 +138,68 @@ public class DownloadAction extends Base
temporaryFile.delete();
}
}
}
Log.v(LOGTAG, "Done");
}
- protected void download(HttpClient client, String source, File temporaryFile)
+ protected void download(String source, File temporaryFile)
throws RecoverableDownloadContentException, UnrecoverableDownloadContentException {
InputStream inputStream = null;
OutputStream outputStream = null;
- final HttpGet request = new HttpGet(source);
-
- final long offset = temporaryFile.exists() ? temporaryFile.length() : 0;
- if (offset > 0) {
- request.setHeader("Range", "bytes=" + offset + "-");
- }
+ HttpURLConnection connection = null;
try {
- final HttpResponse response = client.execute(request);
- final int status = response.getStatusLine().getStatusCode();
- if (status != HttpStatus.SC_OK && status != HttpStatus.SC_PARTIAL_CONTENT) {
+ connection = buildHttpURLConnection(source);
+
+ final long offset = temporaryFile.exists() ? temporaryFile.length() : 0;
+ if (offset > 0) {
+ connection.setRequestProperty("Range", "bytes=" + offset + "-");
+ }
+
+ final int status = connection.getResponseCode();
+ if (status != STATUS_OK && status != STATUS_PARTIAL_CONTENT) {
// We are trying to be smart and only retry if this is an error that might resolve in the future.
// TODO: This is guesstimating at best. We want to implement failure counters (Bug 1215106).
if (status >= 500) {
// Recoverable: Server errors 5xx
throw new RecoverableDownloadContentException(RecoverableDownloadContentException.SERVER,
"(Recoverable) Download failed. Status code: " + status);
} else if (status >= 400) {
// Unrecoverable: Client errors 4xx - Unlikely that this version of the client will ever succeed.
throw new UnrecoverableDownloadContentException("(Unrecoverable) Download failed. Status code: " + status);
} else {
+ // HttpsUrlConnection: -1 (No valid response code)
// Informational 1xx: They have no meaning to us.
// Successful 2xx: We don't know how to handle anything but 200.
// Redirection 3xx: HttpClient should have followed redirects if possible. We should not see those errors here.
throw new UnrecoverableDownloadContentException("(Unrecoverable) Download failed. Status code: " + status);
}
}
- final HttpEntity entity = response.getEntity();
- if (entity == null) {
- // Recoverable: Should not happen for a valid asset
- throw new RecoverableDownloadContentException(RecoverableDownloadContentException.SERVER, "Null entity");
- }
-
- inputStream = new BufferedInputStream(entity.getContent());
- outputStream = openFile(temporaryFile, status == HttpStatus.SC_PARTIAL_CONTENT);
+ inputStream = new BufferedInputStream(connection.getInputStream());
+ outputStream = openFile(temporaryFile, status == STATUS_PARTIAL_CONTENT);
IOUtils.copy(inputStream, outputStream);
inputStream.close();
outputStream.close();
} catch (IOException e) {
// Recoverable: Just I/O discontinuation
throw new RecoverableDownloadContentException(RecoverableDownloadContentException.NETWORK, e);
} finally {
IOUtils.safeStreamClose(inputStream);
IOUtils.safeStreamClose(outputStream);
+
+ if (connection != null) {
+ connection.disconnect();
+ }
}
}
protected OutputStream openFile(File file, boolean append) throws FileNotFoundException {
return new BufferedOutputStream(new FileOutputStream(file, append));
}
protected void extract(File sourceFile, File destinationFile, String checksum)
@@ -253,24 +250,29 @@ public class DownloadAction extends Base
return networkInfo != null && networkInfo.isConnected();
}
protected boolean isActiveNetworkMetered(Context context) {
return ConnectivityManagerCompat.isActiveNetworkMetered(
(ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE));
}
- protected HttpClient buildHttpClient() {
+ protected HttpURLConnection buildHttpURLConnection(String url)
+ throws UnrecoverableDownloadContentException, IOException {
// TODO: Implement proxy support (Bug 1209496)
- return HttpClientBuilder.create()
- .setUserAgent(HardwareUtils.isTablet() ?
- AppConstants.USER_AGENT_FENNEC_TABLET :
- AppConstants.USER_AGENT_FENNEC_MOBILE)
- .setRetryHandler(new DefaultHttpRequestRetryHandler())
- .build();
+ try {
+ HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
+ connection.setRequestProperty("User-Agent", HardwareUtils.isTablet() ?
+ AppConstants.USER_AGENT_FENNEC_TABLET :
+ AppConstants.USER_AGENT_FENNEC_MOBILE);
+ connection.setRequestMethod("GET");
+ return connection;
+ } catch (MalformedURLException e) {
+ throw new UnrecoverableDownloadContentException(e);
+ }
}
protected String createDownloadURL(DownloadContent content) {
return CDN_BASE_URL + content.getLocation();
}
protected File createTemporaryFile(Context context, DownloadContent content)
throws RecoverableDownloadContentException {
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/dlc/TestDownloadAction.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/dlc/TestDownloadAction.java
@@ -1,85 +1,79 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.dlc;
+import android.content.Context;
+
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
import org.mozilla.gecko.background.testhelpers.TestRunner;
import org.mozilla.gecko.dlc.catalog.DownloadContent;
import org.mozilla.gecko.dlc.catalog.DownloadContentCatalog;
-
-import android.content.Context;
-
import org.robolectric.RuntimeEnvironment;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
+import java.net.HttpURLConnection;
import java.util.Arrays;
import java.util.Collections;
-import ch.boye.httpclientandroidlib.HttpEntity;
-import ch.boye.httpclientandroidlib.HttpResponse;
-import ch.boye.httpclientandroidlib.HttpStatus;
-import ch.boye.httpclientandroidlib.StatusLine;
-import ch.boye.httpclientandroidlib.client.HttpClient;
-import ch.boye.httpclientandroidlib.client.methods.HttpGet;
-import ch.boye.httpclientandroidlib.client.methods.HttpUriRequest;
-
import static org.mockito.Matchers.*;
import static org.mockito.Mockito.*;
/**
* DownloadAction: Download content that has been scheduled during "study" or "verify".
*/
@RunWith(TestRunner.class)
public class TestDownloadAction {
private static final String TEST_URL = "http://example.org";
+ private static final int STATUS_OK = 200;
+ private static final int STATUS_PARTIAL_CONTENT = 206;
+
/**
* Scenario: The current network is metered.
*
* Verify that:
* * No download is performed on a metered network
*/
@Test
public void testNothingIsDoneOnMeteredNetwork() throws Exception {
DownloadAction action = spy(new DownloadAction(null));
doReturn(true).when(action).isActiveNetworkMetered(RuntimeEnvironment.application);
action.perform(RuntimeEnvironment.application, null);
- verify(action, never()).buildHttpClient();
- verify(action, never()).download(any(HttpClient.class), anyString(), any(File.class));
+ verify(action, never()).buildHttpURLConnection(anyString());
+ verify(action, never()).download(anyString(), any(File.class));
}
/**
* Scenario: No (connected) network is available.
*
* Verify that:
* * No download is performed
*/
@Test
public void testNothingIsDoneIfNoNetworkIsAvailable() throws Exception {
DownloadAction action = spy(new DownloadAction(null));
doReturn(false).when(action).isConnectedToNetwork(RuntimeEnvironment.application);
action.perform(RuntimeEnvironment.application, null);
verify(action, never()).isActiveNetworkMetered(any(Context.class));
- verify(action, never()).buildHttpClient();
- verify(action, never()).download(any(HttpClient.class), anyString(), any(File.class));
+ verify(action, never()).buildHttpURLConnection(anyString());
+ verify(action, never()).download(anyString(), any(File.class));
}
/**
* Scenario: Content is scheduled for download but already exists locally (with correct checksum).
*
* Verify that:
* * No download is performed for existing file
* * Content is marked as downloaded in the catalog
@@ -97,56 +91,58 @@ public class TestDownloadAction {
File file = mock(File.class);
doReturn(true).when(file).exists();
doReturn(file).when(action).createTemporaryFile(RuntimeEnvironment.application, content);
doReturn(file).when(action).getDestinationFile(RuntimeEnvironment.application, content);
doReturn(true).when(action).verify(eq(file), anyString());
action.perform(RuntimeEnvironment.application, catalog);
- verify(action, never()).download(any(HttpClient.class), anyString(), any(File.class));
+ verify(action, never()).download(anyString(), any(File.class));
verify(catalog).markAsDownloaded(content);
}
/**
* Scenario: Server returns a server error (HTTP 500).
*
* Verify that:
* * Situation is treated as recoverable (RecoverableDownloadContentException)
*/
@Test(expected=BaseAction.RecoverableDownloadContentException.class)
public void testServerErrorsAreRecoverable() throws Exception {
- HttpClient client = mockHttpClient(500, "");
+ HttpURLConnection connection = mockHttpURLConnection(500, "");
File temporaryFile = mock(File.class);
doReturn(false).when(temporaryFile).exists();
DownloadAction action = spy(new DownloadAction(null));
- action.download(client, TEST_URL, temporaryFile);
+ doReturn(connection).when(action).buildHttpURLConnection(anyString());
+ action.download(TEST_URL, temporaryFile);
- verify(client).execute(any(HttpUriRequest.class));
+ verify(connection).getInputStream();
}
/**
* Scenario: Server returns a client error (HTTP 404).
*
* Verify that:
* * Situation is treated as unrecoverable (UnrecoverableDownloadContentException)
*/
@Test(expected=BaseAction.UnrecoverableDownloadContentException.class)
public void testClientErrorsAreUnrecoverable() throws Exception {
- HttpClient client = mockHttpClient(404, "");
+ HttpURLConnection connection = mockHttpURLConnection(404, "");
File temporaryFile = mock(File.class);
doReturn(false).when(temporaryFile).exists();
DownloadAction action = spy(new DownloadAction(null));
- action.download(client, TEST_URL, temporaryFile);
+ doReturn(connection).when(action).buildHttpURLConnection(anyString());
+ action.download(TEST_URL, temporaryFile);
- verify(client).execute(any(HttpUriRequest.class));
+ verify(connection).getInputStream();
}
/**
* Scenario: A successful download has been performed.
*
* Verify that:
* * The content will be extracted to the destination
* * The content is marked as downloaded in the catalog
@@ -164,24 +160,23 @@ public class TestDownloadAction {
DownloadAction action = spy(new DownloadAction(null));
doReturn(false).when(action).isActiveNetworkMetered(RuntimeEnvironment.application);
File file = mockNotExistingFile();
doReturn(file).when(action).createTemporaryFile(RuntimeEnvironment.application, content);
doReturn(file).when(action).getDestinationFile(RuntimeEnvironment.application, content);
doReturn(false).when(action).verify(eq(file), anyString());
- doNothing().when(action).download(any(HttpClient.class), anyString(), eq(file));
+ doNothing().when(action).download(anyString(), eq(file));
doReturn(true).when(action).verify(eq(file), anyString());
doNothing().when(action).extract(eq(file), eq(file), anyString());
action.perform(RuntimeEnvironment.application, catalog);
- verify(action).buildHttpClient();
- verify(action).download(any(HttpClient.class), anyString(), eq(file));
+ verify(action).download(anyString(), eq(file));
verify(action).extract(eq(file), eq(file), anyString());
verify(catalog).markAsDownloaded(content);
}
/**
* Scenario: Pretend a partially downloaded file already exists.
*
* Verify that:
@@ -204,33 +199,30 @@ public class TestDownloadAction {
doReturn(false).when(action).isActiveNetworkMetered(RuntimeEnvironment.application);
File temporaryFile = mockFileWithSize(1337L);
doReturn(temporaryFile).when(action).createTemporaryFile(RuntimeEnvironment.application, content);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
doReturn(outputStream).when(action).openFile(eq(temporaryFile), anyBoolean());
- HttpClient client = mockHttpClient(HttpStatus.SC_PARTIAL_CONTENT, "HelloWorld");
- doReturn(client).when(action).buildHttpClient();
+ HttpURLConnection connection = mockHttpURLConnection(STATUS_PARTIAL_CONTENT, "HelloWorld");
+ doReturn(connection).when(action).buildHttpURLConnection(anyString());
File destinationFile = mockNotExistingFile();
doReturn(destinationFile).when(action).getDestinationFile(RuntimeEnvironment.application, content);
doReturn(true).when(action).verify(eq(temporaryFile), anyString());
doNothing().when(action).extract(eq(temporaryFile), eq(destinationFile), anyString());
action.perform(RuntimeEnvironment.application, catalog);
- ArgumentCaptor<HttpGet> argument = ArgumentCaptor.forClass(HttpGet.class);
- verify(client).execute(argument.capture());
+ verify(connection).getInputStream();
+ verify(connection).setRequestProperty("Range", "bytes=1337-");
- HttpGet request = argument.getValue();
- Assert.assertTrue(request.containsHeader("Range"));
- Assert.assertEquals("bytes=1337-", request.getFirstHeader("Range").getValue());
Assert.assertEquals("HelloWorld", new String(outputStream.toByteArray(), "UTF-8"));
verify(action).openFile(eq(temporaryFile), eq(true));
verify(catalog).markAsDownloaded(content);
verify(temporaryFile).delete();
}
/**
@@ -256,18 +248,18 @@ public class TestDownloadAction {
File temporaryFile = mockFileWithSize(1337L);
doReturn(temporaryFile).when(action).createTemporaryFile(RuntimeEnvironment.application, content);
ByteArrayOutputStream outputStream = spy(new ByteArrayOutputStream());
doReturn(outputStream).when(action).openFile(eq(temporaryFile), anyBoolean());
doThrow(IOException.class).when(outputStream).write(any(byte[].class), anyInt(), anyInt());
- HttpClient client = mockHttpClient(HttpStatus.SC_PARTIAL_CONTENT, "HelloWorld");
- doReturn(client).when(action).buildHttpClient();
+ HttpURLConnection connection = mockHttpURLConnection(STATUS_PARTIAL_CONTENT, "HelloWorld");
+ doReturn(connection).when(action).buildHttpURLConnection(anyString());
doReturn(mockNotExistingFile()).when(action).getDestinationFile(RuntimeEnvironment.application, content);
action.perform(RuntimeEnvironment.application, catalog);
verify(catalog, never()).markAsDownloaded(content);
verify(action, never()).verify(any(File.class), anyString());
verify(temporaryFile, never()).delete();
@@ -301,17 +293,17 @@ public class TestDownloadAction {
File destinationFile = mockNotExistingFile();
doReturn(destinationFile).when(action).getDestinationFile(RuntimeEnvironment.application, content);
doReturn(true).when(action).verify(eq(temporaryFile), anyString());
doNothing().when(action).extract(eq(temporaryFile), eq(destinationFile), anyString());
action.perform(RuntimeEnvironment.application, catalog);
- verify(action, never()).download(any(HttpClient.class), anyString(), eq(temporaryFile));
+ verify(action, never()).download(anyString(), eq(temporaryFile));
verify(action).verify(eq(temporaryFile), anyString());
verify(action).extract(eq(temporaryFile), eq(destinationFile), anyString());
verify(catalog).markAsDownloaded(content);
}
/**
* Scenario: Download is completed but verification (checksum) failed.
*
@@ -328,17 +320,17 @@ public class TestDownloadAction {
.setSize(1337L)
.build();
DownloadContentCatalog catalog = mock(DownloadContentCatalog.class);
doReturn(Collections.singletonList(content)).when(catalog).getScheduledDownloads();
DownloadAction action = spy(new DownloadAction(null));
doReturn(false).when(action).isActiveNetworkMetered(RuntimeEnvironment.application);
- doNothing().when(action).download(any(HttpClient.class), anyString(), any(File.class));
+ doNothing().when(action).download(anyString(), any(File.class));
doReturn(false).when(action).verify(any(File.class), anyString());
File temporaryFile = mockNotExistingFile();
doReturn(temporaryFile).when(action).createTemporaryFile(RuntimeEnvironment.application, content);
File destinationFile = mockNotExistingFile();
doReturn(destinationFile).when(action).getDestinationFile(RuntimeEnvironment.application, content);
@@ -367,18 +359,18 @@ public class TestDownloadAction {
File temporaryFile = mockNotExistingFile();
doReturn(temporaryFile).when(action).createTemporaryFile(RuntimeEnvironment.application, content);
File destinationFile = mockNotExistingFile();
doReturn(destinationFile).when(action).getDestinationFile(RuntimeEnvironment.application, content);
doReturn(true).when(action).hasEnoughDiskSpace(content, destinationFile, temporaryFile);
- verify(action, never()).buildHttpClient();
- verify(action, never()).download(any(HttpClient.class), anyString(), any(File.class));
+ verify(action, never()).buildHttpURLConnection(anyString());
+ verify(action, never()).download(anyString(), any(File.class));
verify(action, never()).verify(any(File.class), anyString());
verify(catalog, never()).markAsDownloaded(content);
}
/**
* Scenario: Not enough storage space for temporary file available.
*
* Verify that:
@@ -439,19 +431,19 @@ public class TestDownloadAction {
DownloadAction action = spy(new DownloadAction(null));
doReturn(true).when(action).isConnectedToNetwork(RuntimeEnvironment.application);
doReturn(false).when(action).isActiveNetworkMetered(RuntimeEnvironment.application);
doReturn(mockNotExistingFile()).when(action).createTemporaryFile(RuntimeEnvironment.application, content);
doReturn(mockNotExistingFile()).when(action).getDestinationFile(RuntimeEnvironment.application, content);
doReturn(true).when(action).hasEnoughDiskSpace(eq(content), any(File.class), any(File.class));
- HttpClient client = mock(HttpClient.class);
- doThrow(IOException.class).when(client).execute(any(HttpUriRequest.class));
- doReturn(client).when(action).buildHttpClient();
+ HttpURLConnection connection = mockHttpURLConnection(STATUS_OK, "");
+ doThrow(IOException.class).when(connection).getInputStream();
+ doReturn(connection).when(action).buildHttpURLConnection(anyString());
action.perform(RuntimeEnvironment.application, catalog);
verify(catalog, never()).rememberFailure(eq(content), anyInt());
verify(catalog, never()).markAsDownloaded(content);
}
/**
@@ -471,17 +463,17 @@ public class TestDownloadAction {
Assert.assertEquals(DownloadContent.STATE_NONE, content.getState());
DownloadAction action = spy(new DownloadAction(null));
doReturn(true).when(action).isConnectedToNetwork(RuntimeEnvironment.application);
doReturn(false).when(action).isActiveNetworkMetered(RuntimeEnvironment.application);
doReturn(mockNotExistingFile()).when(action).createTemporaryFile(RuntimeEnvironment.application, content);
doReturn(mockNotExistingFile()).when(action).getDestinationFile(RuntimeEnvironment.application, content);
doReturn(true).when(action).hasEnoughDiskSpace(eq(content), any(File.class), any(File.class));
- doNothing().when(action).download(any(HttpClient.class), anyString(), any(File.class));
+ doNothing().when(action).download(anyString(), any(File.class));
doReturn(true).when(action).verify(any(File.class), anyString());
File destinationFile = mock(File.class);
doReturn(false).when(destinationFile).exists();
File parentFile = mock(File.class);
doReturn(false).when(parentFile).mkdirs();
doReturn(false).when(parentFile).exists();
doReturn(parentFile).when(destinationFile).getParentFile();
@@ -536,25 +528,17 @@ public class TestDownloadAction {
File parentFile = mock(File.class);
doReturn(usableSpace).when(parentFile).getUsableSpace();
doReturn(parentFile).when(file).getParentFile();
return file;
}
- private static HttpClient mockHttpClient(int statusCode, String content) throws Exception {
- StatusLine status = mock(StatusLine.class);
- doReturn(statusCode).when(status).getStatusCode();
-
- HttpEntity entity = mock(HttpEntity.class);
- doReturn(new ByteArrayInputStream(content.getBytes("UTF-8"))).when(entity).getContent();
+ private static HttpURLConnection mockHttpURLConnection(int statusCode, String content) throws Exception {
+ HttpURLConnection connection = mock(HttpURLConnection.class);
- HttpResponse response = mock(HttpResponse.class);
- doReturn(status).when(response).getStatusLine();
- doReturn(entity).when(response).getEntity();
+ doReturn(statusCode).when(connection).getResponseCode();
+ doReturn(new ByteArrayInputStream(content.getBytes("UTF-8"))).when(connection).getInputStream();
- HttpClient client = mock(HttpClient.class);
- doReturn(response).when(client).execute(any(HttpUriRequest.class));
-
- return client;
+ return connection;
}
}