summaryrefslogtreecommitdiffstats
path: root/mobile/android/base/java/org/mozilla/gecko/dlc/BaseAction.java
blob: 28d6b238df6e26c9069a9c370062418ae09f645c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
/* -*- 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);
        }
    }
}