/* -*- 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 android.support.annotation.IntDef; import android.util.Log; import org.mozilla.gecko.AppConstants; import org.mozilla.gecko.background.nativecode.NativeCrypto; import org.mozilla.gecko.dlc.catalog.DownloadContent; import org.mozilla.gecko.dlc.catalog.DownloadContentCatalog; import org.mozilla.gecko.sync.Utils; import org.mozilla.gecko.util.HardwareUtils; import org.mozilla.gecko.util.IOUtils; import org.mozilla.gecko.util.ProxySelector; import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; public abstract class BaseAction { private static final String LOGTAG = "GeckoDLCBaseAction"; /** * Exception indicating a recoverable error has happened. Download of the content will be retried later. */ /* package-private */ static class RecoverableDownloadContentException extends Exception { private static final long serialVersionUID = -2246772819507370734L; @IntDef({MEMORY, DISK_IO, SERVER, NETWORK}) public @interface ErrorType {} public static final int MEMORY = 1; public static final int DISK_IO = 2; public static final int SERVER = 3; public static final int NETWORK = 4; private int errorType; public RecoverableDownloadContentException(@ErrorType int errorType, String message) { super(message); this.errorType = errorType; } public RecoverableDownloadContentException(@ErrorType int errorType, Throwable cause) { super(cause); this.errorType = errorType; } @ErrorType public int getErrorType() { return errorType; } /** * Should this error be counted as failure? If this type of error will happen multiple times in a row then this * error will be treated as permanently and the operation will not be tried again until the content changes. */ public boolean shouldBeCountedAsFailure() { if (NETWORK == errorType) { return false; // Always retry after network errors } return true; } } /** * If this exception is thrown the content will be marked as unrecoverable, permanently failed and we will not try * downloading it again - until a newer version of the content is available. */ /* package-private */ static class UnrecoverableDownloadContentException extends Exception { private static final long serialVersionUID = 8956080754787367105L; public UnrecoverableDownloadContentException(String message) { super(message); } public UnrecoverableDownloadContentException(Throwable cause) { super(cause); } } public abstract void perform(Context context, DownloadContentCatalog catalog); protected File getDestinationFile(Context context, DownloadContent content) throws UnrecoverableDownloadContentException, RecoverableDownloadContentException { if (content.isFont()) { File destinationDirectory = new File(context.getApplicationInfo().dataDir, "fonts"); if (!destinationDirectory.exists() && !destinationDirectory.mkdirs()) { throw new RecoverableDownloadContentException(RecoverableDownloadContentException.DISK_IO, "Destination directory does not exist and cannot be created"); } return new File(destinationDirectory, content.getFilename()); } // Unrecoverable: We downloaded a file and we don't know what to do with it (Should not happen) throw new UnrecoverableDownloadContentException("Can't determine destination for kind: " + content.getKind()); } protected boolean verify(File file, String expectedChecksum) throws RecoverableDownloadContentException, UnrecoverableDownloadContentException { InputStream inputStream = null; try { inputStream = new BufferedInputStream(new FileInputStream(file)); byte[] ctx = NativeCrypto.sha256init(); if (ctx == null) { throw new RecoverableDownloadContentException(RecoverableDownloadContentException.MEMORY, "Could not create SHA-256 context"); } byte[] buffer = new byte[4096]; int read; while ((read = inputStream.read(buffer)) != -1) { NativeCrypto.sha256update(ctx, buffer, read); } String actualChecksum = Utils.byte2Hex(NativeCrypto.sha256finalize(ctx)); if (!actualChecksum.equalsIgnoreCase(expectedChecksum)) { Log.w(LOGTAG, "Checksums do not match. Expected=" + expectedChecksum + ", Actual=" + actualChecksum); return false; } return true; } catch (IOException e) { // Recoverable: Just I/O discontinuation throw new RecoverableDownloadContentException(RecoverableDownloadContentException.DISK_IO, e); } finally { IOUtils.safeStreamClose(inputStream); } } protected HttpURLConnection buildHttpURLConnection(String url) throws UnrecoverableDownloadContentException, IOException { try { System.setProperty("http.keepAlive", "true"); HttpURLConnection connection = (HttpURLConnection) ProxySelector.openConnectionWithProxy(new URI(url)); connection.setRequestProperty("User-Agent", HardwareUtils.isTablet() ? AppConstants.USER_AGENT_FENNEC_TABLET : AppConstants.USER_AGENT_FENNEC_MOBILE); connection.setRequestMethod("GET"); connection.setInstanceFollowRedirects(true); return connection; } catch (MalformedURLException e) { throw new UnrecoverableDownloadContentException(e); } catch (URISyntaxException e) { throw new UnrecoverableDownloadContentException(e); } } }