fdroidclient / F-Droid / src / org / fdroid / fdroid / net / ApkDownloader.java @ 69ecaf02
History | View | Annotate | Download (10 KB)
1 |
/*
|
---|---|
2 |
* Copyright (C) 2010-2012 Ciaran Gultnieks <ciaran@ciarang.com>
|
3 |
* Copyright (C) 2011 Henrik Tunedal <tunedal@gmail.com>
|
4 |
*
|
5 |
* This program is free software; you can redistribute it and/or
|
6 |
* modify it under the terms of the GNU General Public License
|
7 |
* as published by the Free Software Foundation; either version 3
|
8 |
* of the License, or (at your option) any later version.
|
9 |
*
|
10 |
* This program is distributed in the hope that it will be useful,
|
11 |
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
12 |
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
13 |
* GNU General Public License for more details.
|
14 |
*
|
15 |
* You should have received a copy of the GNU General Public License
|
16 |
* along with this program; if not, write to the Free Software
|
17 |
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
18 |
* MA 02110-1301, USA.
|
19 |
*/
|
20 |
|
21 |
package org.fdroid.fdroid.net; |
22 |
|
23 |
import android.content.Context; |
24 |
import android.os.Build; |
25 |
import android.content.Intent; |
26 |
import android.os.Bundle; |
27 |
import android.support.annotation.NonNull; |
28 |
import android.support.v4.content.LocalBroadcastManager; |
29 |
import android.util.Log; |
30 |
|
31 |
import org.fdroid.fdroid.Hasher; |
32 |
import org.fdroid.fdroid.Preferences; |
33 |
import org.fdroid.fdroid.ProgressListener; |
34 |
import org.fdroid.fdroid.Utils; |
35 |
import org.fdroid.fdroid.compat.FileCompat; |
36 |
import org.fdroid.fdroid.data.Apk; |
37 |
import org.fdroid.fdroid.data.App; |
38 |
import org.fdroid.fdroid.data.SanitizedFile; |
39 |
|
40 |
import java.io.File; |
41 |
import java.io.IOException; |
42 |
import java.net.URL; |
43 |
import java.security.NoSuchAlgorithmException; |
44 |
|
45 |
/**
|
46 |
* Downloads and verifies (against the Apk.hash) the apk file.
|
47 |
* If the file has previously been downloaded, it will make use of that
|
48 |
* instead, without going to the network to download a new one.
|
49 |
*/
|
50 |
public class ApkDownloader implements AsyncDownloader.Listener { |
51 |
|
52 |
private static final String TAG = "ApkDownloader"; |
53 |
|
54 |
public static final String EVENT_APK_DOWNLOAD_COMPLETE = "apkDownloadComplete"; |
55 |
public static final String EVENT_APK_DOWNLOAD_CANCELLED = "apkDownloadCancelled"; |
56 |
public static final String EVENT_ERROR = "apkDownloadError"; |
57 |
|
58 |
public static final String ACTION_STATUS = "apkDownloadStatus"; |
59 |
public static final String EXTRA_TYPE = "apkDownloadStatusType"; |
60 |
public static final String EXTRA_URL = "apkDownloadUrl"; |
61 |
|
62 |
public static final int ERROR_HASH_MISMATCH = 101; |
63 |
public static final int ERROR_DOWNLOAD_FAILED = 102; |
64 |
|
65 |
private static final String EVENT_SOURCE_ID = "sourceId"; |
66 |
private static long downloadIdCounter = 0; |
67 |
|
68 |
/**
|
69 |
* Used as a key to pass data through with an error event, explaining the type of event.
|
70 |
*/
|
71 |
public static final String EVENT_DATA_ERROR_TYPE = "apkDownloadErrorType"; |
72 |
|
73 |
@NonNull private final App app; |
74 |
@NonNull private final Apk curApk; |
75 |
@NonNull private final Context context; |
76 |
@NonNull private final String repoAddress; |
77 |
@NonNull private final SanitizedFile localFile; |
78 |
@NonNull private final SanitizedFile potentiallyCachedFile; |
79 |
|
80 |
private ProgressListener listener;
|
81 |
private AsyncDownloader dlWrapper = null; |
82 |
private boolean isComplete = false; |
83 |
|
84 |
private final long id = ++downloadIdCounter; |
85 |
|
86 |
public void setProgressListener(ProgressListener listener) { |
87 |
this.listener = listener;
|
88 |
} |
89 |
|
90 |
public void removeProgressListener() { |
91 |
setProgressListener(null);
|
92 |
} |
93 |
|
94 |
public ApkDownloader(@NonNull final Context context, @NonNull final App app, @NonNull final Apk apk, @NonNull final String repoAddress) { |
95 |
this.context = context;
|
96 |
this.app = app;
|
97 |
curApk = apk; |
98 |
this.repoAddress = repoAddress;
|
99 |
localFile = new SanitizedFile(Utils.getApkDownloadDir(context), apk.apkName);
|
100 |
potentiallyCachedFile = new SanitizedFile(Utils.getApkCacheDir(context), apk.apkName);
|
101 |
} |
102 |
|
103 |
/**
|
104 |
* The downloaded APK. Valid only when getStatus() has returned STATUS.DONE.
|
105 |
*/
|
106 |
public SanitizedFile localFile() {
|
107 |
return localFile;
|
108 |
} |
109 |
|
110 |
/**
|
111 |
* When stopping/starting downloaders multiple times (on different threads), it can
|
112 |
* get weird whereby different threads are sending progress events. It is important
|
113 |
* to be able to see which downloader these progress events are coming from.
|
114 |
*/
|
115 |
public boolean isEventFromThis(Event event) { |
116 |
return event.getData().containsKey(EVENT_SOURCE_ID) && event.getData().getLong(EVENT_SOURCE_ID) == id;
|
117 |
} |
118 |
|
119 |
private Hasher createHasher(File apkFile) { |
120 |
Hasher hasher; |
121 |
try {
|
122 |
hasher = new Hasher(curApk.hashType, apkFile);
|
123 |
} catch (NoSuchAlgorithmException e) { |
124 |
Log.e(TAG, "Error verifying hash of cached apk at " + apkFile + ". " + |
125 |
"I don't understand what the " + curApk.hashType + " hash algorithm is :("); |
126 |
hasher = null;
|
127 |
} |
128 |
return hasher;
|
129 |
} |
130 |
|
131 |
private boolean hashMatches(@NonNull final File apkFile) { |
132 |
if (!apkFile.exists()) {
|
133 |
return false; |
134 |
} |
135 |
Hasher hasher = createHasher(apkFile); |
136 |
return hasher != null && hasher.match(curApk.hash); |
137 |
} |
138 |
|
139 |
/**
|
140 |
* If an existing cached version exists, and matches the hash of the apk we
|
141 |
* want to download, then we will return true. Otherwise, we return false
|
142 |
* (and remove the cached file - if it exists and didn't match the correct hash).
|
143 |
*/
|
144 |
private boolean verifyOrDelete(@NonNull final File apkFile) { |
145 |
if (apkFile.exists()) {
|
146 |
if (hashMatches(apkFile)) {
|
147 |
Utils.DebugLog(TAG, "Using cached apk at " + apkFile);
|
148 |
return true; |
149 |
} |
150 |
Utils.DebugLog(TAG, "Not using cached apk at " + apkFile + "(hash doesn't match, will delete file)"); |
151 |
delete(apkFile); |
152 |
} |
153 |
return false; |
154 |
} |
155 |
|
156 |
private void delete(@NonNull final File file) { |
157 |
if (file.exists()) {
|
158 |
if (!file.delete()) {
|
159 |
Log.w(TAG, "Could not delete file " + file);
|
160 |
} |
161 |
} |
162 |
} |
163 |
|
164 |
private void prepareApkFileAndSendCompleteMessage() { |
165 |
|
166 |
// Need the apk to be world readable, so that the installer is able to read it.
|
167 |
// Note that saving it into external storage for the purpose of letting the installer
|
168 |
// have access is insecure, because apps with permission to write to the external
|
169 |
// storage can overwrite the app between F-Droid asking for it to be installed and
|
170 |
// the installer actually installing it.
|
171 |
FileCompat.setReadable(localFile, true, false); |
172 |
|
173 |
isComplete = true;
|
174 |
sendMessage(EVENT_APK_DOWNLOAD_COMPLETE); |
175 |
} |
176 |
|
177 |
public boolean isComplete() { |
178 |
return this.isComplete; |
179 |
} |
180 |
|
181 |
/**
|
182 |
* If the download successfully spins up a new thread to start downloading, then we return
|
183 |
* true, otherwise false. This is useful, e.g. when we use a cached version, and so don't
|
184 |
* want to bother with progress dialogs et al.
|
185 |
*/
|
186 |
public boolean download() { |
187 |
|
188 |
// Can we use the cached version?
|
189 |
if (verifyOrDelete(potentiallyCachedFile)) {
|
190 |
delete(localFile); |
191 |
Utils.copy(potentiallyCachedFile, localFile); |
192 |
prepareApkFileAndSendCompleteMessage(); |
193 |
return false; |
194 |
} |
195 |
|
196 |
String remoteAddress = Utils.getApkUrl(repoAddress, curApk);
|
197 |
Utils.DebugLog(TAG, "Downloading apk from " + remoteAddress + " to " + localFile); |
198 |
|
199 |
try {
|
200 |
dlWrapper = DownloaderFactory.createAsync(context, remoteAddress, localFile, app.name + " " + curApk.version, curApk.id, this); |
201 |
dlWrapper.download(); |
202 |
return true; |
203 |
} catch (IOException e) { |
204 |
onErrorDownloading(e.getLocalizedMessage()); |
205 |
} |
206 |
|
207 |
return false; |
208 |
} |
209 |
|
210 |
private void sendMessage(String type) { |
211 |
sendProgressEvent(new ProgressListener.Event(type));
|
212 |
} |
213 |
|
214 |
private void sendError(int errorType) { |
215 |
Bundle data = new Bundle(1); |
216 |
data.putInt(EVENT_DATA_ERROR_TYPE, errorType); |
217 |
sendProgressEvent(new Event(EVENT_ERROR, data)); |
218 |
} |
219 |
|
220 |
// TODO: Completely remove progress listener, only use broadcasts...
|
221 |
private void sendProgressEvent(Event event) { |
222 |
|
223 |
event.getData().putLong(EVENT_SOURCE_ID, id); |
224 |
|
225 |
if (listener != null) { |
226 |
listener.onProgress(event); |
227 |
} |
228 |
|
229 |
Intent intent = new Intent(ACTION_STATUS);
|
230 |
intent.putExtras(event.getData()); |
231 |
intent.putExtra(EXTRA_TYPE, event.type); |
232 |
intent.putExtra(EXTRA_URL, Utils.getApkUrl(repoAddress, curApk)); |
233 |
LocalBroadcastManager.getInstance(context).sendBroadcast(intent); |
234 |
} |
235 |
|
236 |
@Override
|
237 |
public void onErrorDownloading(String localisedExceptionDetails) { |
238 |
Log.e(TAG, "Download failed: " + localisedExceptionDetails);
|
239 |
sendError(ERROR_DOWNLOAD_FAILED); |
240 |
delete(localFile); |
241 |
} |
242 |
|
243 |
private void cacheIfRequired() { |
244 |
if (Preferences.get().shouldCacheApks()) { |
245 |
Utils.DebugLog(TAG, "Copying .apk file to cache at " + potentiallyCachedFile.getAbsolutePath());
|
246 |
Utils.copy(localFile, potentiallyCachedFile); |
247 |
} |
248 |
} |
249 |
|
250 |
@Override
|
251 |
public void onDownloadComplete() { |
252 |
|
253 |
if (!verifyOrDelete(localFile)) {
|
254 |
sendError(ERROR_HASH_MISMATCH); |
255 |
return;
|
256 |
} |
257 |
|
258 |
cacheIfRequired(); |
259 |
|
260 |
Utils.DebugLog(TAG, "Download finished: " + localFile);
|
261 |
prepareApkFileAndSendCompleteMessage(); |
262 |
} |
263 |
|
264 |
@Override
|
265 |
public void onDownloadCancelled() { |
266 |
sendMessage(EVENT_APK_DOWNLOAD_CANCELLED); |
267 |
} |
268 |
|
269 |
@Override
|
270 |
public void onProgress(Event event) { |
271 |
sendProgressEvent(event); |
272 |
} |
273 |
|
274 |
/**
|
275 |
* Attempts to cancel the download (if in progress) and also removes the progress
|
276 |
* listener
|
277 |
*
|
278 |
* @param userRequested - true if the user requested the cancel (via button click), otherwise false.
|
279 |
*/
|
280 |
public void cancel(boolean userRequested) { |
281 |
if (dlWrapper != null) { |
282 |
dlWrapper.attemptCancel(userRequested); |
283 |
} |
284 |
} |
285 |
|
286 |
public Apk getApk() { return curApk; } |
287 |
|
288 |
public int getBytesRead() { return dlWrapper != null ? dlWrapper.getBytesRead() : 0; } |
289 |
|
290 |
public int getTotalBytes() { return dlWrapper != null ? dlWrapper.getTotalBytes() : 0; } |
291 |
} |