> getHeaders() {
251 | return Collections.unmodifiableList(mRequestHeaders);
252 | }
253 |
254 | public void sendIntentIfRequested() {
255 | if (mPackage == null) {
256 | return;
257 | }
258 |
259 | Intent intent;
260 | if (mIsPublicApi) {
261 | intent = new Intent(DownloadManager.ACTION_DOWNLOAD_COMPLETE);
262 | intent.setPackage(mPackage);
263 | intent.putExtra(DownloadManager.EXTRA_DOWNLOAD_ID, mId);
264 | } else { // legacy behavior
265 | if (mClass == null) {
266 | return;
267 | }
268 | intent = new Intent(Downloads.ACTION_DOWNLOAD_COMPLETED);
269 | intent.setClassName(mPackage, mClass);
270 | if (mExtras != null) {
271 | intent.putExtra(Downloads.COLUMN_NOTIFICATION_EXTRAS, mExtras);
272 | }
273 | // We only send the content: URI, for security reasons. Otherwise, malicious
274 | // applications would have an easier time spoofing download results by
275 | // sending spoofed intents.
276 | intent.setData(getMyDownloadsUri());
277 | }
278 | mSystemFacade.sendBroadcast(intent);
279 | }
280 |
281 | /**
282 | * Returns the time when a download should be restarted.
283 | */
284 | public long restartTime(long now) {
285 | if (mNumFailed == 0) {
286 | return now;
287 | }
288 | if (mRetryAfter > 0) {
289 | return mLastMod + mRetryAfter;
290 | }
291 | return mLastMod +
292 | Constants.RETRY_FIRST_DELAY *
293 | (1000 + mFuzz) * (1 << (mNumFailed - 1));
294 | }
295 |
296 | /**
297 | * Returns whether this download (which the download manager hasn't seen yet)
298 | * should be started.
299 | */
300 | private boolean isReadyToStart(long now) {
301 | if (mHasActiveThread) {
302 | // already running
303 | return false;
304 | }
305 | if (mControl == Downloads.CONTROL_PAUSED) {
306 | // the download is paused, so it's not going to start
307 | return false;
308 | }
309 | switch (mStatus) {
310 | case 0: // status hasn't been initialized yet, this is a new download
311 | case Downloads.STATUS_PENDING: // download is explicit marked as ready to start
312 | case Downloads.STATUS_RUNNING: // download interrupted (process killed etc) while
313 | // running, without a chance to update the database
314 | return true;
315 |
316 | case Downloads.STATUS_WAITING_FOR_NETWORK:
317 | case Downloads.STATUS_QUEUED_FOR_WIFI:
318 | return checkCanUseNetwork() == NETWORK_OK;
319 |
320 | case Downloads.STATUS_WAITING_TO_RETRY:
321 | // download was waiting for a delayed restart
322 | return restartTime(now) <= now;
323 | }
324 | return false;
325 | }
326 |
327 | /**
328 | * Returns whether this download has a visible notification after
329 | * completion.
330 | */
331 | public boolean hasCompletionNotification() {
332 | if (!Downloads.isStatusCompleted(mStatus)) {
333 | return false;
334 | }
335 | if (mVisibility == Downloads.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) {
336 | return true;
337 | }
338 | return false;
339 | }
340 |
341 | /**
342 | * Returns whether this download is allowed to use the network.
343 | * @return one of the NETWORK_* constants
344 | */
345 | public int checkCanUseNetwork() {
346 | Integer networkType = mSystemFacade.getActiveNetworkType();
347 | if (networkType == null) {
348 | return NETWORK_NO_CONNECTION;
349 | }
350 | if (!isRoamingAllowed() && mSystemFacade.isNetworkRoaming()) {
351 | return NETWORK_CANNOT_USE_ROAMING;
352 | }
353 | return checkIsNetworkTypeAllowed(networkType);
354 | }
355 |
356 | private boolean isRoamingAllowed() {
357 | if (mIsPublicApi) {
358 | return mAllowRoaming;
359 | } else { // legacy behavior
360 | return true;
361 | }
362 | }
363 |
364 | /**
365 | * @return a non-localized string appropriate for logging corresponding to one of the
366 | * NETWORK_* constants.
367 | */
368 | public String getLogMessageForNetworkError(int networkError) {
369 | switch (networkError) {
370 | case NETWORK_RECOMMENDED_UNUSABLE_DUE_TO_SIZE:
371 | return "download size exceeds recommended limit for mobile network";
372 |
373 | case NETWORK_UNUSABLE_DUE_TO_SIZE:
374 | return "download size exceeds limit for mobile network";
375 |
376 | case NETWORK_NO_CONNECTION:
377 | return "no network connection available";
378 |
379 | case NETWORK_CANNOT_USE_ROAMING:
380 | return "download cannot use the current network connection because it is roaming";
381 |
382 | case NETWORK_TYPE_DISALLOWED_BY_REQUESTOR:
383 | return "download was requested to not use the current network type";
384 |
385 | default:
386 | return "unknown error with network connectivity";
387 | }
388 | }
389 |
390 | /**
391 | * Check if this download can proceed over the given network type.
392 | * @param networkType a constant from ConnectivityManager.TYPE_*.
393 | * @return one of the NETWORK_* constants
394 | */
395 | private int checkIsNetworkTypeAllowed(int networkType) {
396 | if (mIsPublicApi) {
397 | int flag = translateNetworkTypeToApiFlag(networkType);
398 | if ((flag & mAllowedNetworkTypes) == 0) {
399 | return NETWORK_TYPE_DISALLOWED_BY_REQUESTOR;
400 | }
401 | }
402 | return checkSizeAllowedForNetwork(networkType);
403 | }
404 |
405 | /**
406 | * Translate a ConnectivityManager.TYPE_* constant to the corresponding
407 | * DownloadManager.Request.NETWORK_* bit flag.
408 | */
409 | private int translateNetworkTypeToApiFlag(int networkType) {
410 | switch (networkType) {
411 | case ConnectivityManager.TYPE_MOBILE:
412 | return DownloadManager.Request.NETWORK_MOBILE;
413 |
414 | case ConnectivityManager.TYPE_WIFI:
415 | return DownloadManager.Request.NETWORK_WIFI;
416 |
417 | default:
418 | return 0;
419 | }
420 | }
421 |
422 | /**
423 | * Check if the download's size prohibits it from running over the current network.
424 | * @return one of the NETWORK_* constants
425 | */
426 | private int checkSizeAllowedForNetwork(int networkType) {
427 | if (mTotalBytes <= 0) {
428 | return NETWORK_OK; // we don't know the size yet
429 | }
430 | if (networkType == ConnectivityManager.TYPE_WIFI) {
431 | return NETWORK_OK; // anything goes over wifi
432 | }
433 | Long maxBytesOverMobile = mSystemFacade.getMaxBytesOverMobile();
434 | if (maxBytesOverMobile != null && mTotalBytes > maxBytesOverMobile) {
435 | return NETWORK_UNUSABLE_DUE_TO_SIZE;
436 | }
437 | if (mBypassRecommendedSizeLimit == 0) {
438 | Long recommendedMaxBytesOverMobile = mSystemFacade.getRecommendedMaxBytesOverMobile();
439 | if (recommendedMaxBytesOverMobile != null
440 | && mTotalBytes > recommendedMaxBytesOverMobile) {
441 | return NETWORK_RECOMMENDED_UNUSABLE_DUE_TO_SIZE;
442 | }
443 | }
444 | return NETWORK_OK;
445 | }
446 |
447 | void startIfReady(long now) {
448 | if (!isReadyToStart(now)) {
449 | return;
450 | }
451 |
452 | if (Constants.LOGV) {
453 | Log.v(Constants.TAG, "Service spawning thread to handle download " + mId);
454 | }
455 | if (mHasActiveThread) {
456 | throw new IllegalStateException("Multiple threads on same download");
457 | }
458 | if (mStatus != Downloads.STATUS_RUNNING) {
459 | mStatus = Downloads.STATUS_RUNNING;
460 | ContentValues values = new ContentValues();
461 | values.put(Downloads.COLUMN_STATUS, mStatus);
462 | mContext.getContentResolver().update(getAllDownloadsUri(), values, null, null);
463 | return;
464 | }
465 | DownloadThread downloader = new DownloadThread(mContext, mSystemFacade, this);
466 | mHasActiveThread = true;
467 | mSystemFacade.startThread(downloader);
468 | }
469 |
470 | public Uri getMyDownloadsUri() {
471 | return ContentUris.withAppendedId(Downloads.CONTENT_URI, mId);
472 | }
473 |
474 | public Uri getAllDownloadsUri() {
475 | return ContentUris.withAppendedId(Downloads.ALL_DOWNLOADS_CONTENT_URI, mId);
476 | }
477 |
478 |
479 | public void logVerboseInfo() {
480 | Log.v(Constants.TAG, "Service adding new entry");
481 | Log.v(Constants.TAG, "ID : " + mId);
482 | Log.v(Constants.TAG, "URI : " + ((mUri != null) ? "yes" : "no"));
483 | Log.v(Constants.TAG, "NO_INTEG: " + mNoIntegrity);
484 | Log.v(Constants.TAG, "HINT : " + mHint);
485 | Log.v(Constants.TAG, "FILENAME: " + mFileName);
486 | Log.v(Constants.TAG, "MIMETYPE: " + mMimeType);
487 | Log.v(Constants.TAG, "DESTINAT: " + mDestination);
488 | Log.v(Constants.TAG, "VISIBILI: " + mVisibility);
489 | Log.v(Constants.TAG, "CONTROL : " + mControl);
490 | Log.v(Constants.TAG, "STATUS : " + mStatus);
491 | Log.v(Constants.TAG, "FAILED_C: " + mNumFailed);
492 | Log.v(Constants.TAG, "RETRY_AF: " + mRetryAfter);
493 | Log.v(Constants.TAG, "LAST_MOD: " + mLastMod);
494 | Log.v(Constants.TAG, "PACKAGE : " + mPackage);
495 | Log.v(Constants.TAG, "CLASS : " + mClass);
496 | Log.v(Constants.TAG, "COOKIES : " + ((mCookies != null) ? "yes" : "no"));
497 | Log.v(Constants.TAG, "AGENT : " + mUserAgent);
498 | Log.v(Constants.TAG, "REFERER : " + ((mReferer != null) ? "yes" : "no"));
499 | Log.v(Constants.TAG, "TOTAL : " + mTotalBytes);
500 | Log.v(Constants.TAG, "CURRENT : " + mCurrentBytes);
501 | Log.v(Constants.TAG, "ETAG : " + mETag);
502 | Log.v(Constants.TAG, "DELETED : " + mDeleted);
503 | }
504 |
505 | /**
506 | * Returns the amount of time (as measured from the "now" parameter)
507 | * at which a download will be active.
508 | * 0 = immediately - service should stick around to handle this download.
509 | * -1 = never - service can go away without ever waking up.
510 | * positive value - service must wake up in the future, as specified in ms from "now"
511 | */
512 | long nextAction(long now) {
513 | if (Downloads.isStatusCompleted(mStatus)) {
514 | return -1;
515 | }
516 | if (mStatus != Downloads.STATUS_WAITING_TO_RETRY) {
517 | return 0;
518 | }
519 | long when = restartTime(now);
520 | if (when <= now) {
521 | return 0;
522 | }
523 | return when - now;
524 | }
525 | }
526 |
--------------------------------------------------------------------------------
/filedownload/src/main/java/com/richsjeson/filedownload/api/Downloads.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2008 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.richsjeson.filedownload.api;
18 |
19 | import android.net.Uri;
20 | import android.provider.BaseColumns;
21 |
22 | /**
23 | * The Download Manager
24 | *
25 | * @pending
26 | */
27 | public final class Downloads implements BaseColumns {
28 |
29 | /**
30 | * DownloadProvider authority
31 | */
32 | public static final String AUTHORITY = "com.richsjeson.filedownload";
33 |
34 | /**
35 | * @hide
36 | */
37 | private Downloads() {
38 | }
39 |
40 | /**
41 | * The permission to access the download manager
42 | *
43 | * @hide
44 | */
45 | public static final String PERMISSION_ACCESS = "com.richsjeson.filedownload.permission.ACCESS_DOWNLOAD_MANAGER";
46 |
47 | /**
48 | * The permission to access the download manager's advanced functions
49 | *
50 | * @hide
51 | */
52 | public static final String PERMISSION_ACCESS_ADVANCED = "com.richsjeson.filedownload.permission.ACCESS_DOWNLOAD_MANAGER_ADVANCED";
53 |
54 | /**
55 | * The permission to access the all the downloads in the manager.
56 | */
57 | public static final String PERMISSION_ACCESS_ALL = "com.richsjeson.filedownload.permission.ACCESS_ALL_DOWNLOADS";
58 |
59 | /**
60 | * The permission to send broadcasts on download completion
61 | *
62 | * @hide
63 | */
64 | public static final String PERMISSION_SEND_INTENTS = "com.richsjeson.filedownload.permission.SEND_DOWNLOAD_COMPLETED_INTENTS";
65 |
66 | /**
67 | * The permission to download files without any system notification being
68 | * shown.
69 | */
70 | public static final String PERMISSION_NO_NOTIFICATION = "com.richsjeson.filedownload.permission.DOWNLOAD_WITHOUT_NOTIFICATION";
71 |
72 | /**
73 | * The content:// URI for the data table in the provider
74 | *
75 | * @hide
76 | */
77 | public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY
78 | + "/my_downloads");
79 |
80 | /**
81 | * The content URI for accessing all downloads across all UIDs (requires the
82 | * ACCESS_ALL_DOWNLOADS permission).
83 | */
84 | public static final Uri ALL_DOWNLOADS_CONTENT_URI = Uri.parse("content://"
85 | + AUTHORITY + "/all_downloads");
86 |
87 | /**
88 | * Broadcast Action: this is sent by the download manager to the app that
89 | * had initiated a download when that download completes. The download's
90 | * content: uri is specified in the intent's data.
91 | *
92 | * @hide
93 | */
94 | public static final String ACTION_DOWNLOAD_COMPLETED = "android.intent.action.DOWNLOAD_COMPLETED";
95 |
96 | /**
97 | * Broadcast Action: this is sent by the download manager to the app that
98 | * had initiated a download when the user selects the notification
99 | * associated with that download. The download's content: uri is specified
100 | * in the intent's data if the click is associated with a single download,
101 | * or Downloads.CONTENT_URI if the notification is associated with multiple
102 | * downloads. Note: this is not currently sent for downloads that have
103 | * completed successfully.
104 | *
105 | * @hide
106 | */
107 | public static final String ACTION_NOTIFICATION_CLICKED = "android.intent.action.DOWNLOAD_NOTIFICATION_CLICKED";
108 |
109 | /**
110 | * The name of the column containing the URI of the data being downloaded.
111 | *
112 | * Type: TEXT
113 | *
114 | *
115 | * Owner can Init/Read
116 | *
117 | *
118 | * @hide
119 | */
120 | public static final String COLUMN_URI = "uri";
121 |
122 | /**
123 | * The name of the column containing application-specific data.
124 | *
125 | * Type: TEXT
126 | *
127 | *
128 | * Owner can Init/Read/Write
129 | *
130 | *
131 | * @hide
132 | */
133 | public static final String COLUMN_APP_DATA = "entity";
134 |
135 | /**
136 | * The name of the column containing the flags that indicates whether the
137 | * initiating application is capable of verifying the integrity of the
138 | * downloaded file. When this flag is set, the download manager performs
139 | * downloads and reports success even in some situations where it can't
140 | * guarantee that the download has completed (e.g. when doing a byte-range
141 | * request without an ETag, or when it can't determine whether a download
142 | * fully completed).
143 | *
144 | * Type: BOOLEAN
145 | *
146 | *
147 | * Owner can Init
148 | *
149 | *
150 | * @hide
151 | */
152 | public static final String COLUMN_NO_INTEGRITY = "no_integrity";
153 |
154 | /**
155 | * The name of the column containing the filename that the initiating
156 | * application recommends. When possible, the download manager will attempt
157 | * to use this filename, or a variation, as the actual name for the file.
158 | *
159 | * Type: TEXT
160 | *
161 | *
162 | * Owner can Init
163 | *
164 | *
165 | * @hide
166 | */
167 | public static final String COLUMN_FILE_NAME_HINT = "hint";
168 |
169 | /**
170 | * The name of the column containing the filename where the downloaded data
171 | * was actually stored.
172 | *
173 | * Type: TEXT
174 | *
175 | *
176 | * Owner can Read
177 | *
178 | *
179 | * @hide
180 | */
181 | public static final String _DATA = "_data";
182 |
183 | /**
184 | * The name of the column containing the MIME type of the downloaded data.
185 | *
186 | * Type: TEXT
187 | *
188 | *
189 | * Owner can Init/Read
190 | *
191 | *
192 | * @hide
193 | */
194 | public static final String COLUMN_MIME_TYPE = "mimetype";
195 |
196 | /**
197 | * The name of the column containing the flag that controls the destination
198 | * of the download. See the DESTINATION_* constants for a list of legal
199 | * values.
200 | *
201 | * Type: INTEGER
202 | *
203 | *
204 | * Owner can Init
205 | *
206 | *
207 | * @hide
208 | */
209 | public static final String COLUMN_DESTINATION = "destination";
210 |
211 | /**
212 | * The name of the column containing the flags that controls whether the
213 | * download is displayed by the UI. See the VISIBILITY_* constants for a
214 | * list of legal values.
215 | *
216 | * Type: INTEGER
217 | *
218 | *
219 | * Owner can Init/Read/Write
220 | *
221 | *
222 | * @hide
223 | */
224 | public static final String COLUMN_VISIBILITY = "visibility";
225 |
226 | /**
227 | * The name of the column containing the current control state of the
228 | * download. Applications can write to this to control (pause/resume) the
229 | * download. the CONTROL_* constants for a list of legal values.
230 | *
231 | * Type: INTEGER
232 | *
233 | *
234 | * Owner can Read
235 | *
236 | *
237 | * @hide
238 | */
239 | public static final String COLUMN_CONTROL = "control";
240 |
241 | /**
242 | * The name of the column containing the current status of the download.
243 | * Applications can read this to follow the progress of each download. See
244 | * the STATUS_* constants for a list of legal values.
245 | *
246 | * Type: INTEGER
247 | *
248 | *
249 | * Owner can Read
250 | *
251 | *
252 | * @hide
253 | */
254 | public static final String COLUMN_STATUS = "status";
255 |
256 | /**
257 | * The name of the column containing the date at which some interesting
258 | * status changed in the download. Stored as a System.currentTimeMillis()
259 | * value.
260 | *
261 | * Type: BIGINT
262 | *
263 | *
264 | * Owner can Read
265 | *
266 | *
267 | * @hide
268 | */
269 | public static final String COLUMN_LAST_MODIFICATION = "lastmod";
270 |
271 | /**
272 | * The name of the column containing the package name of the application
273 | * that initiating the download. The download manager will send
274 | * notifications to a component in this package when the download completes.
275 | *
276 | * Type: TEXT
277 | *
278 | *
279 | * Owner can Init/Read
280 | *
281 | *
282 | * @hide
283 | */
284 | public static final String COLUMN_NOTIFICATION_PACKAGE = "notificationpackage";
285 |
286 | /**
287 | * The name of the column containing the component name of the class that
288 | * will receive notifications associated with the download. The
289 | * package/class combination is passed to
290 | * Intent.setClassName(String,String).
291 | *
292 | * Type: TEXT
293 | *
294 | *
295 | * Owner can Init/Read
296 | *
297 | *
298 | * @hide
299 | */
300 | public static final String COLUMN_NOTIFICATION_CLASS = "notificationclass";
301 |
302 | /**
303 | * If extras are specified when requesting a download they will be provided
304 | * in the intent that is sent to the specified class and package when a
305 | * download has finished.
306 | *
307 | * Type: TEXT
308 | *
309 | *
310 | * Owner can Init
311 | *
312 | *
313 | * @hide
314 | */
315 | public static final String COLUMN_NOTIFICATION_EXTRAS = "notificationextras";
316 |
317 | /**
318 | * The name of the column contain the values of the cookie to be used for
319 | * the download. This is used directly as the value for the Cookie: HTTP
320 | * header that gets sent with the request.
321 | *
322 | * Type: TEXT
323 | *
324 | *
325 | * Owner can Init
326 | *
327 | *
328 | * @hide
329 | */
330 | public static final String COLUMN_COOKIE_DATA = "cookiedata";
331 |
332 | /**
333 | * The name of the column containing the user agent that the initiating
334 | * application wants the download manager to use for this download.
335 | *
336 | * Type: TEXT
337 | *
338 | *
339 | * Owner can Init
340 | *
341 | *
342 | * @hide
343 | */
344 | public static final String COLUMN_USER_AGENT = "useragent";
345 |
346 | /**
347 | * The name of the column containing the referer (sic) that the initiating
348 | * application wants the download manager to use for this download.
349 | *
350 | * Type: TEXT
351 | *
352 | *
353 | * Owner can Init
354 | *
355 | *
356 | * @hide
357 | */
358 | public static final String COLUMN_REFERER = "referer";
359 |
360 | /**
361 | * The name of the column containing the total size of the file being
362 | * downloaded.
363 | *
364 | * Type: INTEGER
365 | *
366 | *
367 | * Owner can Read
368 | *
369 | *
370 | * @hide
371 | */
372 | public static final String COLUMN_TOTAL_BYTES = "total_bytes";
373 |
374 | /**
375 | * The name of the column containing the size of the part of the file that
376 | * has been downloaded so far.
377 | *
378 | * Type: INTEGER
379 | *
380 | *
381 | * Owner can Read
382 | *
383 | *
384 | * @hide
385 | */
386 | public static final String COLUMN_CURRENT_BYTES = "current_bytes";
387 |
388 | /**
389 | * The name of the column where the initiating application can provide the
390 | * UID of another application that is allowed to access this download. If
391 | * multiple applications share the same UID, all those applications will be
392 | * allowed to access this download. This column can be updated after the
393 | * download is initiated. This requires the permission
394 | * com.mozillaonline.permission.ACCESS_DOWNLOAD_MANAGER_ADVANCED.
395 | *
396 | * Type: INTEGER
397 | *
398 | *
399 | * Owner can Init
400 | *
401 | *
402 | * @hide
403 | */
404 | public static final String COLUMN_OTHER_UID = "otheruid";
405 |
406 | /**
407 | * The name of the column where the initiating application can provided the
408 | * title of this download. The title will be displayed ito the user in the
409 | * list of downloads.
410 | *
411 | * Type: TEXT
412 | *
413 | *
414 | * Owner can Init/Read/Write
415 | *
416 | *
417 | * @hide
418 | */
419 | public static final String COLUMN_TITLE = "title";
420 |
421 | /**
422 | * The name of the column where the initiating application can provide the
423 | * description of this download. The description will be displayed to the
424 | * user in the list of downloads.
425 | *
426 | * Type: TEXT
427 | *
428 | *
429 | * Owner can Init/Read/Write
430 | *
431 | *
432 | * @hide
433 | */
434 | public static final String COLUMN_DESCRIPTION = "description";
435 |
436 | /**
437 | * The name of the column indicating whether the download was requesting
438 | * through the public API. This controls some differences in behavior.
439 | *
440 | * Type: BOOLEAN
441 | *
442 | *
443 | * Owner can Init/Read
444 | *
445 | */
446 | public static final String COLUMN_IS_PUBLIC_API = "is_public_api";
447 |
448 | /**
449 | * The name of the column indicating whether roaming connections can be
450 | * used. This is only used for public API downloads.
451 | *
452 | * Type: BOOLEAN
453 | *
454 | *
455 | * Owner can Init/Read
456 | *
457 | */
458 | public static final String COLUMN_ALLOW_ROAMING = "allow_roaming";
459 |
460 | /**
461 | * The name of the column holding a bitmask of allowed network types. This
462 | * is only used for public API downloads.
463 | *
464 | * Type: INTEGER
465 | *
466 | *
467 | * Owner can Init/Read
468 | *
469 | */
470 | public static final String COLUMN_ALLOWED_NETWORK_TYPES = "allowed_network_types";
471 |
472 | /**
473 | * Whether or not this download should be displayed in the system's
474 | * Downloads UI. Defaults to true.
475 | *
476 | * Type: INTEGER
477 | *
478 | *
479 | * Owner can Init/Read
480 | *
481 | */
482 | public static final String COLUMN_IS_VISIBLE_IN_DOWNLOADS_UI = "is_visible_in_downloads_ui";
483 |
484 | /**
485 | * If true, the user has confirmed that this download can proceed over the
486 | * mobile network even though it exceeds the recommended maximum size.
487 | *
488 | * Type: BOOLEAN
489 | *
490 | */
491 | public static final String COLUMN_BYPASS_RECOMMENDED_SIZE_LIMIT = "bypass_recommended_size_limit";
492 |
493 | /**
494 | * Set to true if this download is deleted.
495 | *
496 | * Type: BOOLEAN
497 | *
498 | *
499 | * Owner can Read
500 | *
501 | *
502 | * @hide
503 | */
504 | public static final String COLUMN_DELETED = "deleted";
505 |
506 | /**
507 | * Set to true if this download is deleted.
508 | *
509 | * Type: COLUMN_ERROR_CODE 下载的错误状态值
510 | *
511 | *
512 | * Owner can Read
513 | *
514 | *
515 | * @hide
516 | */
517 | public static final String COLUMN_ERROR_CODE = "error_code";
518 |
519 |
520 | public static final String COLUMN_FILE_MD5= "error_code";
521 |
522 | /*
523 | * Lists the destinations that an application can specify for a download.
524 | */
525 |
526 | /**
527 | * This download will be saved to the external storage. This is the default
528 | * behavior, and should be used for any file that the user can freely
529 | * access, copy, delete. Even with that destination, unencrypted DRM files
530 | * are saved in secure internal storage. Downloads to the external
531 | * destination only write files for which there is a registered handler. The
532 | * resulting files are accessible by filename to all applications.
533 | *
534 | * @hide
535 | */
536 | public static final int DESTINATION_EXTERNAL = 0;
537 |
538 | /**
539 | * This download will be saved to the location given by the file URI in
540 | * {@link #COLUMN_FILE_NAME_HINT}.
541 | */
542 | public static final int DESTINATION_FILE_URI = 4;
543 |
544 | /**
545 | * This download is allowed to run.
546 | *
547 | * @hide
548 | */
549 | public static final int CONTROL_RUN = 0;
550 |
551 | /**
552 | * This download must pause at the first opportunity.
553 | *
554 | * @hide
555 | */
556 | public static final int CONTROL_PAUSED = 1;
557 |
558 | /*
559 | * Lists the states that the download manager can set on a download to
560 | * notify applications of the download progress. The codes follow the HTTP
561 | * families:
1xx: informational
2xx: success
3xx: redirects (not
562 | * used by the download manager)
4xx: client errors
5xx: server
563 | * errors
564 | */
565 |
566 | /**
567 | * Returns whether the status is informational (i.e. 1xx).
568 | *
569 | * @hide
570 | */
571 | public static boolean isStatusInformational(int status) {
572 | return (status >= 100 && status < 200);
573 | }
574 |
575 | /**
576 | * Returns whether the status is a success (i.e. 2xx).
577 | *
578 | * @hide
579 | */
580 | public static boolean isStatusSuccess(int status) {
581 | return (status >= 200 && status < 300);
582 | }
583 |
584 | /**
585 | * Returns whether the status is an error (i.e. 4xx or 5xx).
586 | *
587 | * @hide
588 | */
589 | public static boolean isStatusError(int status) {
590 | return (status >= 400 && status < 600);
591 | }
592 |
593 | /**
594 | * Returns whether the status is a client error (i.e. 4xx).
595 | *
596 | * @hide
597 | */
598 | public static boolean isStatusClientError(int status) {
599 | return (status >= 400 && status < 500);
600 | }
601 |
602 | /**
603 | * Returns whether the status is a server error (i.e. 5xx).
604 | *
605 | * @hide
606 | */
607 | public static boolean isStatusServerError(int status) {
608 | return (status >= 500 && status < 600);
609 | }
610 |
611 | /**
612 | * Returns whether the download has completed (either with success or
613 | * error).
614 | *
615 | * @hide
616 | */
617 | public static boolean isStatusCompleted(int status) {
618 | return (status >= 200 && status < 300)
619 | || (status >= 400 && status < 600);
620 | }
621 |
622 | /**
623 | * This download hasn't stated yet
624 | *
625 | * @hide
626 | */
627 | public static final int STATUS_PENDING = 190;
628 |
629 | /**
630 | * This download has started
631 | *
632 | * @hide
633 | */
634 | public static final int STATUS_RUNNING = 192;
635 |
636 | /**
637 | * This download has been paused by the owning app.
638 | */
639 | public static final int STATUS_PAUSED_BY_APP = 193;
640 |
641 | /**
642 | * This download encountered some network error and is waiting before
643 | * retrying the request.
644 | */
645 | public static final int STATUS_WAITING_TO_RETRY = 194;
646 |
647 | /**
648 | * This download is waiting for network connectivity to proceed.
649 | */
650 | public static final int STATUS_WAITING_FOR_NETWORK = 195;
651 |
652 | /**
653 | * This download exceeded a size limit for mobile networks and is waiting
654 | * for a Wi-Fi connection to proceed.
655 | */
656 | public static final int STATUS_QUEUED_FOR_WIFI = 196;
657 |
658 | /**
659 | * This download has successfully completed. Warning: there might be other
660 | * status values that indicate success in the future. Use isSucccess() to
661 | * capture the entire category.
662 | *
663 | * @hide
664 | */
665 | public static final int STATUS_SUCCESS = 200;
666 |
667 | /**
668 | * This request couldn't be parsed. This is also used when processing
669 | * requests with unknown/unsupported URI schemes.
670 | *
671 | * @hide
672 | */
673 | public static final int STATUS_BAD_REQUEST = 400;
674 |
675 | /**
676 | * This download can't be performed because the content type cannot be
677 | * handled.
678 | *
679 | * @hide
680 | */
681 | public static final int STATUS_NOT_ACCEPTABLE = 406;
682 |
683 | /**
684 | * This download cannot be performed because the length cannot be determined
685 | * accurately. This is the code for the HTTP error "Length Required", which
686 | * is typically used when making requests that require a content length but
687 | * don't have one, and it is also used in the client when a response is
688 | * received whose length cannot be determined accurately (therefore making
689 | * it impossible to know when a download completes).
690 | *
691 | * @hide
692 | */
693 | public static final int STATUS_LENGTH_REQUIRED = 411;
694 |
695 | /**
696 | * This download was interrupted and cannot be resumed. This is the code for
697 | * the HTTP error "Precondition Failed", and it is also used in situations
698 | * where the client doesn't have an ETag at all.
699 | *
700 | * @hide
701 | */
702 | public static final int STATUS_PRECONDITION_FAILED = 412;
703 |
704 | /**
705 | * The lowest-valued error status that is not an actual HTTP status code.
706 | */
707 | public static final int MIN_ARTIFICIAL_ERROR_STATUS = 488;
708 |
709 | /**
710 | * The requested destination file already exists.
711 | */
712 | public static final int STATUS_FILE_ALREADY_EXISTS_ERROR = 488;
713 |
714 | /**
715 | * Some possibly transient error occurred, but we can't resume the download.
716 | * 不能被重置,可执行断点下下载
717 | */
718 | public static final int STATUS_CANNOT_RESUME = 3001;
719 |
720 | /**
721 | * This download was canceled
722 | *
723 | * @hide
724 | */
725 | public static final int STATUS_CANCELED = 490;
726 |
727 | /**
728 | * This download has completed with an error. Warning: there will be other
729 | * status values that indicate errors in the future. Use isStatusError() to
730 | * capture the entire category.
731 | *
732 | * @hide
733 | */
734 | public static final int STATUS_UNKNOWN_ERROR = 491;
735 |
736 | /**
737 | * This download couldn't be completed because of a storage issue.
738 | * Typically, that's because the filesystem is missing or full. Use the more
739 | * specific {@link #STATUS_INSUFFICIENT_SPACE_ERROR} and
740 | * {@link #STATUS_DEVICE_NOT_FOUND_ERROR} when appropriate.
741 | *
742 | * @hide
743 | */
744 | public static final int STATUS_FILE_ERROR = 492;
745 |
746 | /**
747 | * This download couldn't be completed because of an HTTP redirect response
748 | * that the download manager couldn't handle.
749 | *
750 | * @hide
751 | */
752 | public static final int STATUS_UNHANDLED_REDIRECT = 493;
753 |
754 | /**
755 | * This download couldn't be completed because of an unspecified unhandled
756 | * HTTP code.
757 | *
758 | * @hide
759 | */
760 | public static final int STATUS_UNHANDLED_HTTP_CODE = 494;
761 |
762 | /**
763 | * This download couldn't be completed because of an error receiving or
764 | * processing data at the HTTP level.
765 | *
766 | * @hide
767 | */
768 | public static final int STATUS_HTTP_DATA_ERROR = 495;
769 |
770 | /**
771 | * This download couldn't be completed because of an HttpException while
772 | * setting up the request.
773 | *
774 | * @hide
775 | */
776 | public static final int STATUS_HTTP_EXCEPTION = 496;
777 |
778 | /**
779 | * This download couldn't be completed because there were too many
780 | * redirects.
781 | *
782 | * @hide
783 | */
784 | public static final int STATUS_TOO_MANY_REDIRECTS = 497;
785 |
786 | /**
787 | * This download couldn't be completed due to insufficient storage space.
788 | * Typically, this is because the SD card is full.
789 | *
790 | * @hide
791 | */
792 | public static final int STATUS_INSUFFICIENT_SPACE_ERROR = 498;
793 |
794 | /**
795 | * This download couldn't be completed because no external storage device
796 | * was found. Typically, this is because the SD card is not mounted.
797 | *
798 | * @hide
799 | */
800 | public static final int STATUS_DEVICE_NOT_FOUND_ERROR = 499;
801 |
802 | /**
803 | * This download is visible but only shows in the notifications while it's
804 | * in progress.
805 | *
806 | * @hide
807 | */
808 | public static final int VISIBILITY_VISIBLE = 0;
809 |
810 | /**
811 | * This download is visible and shows in the notifications while in progress
812 | * and after completion.
813 | *
814 | * @hide
815 | */
816 | public static final int VISIBILITY_VISIBLE_NOTIFY_COMPLETED = 1;
817 |
818 | /**
819 | * This download doesn't show in the UI or in the notifications.
820 | *
821 | * @hide
822 | */
823 | public static final int VISIBILITY_HIDDEN = 2;
824 |
825 | /**
826 | * Constants related to HTTP request headers associated with each download.
827 | */
828 | public static class RequestHeaders {
829 | public static final String HEADERS_DB_TABLE = "request_headers";
830 | public static final String COLUMN_DOWNLOAD_ID = "download_id";
831 | public static final String COLUMN_HEADER = "header";
832 | public static final String COLUMN_VALUE = "value";
833 |
834 | /**
835 | * Path segment to add to a download URI to retrieve request headers
836 | */
837 | public static final String URI_SEGMENT = "headers";
838 |
839 | /**
840 | * Prefix for ContentValues keys that contain HTTP header lines, to be
841 | * passed to DownloadProvider.insert().
842 | */
843 | public static final String INSERT_KEY_PREFIX = "http_header_";
844 | }
845 | }
846 |
--------------------------------------------------------------------------------
/filedownload/src/main/java/com/richsjeson/filedownload/api/Helpers.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2008 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.richsjeson.filedownload.api;
18 |
19 | import java.io.File;
20 | import java.util.Random;
21 | import java.util.Set;
22 | import java.util.regex.Matcher;
23 | import java.util.regex.Pattern;
24 |
25 | import android.content.ContentResolver;
26 | import android.content.Context;
27 | import android.content.Intent;
28 | import android.content.pm.PackageManager;
29 | import android.content.pm.ResolveInfo;
30 | import android.net.Uri;
31 | import android.os.Environment;
32 | import android.os.StatFs;
33 | import android.os.SystemClock;
34 | import android.util.Config;
35 | import android.util.Log;
36 | import android.webkit.MimeTypeMap;
37 |
38 | /**
39 | * Some helper functions for the download manager
40 | */
41 | public class Helpers {
42 |
43 | public static Random sRandom = new Random(SystemClock.uptimeMillis());
44 |
45 | /** Regex used to parse content-disposition headers */
46 | private static final Pattern CONTENT_DISPOSITION_PATTERN = Pattern
47 | .compile("attachment;\\s*filename\\s*=\\s*\"([^\"]*)\"");
48 |
49 | private Helpers() {
50 | }
51 |
52 | /*
53 | * Parse the Content-Disposition HTTP Header. The format of the header is
54 | * defined here: http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html This
55 | * header provides a filename for content that is going to be downloaded to
56 | * the file system. We only support the attachment type.
57 | */
58 | private static String parseContentDisposition(String contentDisposition) {
59 | try {
60 | Matcher m = CONTENT_DISPOSITION_PATTERN.matcher(contentDisposition);
61 | if (m.find()) {
62 | return m.group(1);
63 | }
64 | } catch (IllegalStateException ex) {
65 | // This function is defined as returning null when it can't parse
66 | // the header
67 | }
68 | return null;
69 | }
70 |
71 | /**
72 | * Exception thrown from methods called by generateSaveFile() for any fatal
73 | * error.
74 | */
75 | public static class GenerateSaveFileError extends Exception {
76 | private static final long serialVersionUID = 4293675292408637112L;
77 |
78 | int mStatus;
79 | String mMessage;
80 |
81 | public GenerateSaveFileError(int status, String message) {
82 | mStatus = status;
83 | mMessage = message;
84 | }
85 | }
86 |
87 | /**
88 | * Creates a filename (where the file should be saved) from info about a
89 | * download.
90 | */
91 | public static String generateSaveFile(Context context, String url,
92 | String hint, String contentDisposition, String contentLocation,
93 | String mimeType, int destination, long contentLength,
94 | boolean isPublicApi) throws GenerateSaveFileError {
95 | checkCanHandleDownload(context, mimeType, destination, isPublicApi);
96 | if (destination == Downloads.DESTINATION_FILE_URI) {
97 | return getPathForFileUri(url, hint, contentDisposition,
98 | contentLocation, mimeType, destination, contentLength);
99 | } else {
100 | return chooseFullPath(context, url, hint, contentDisposition,
101 | contentLocation, mimeType, destination, contentLength);
102 | }
103 | }
104 |
105 | private static String getPathForFileUri(String url, String hint,
106 | String contentDisposition, String contentLocation, String mimeType,
107 | int destination, long contentLength) throws GenerateSaveFileError {
108 | if (!isExternalMediaMounted()) {
109 | throw new GenerateSaveFileError(
110 | Downloads.STATUS_DEVICE_NOT_FOUND_ERROR,
111 | "external media not mounted");
112 | }
113 | String path = Uri.parse(hint).getPath();
114 | if (path.endsWith("/")) {
115 | String basePath = path.substring(0, path.length() - 1);
116 | path = generateFilePath(basePath, url, contentDisposition,
117 | contentLocation, mimeType, destination, contentLength);
118 | } else if (new File(path).exists()) {
119 | Log.d(Constants.TAG, "File already exists: " + path);
120 | throw new GenerateSaveFileError(
121 | Downloads.STATUS_FILE_ALREADY_EXISTS_ERROR,
122 | "requested destination file already exists");
123 |
124 | //如果文件存在,则检测MD5值
125 |
126 | }
127 | if (getAvailableBytes(getFilesystemRoot(path)) < contentLength) {
128 | throw new GenerateSaveFileError(
129 | Downloads.STATUS_INSUFFICIENT_SPACE_ERROR,
130 | "insufficient space on external storage");
131 | }
132 |
133 | return path;
134 | }
135 |
136 | /**
137 | * @return the root of the filesystem containing the given path
138 | */
139 | public static File getFilesystemRoot(String path) {
140 | File cache = Environment.getDownloadCacheDirectory();
141 | if (path.startsWith(cache.getPath())) {
142 | return cache;
143 | }
144 | File external = Environment.getExternalStorageDirectory();
145 | if (path.startsWith(external.getPath())) {
146 | return external;
147 | }
148 | throw new IllegalArgumentException(
149 | "Cannot determine filesystem root for " + path);
150 | }
151 |
152 | private static String generateFilePath(String basePath, String url,
153 | String contentDisposition, String contentLocation, String mimeType,
154 | int destination, long contentLength) throws GenerateSaveFileError {
155 | String filename = chooseFilename(url, null, contentDisposition,
156 | contentLocation, destination);
157 |
158 | // Split filename between base and extension
159 | // Add an extension if filename does not have one
160 | String extension = null;
161 | int dotIndex = filename.indexOf('.');
162 | if (dotIndex < 0) {
163 | extension = chooseExtensionFromMimeType(mimeType, true);
164 | } else {
165 | extension = chooseExtensionFromFilename(mimeType, destination,
166 | filename, dotIndex);
167 | filename = filename.substring(0, dotIndex);
168 | }
169 |
170 | boolean recoveryDir = Constants.RECOVERY_DIRECTORY
171 | .equalsIgnoreCase(filename + extension);
172 |
173 | filename = basePath + File.separator + filename;
174 |
175 | if (Constants.LOGVV) {
176 | Log.v(Constants.TAG, "target file: " + filename + extension);
177 | }
178 |
179 | return chooseUniqueFilename(destination, filename, extension,
180 | recoveryDir);
181 | }
182 |
183 | private static String chooseFullPath(Context context, String url,
184 | String hint, String contentDisposition, String contentLocation,
185 | String mimeType, int destination, long contentLength)
186 | throws GenerateSaveFileError {
187 | File base = locateDestinationDirectory(context, mimeType, destination,
188 | contentLength);
189 | return generateFilePath(base.getPath(), url, contentDisposition,
190 | contentLocation, mimeType, destination, contentLength);
191 | }
192 |
193 | private static void checkCanHandleDownload(Context context,
194 | String mimeType, int destination, boolean isPublicApi)
195 | throws GenerateSaveFileError {
196 | if (isPublicApi) {
197 | return;
198 | }
199 |
200 | if (destination == Downloads.DESTINATION_EXTERNAL) {
201 | if (mimeType == null) {
202 | throw new GenerateSaveFileError(
203 | Downloads.STATUS_NOT_ACCEPTABLE,
204 | "external download with no mime type not allowed");
205 | }
206 | // Check to see if we are allowed to download this file. Only files
207 | // that can be handled by the platform can be downloaded.
208 | // special case DRM files, which we should always allow downloading.
209 | Intent intent = new Intent(Intent.ACTION_VIEW);
210 |
211 | // We can provide data as either content: or file: URIs,
212 | // so allow both. (I think it would be nice if we just did
213 | // everything as content: URIs)
214 | // Actually, right now the download manager's UId restrictions
215 | // prevent use from using content: so it's got to be file: or
216 | // nothing
217 |
218 | PackageManager pm = context.getPackageManager();
219 | intent.setDataAndType(Uri.fromParts("file", "", null), mimeType);
220 | ResolveInfo ri = pm.resolveActivity(intent,
221 | PackageManager.MATCH_DEFAULT_ONLY);
222 |
223 | if (ri == null) {
224 | if (Constants.LOGV) {
225 | Log.v(Constants.TAG, "no handler found for type "
226 | + mimeType);
227 | }
228 | throw new GenerateSaveFileError(
229 | Downloads.STATUS_NOT_ACCEPTABLE,
230 | "no handler found for this download type");
231 | }
232 | }
233 | }
234 |
235 | private static File locateDestinationDirectory(Context context,
236 | String mimeType, int destination, long contentLength)
237 | throws GenerateSaveFileError {
238 | return getExternalDestination(contentLength);
239 | }
240 |
241 | private static File getExternalDestination(long contentLength)
242 | throws GenerateSaveFileError {
243 | if (!isExternalMediaMounted()) {
244 | throw new GenerateSaveFileError(
245 | Downloads.STATUS_DEVICE_NOT_FOUND_ERROR,
246 | "external media not mounted");
247 | }
248 |
249 | File root = Environment.getExternalStorageDirectory();
250 | if (getAvailableBytes(root) < contentLength) {
251 | // Insufficient space.
252 | Log.d(Constants.TAG, "download aborted - not enough free space");
253 | throw new GenerateSaveFileError(
254 | Downloads.STATUS_INSUFFICIENT_SPACE_ERROR,
255 | "insufficient space on external media");
256 | }
257 |
258 | File base = new File(root.getPath() + Constants.DEFAULT_DL_SUBDIR);
259 | if (!base.isDirectory() && !base.mkdir()) {
260 | // Can't create download directory, e.g. because a file called
261 | // "download"
262 | // already exists at the root level, or the SD card filesystem is
263 | // read-only.
264 | throw new GenerateSaveFileError(Downloads.STATUS_FILE_ERROR,
265 | "unable to create external downloads directory "
266 | + base.getPath());
267 | }
268 | return base;
269 | }
270 |
271 | public static boolean isExternalMediaMounted() {
272 | if (!Environment.getExternalStorageState().equals(
273 | Environment.MEDIA_MOUNTED)) {
274 | // No SD card found.
275 | Log.d(Constants.TAG, "no external storage");
276 | return false;
277 | }
278 | return true;
279 | }
280 |
281 | /**
282 | * @return the number of bytes available on the filesystem rooted at the
283 | * given File
284 | */
285 | public static long getAvailableBytes(File root) {
286 | StatFs stat = new StatFs(root.getPath());
287 | // put a bit of margin (in case creating the file grows the system by a
288 | // few blocks)
289 | long availableBlocks = (long) stat.getAvailableBlocks() - 4;
290 | return stat.getBlockSize() * availableBlocks;
291 | }
292 |
293 | private static String chooseFilename(String url, String hint,
294 | String contentDisposition, String contentLocation, int destination) {
295 | String filename = null;
296 |
297 | // First, try to use the hint from the application, if there's one
298 | if (filename == null && hint != null && !hint.endsWith("/")) {
299 | if (Constants.LOGVV) {
300 | Log.v(Constants.TAG, "getting filename from hint");
301 | }
302 | int index = hint.lastIndexOf('/') + 1;
303 | if (index > 0) {
304 | filename = hint.substring(index);
305 | } else {
306 | filename = hint;
307 | }
308 | }
309 |
310 | // If we couldn't do anything with the hint, move toward the content
311 | // disposition
312 | if (filename == null && contentDisposition != null) {
313 | filename = parseContentDisposition(contentDisposition);
314 | if (filename != null) {
315 | if (Constants.LOGVV) {
316 | Log.v(Constants.TAG,
317 | "getting filename from content-disposition");
318 | }
319 | int index = filename.lastIndexOf('/') + 1;
320 | if (index > 0) {
321 | filename = filename.substring(index);
322 | }
323 | }
324 | }
325 |
326 | // If we still have nothing at this point, try the content location
327 | if (filename == null && contentLocation != null) {
328 | String decodedContentLocation = Uri.decode(contentLocation);
329 | if (decodedContentLocation != null
330 | && !decodedContentLocation.endsWith("/")
331 | && decodedContentLocation.indexOf('?') < 0) {
332 | if (Constants.LOGVV) {
333 | Log.v(Constants.TAG,
334 | "getting filename from content-location");
335 | }
336 | int index = decodedContentLocation.lastIndexOf('/') + 1;
337 | if (index > 0) {
338 | filename = decodedContentLocation.substring(index);
339 | } else {
340 | filename = decodedContentLocation;
341 | }
342 | }
343 | }
344 |
345 | // If all the other http-related approaches failed, use the plain uri
346 | if (filename == null) {
347 | String decodedUrl = Uri.decode(url);
348 | if (decodedUrl != null && !decodedUrl.endsWith("/")
349 | && decodedUrl.indexOf('?') < 0) {
350 | int index = decodedUrl.lastIndexOf('/') + 1;
351 | if (index > 0) {
352 | if (Constants.LOGVV) {
353 | Log.v(Constants.TAG, "getting filename from uri");
354 | }
355 | filename = decodedUrl.substring(index);
356 | }
357 | }
358 | }
359 |
360 | // Finally, if couldn't get filename from URI, get a generic filename
361 | if (filename == null) {
362 | if (Constants.LOGVV) {
363 | Log.v(Constants.TAG, "using default filename");
364 | }
365 | filename = Constants.DEFAULT_DL_FILENAME;
366 | }
367 |
368 | filename = filename.replaceAll("[^a-zA-Z0-9\\.\\-_]+", "_");
369 |
370 | return filename;
371 | }
372 |
373 | private static String chooseExtensionFromMimeType(String mimeType,
374 | boolean useDefaults) {
375 | String extension = null;
376 | if (mimeType != null) {
377 | extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(
378 | mimeType);
379 | if (extension != null) {
380 | if (Constants.LOGVV) {
381 | Log.v(Constants.TAG, "adding extension from type");
382 | }
383 | extension = "." + extension;
384 | } else {
385 | if (Constants.LOGVV) {
386 | Log.v(Constants.TAG, "couldn't find extension for "
387 | + mimeType);
388 | }
389 | }
390 | }
391 | if (extension == null) {
392 | if (mimeType != null && mimeType.toLowerCase().startsWith("text/")) {
393 | if (mimeType.equalsIgnoreCase("text/html")) {
394 | if (Constants.LOGVV) {
395 | Log.v(Constants.TAG, "adding default html extension");
396 | }
397 | extension = Constants.DEFAULT_DL_HTML_EXTENSION;
398 | } else if (useDefaults) {
399 | if (Constants.LOGVV) {
400 | Log.v(Constants.TAG, "adding default text extension");
401 | }
402 | extension = Constants.DEFAULT_DL_TEXT_EXTENSION;
403 | }
404 | } else if (useDefaults) {
405 | if (Constants.LOGVV) {
406 | Log.v(Constants.TAG, "adding default binary extension");
407 | }
408 | extension = Constants.DEFAULT_DL_BINARY_EXTENSION;
409 | }
410 | }
411 | return extension;
412 | }
413 |
414 | private static String chooseExtensionFromFilename(String mimeType,
415 | int destination, String filename, int dotIndex) {
416 | String extension = null;
417 | if (mimeType != null) {
418 | // Compare the last segment of the extension against the mime type.
419 | // If there's a mismatch, discard the entire extension.
420 | int lastDotIndex = filename.lastIndexOf('.');
421 | String typeFromExt = MimeTypeMap.getSingleton()
422 | .getMimeTypeFromExtension(
423 | filename.substring(lastDotIndex + 1));
424 | if (typeFromExt == null || !typeFromExt.equalsIgnoreCase(mimeType)) {
425 | extension = chooseExtensionFromMimeType(mimeType, false);
426 | if (extension != null) {
427 | if (Constants.LOGVV) {
428 | Log.v(Constants.TAG, "substituting extension from type");
429 | }
430 | } else {
431 | if (Constants.LOGVV) {
432 | Log.v(Constants.TAG, "couldn't find extension for "
433 | + mimeType);
434 | }
435 | }
436 | }
437 | }
438 | if (extension == null) {
439 | if (Constants.LOGVV) {
440 | Log.v(Constants.TAG, "keeping extension");
441 | }
442 | extension = filename.substring(dotIndex);
443 | }
444 | return extension;
445 | }
446 |
447 | private static String chooseUniqueFilename(int destination,
448 | String filename, String extension, boolean recoveryDir)
449 | throws GenerateSaveFileError {
450 | String fullFilename = filename + extension;
451 | if (!new File(fullFilename).exists() && !recoveryDir) {
452 | return fullFilename;
453 | }
454 | filename = filename + Constants.FILENAME_SEQUENCE_SEPARATOR;
455 | /*
456 | * This number is used to generate partially randomized filenames to
457 | * avoid collisions. It starts at 1. The next 9 iterations increment it
458 | * by 1 at a time (up to 10). The next 9 iterations increment it by 1 to
459 | * 10 (random) at a time. The next 9 iterations increment it by 1 to 100
460 | * (random) at a time. ... Up to the point where it increases by
461 | * 100000000 at a time. (the maximum value that can be reached is
462 | * 1000000000) As soon as a number is reached that generates a filename
463 | * that doesn't exist, that filename is used. If the filename coming in
464 | * is [base].[ext], the generated filenames are [base]-[sequence].[ext].
465 | */
466 | int sequence = 1;
467 | for (int magnitude = 1; magnitude < 1000000000; magnitude *= 10) {
468 | for (int iteration = 0; iteration < 9; ++iteration) {
469 | fullFilename = filename + sequence + extension;
470 | if (!new File(fullFilename).exists()) {
471 | return fullFilename;
472 | }
473 | if (Constants.LOGVV) {
474 | Log.v(Constants.TAG, "file with sequence number "
475 | + sequence + " exists");
476 | }
477 | sequence += sRandom.nextInt(magnitude) + 1;
478 | }
479 | }
480 | throw new GenerateSaveFileError(Downloads.STATUS_FILE_ERROR,
481 | "failed to generate an unused filename on internal download storage");
482 | }
483 |
484 | /**
485 | * Returns whether the network is available
486 | */
487 | public static boolean isNetworkAvailable(SystemFacade system) {
488 | return system.getActiveNetworkType() != null;
489 | }
490 |
491 | /**
492 | * Checks whether the filename looks legitimate
493 | */
494 | public static boolean isFilenameValid(String filename) {
495 | filename = filename.replaceFirst("/+", "/"); // normalize leading
496 | // slashes
497 | return filename.startsWith(Environment.getDownloadCacheDirectory()
498 | .toString())
499 | || filename.startsWith(Environment
500 | .getExternalStorageDirectory().toString());
501 | }
502 |
503 | /**
504 | * Checks whether this looks like a legitimate selection parameter
505 | */
506 | public static void validateSelection(String selection,
507 | Set allowedColumns) {
508 | try {
509 | if (selection == null || selection.length() == 0) {
510 | return;
511 | }
512 | Lexer lexer = new Lexer(selection, allowedColumns);
513 | parseExpression(lexer);
514 | if (lexer.currentToken() != Lexer.TOKEN_END) {
515 | throw new IllegalArgumentException("syntax error");
516 | }
517 | } catch (RuntimeException ex) {
518 | if (Constants.LOGV) {
519 | Log.d(Constants.TAG, "invalid selection [" + selection
520 | + "] triggered " + ex);
521 | } else if (Config.LOGD) {
522 | Log.d(Constants.TAG, "invalid selection triggered " + ex);
523 | }
524 | throw ex;
525 | }
526 |
527 | }
528 |
529 | // expression <- ( expression ) | statement [AND_OR ( expression ) |
530 | // statement] *
531 | // | statement [AND_OR expression]*
532 | private static void parseExpression(Lexer lexer) {
533 | for (;;) {
534 | // ( expression )
535 | if (lexer.currentToken() == Lexer.TOKEN_OPEN_PAREN) {
536 | lexer.advance();
537 | parseExpression(lexer);
538 | if (lexer.currentToken() != Lexer.TOKEN_CLOSE_PAREN) {
539 | throw new IllegalArgumentException(
540 | "syntax error, unmatched parenthese");
541 | }
542 | lexer.advance();
543 | } else {
544 | // statement
545 | parseStatement(lexer);
546 | }
547 | if (lexer.currentToken() != Lexer.TOKEN_AND_OR) {
548 | break;
549 | }
550 | lexer.advance();
551 | }
552 | }
553 |
554 | // statement <- COLUMN COMPARE VALUE
555 | // | COLUMN IS NULL
556 | private static void parseStatement(Lexer lexer) {
557 | // both possibilities start with COLUMN
558 | if (lexer.currentToken() != Lexer.TOKEN_COLUMN) {
559 | throw new IllegalArgumentException(
560 | "syntax error, expected column name");
561 | }
562 | lexer.advance();
563 |
564 | // statement <- COLUMN COMPARE VALUE
565 | if (lexer.currentToken() == Lexer.TOKEN_COMPARE) {
566 | lexer.advance();
567 | if (lexer.currentToken() != Lexer.TOKEN_VALUE) {
568 | throw new IllegalArgumentException(
569 | "syntax error, expected quoted string");
570 | }
571 | lexer.advance();
572 | return;
573 | }
574 |
575 | // statement <- COLUMN IS NULL
576 | if (lexer.currentToken() == Lexer.TOKEN_IS) {
577 | lexer.advance();
578 | if (lexer.currentToken() != Lexer.TOKEN_NULL) {
579 | throw new IllegalArgumentException(
580 | "syntax error, expected NULL");
581 | }
582 | lexer.advance();
583 | return;
584 | }
585 |
586 | // didn't get anything good after COLUMN
587 | throw new IllegalArgumentException("syntax error after column name");
588 | }
589 |
590 | /**
591 | * A simple lexer that recognizes the words of our restricted subset of SQL
592 | * where clauses
593 | */
594 | private static class Lexer {
595 | public static final int TOKEN_START = 0;
596 | public static final int TOKEN_OPEN_PAREN = 1;
597 | public static final int TOKEN_CLOSE_PAREN = 2;
598 | public static final int TOKEN_AND_OR = 3;
599 | public static final int TOKEN_COLUMN = 4;
600 | public static final int TOKEN_COMPARE = 5;
601 | public static final int TOKEN_VALUE = 6;
602 | public static final int TOKEN_IS = 7;
603 | public static final int TOKEN_NULL = 8;
604 | public static final int TOKEN_END = 9;
605 |
606 | private final String mSelection;
607 | private final Set mAllowedColumns;
608 | private int mOffset = 0;
609 | private int mCurrentToken = TOKEN_START;
610 | private final char[] mChars;
611 |
612 | public Lexer(String selection, Set allowedColumns) {
613 | mSelection = selection;
614 | mAllowedColumns = allowedColumns;
615 | mChars = new char[mSelection.length()];
616 | mSelection.getChars(0, mChars.length, mChars, 0);
617 | advance();
618 | }
619 |
620 | public int currentToken() {
621 | return mCurrentToken;
622 | }
623 |
624 | public void advance() {
625 | char[] chars = mChars;
626 |
627 | // consume whitespace
628 | while (mOffset < chars.length && chars[mOffset] == ' ') {
629 | ++mOffset;
630 | }
631 |
632 | // end of input
633 | if (mOffset == chars.length) {
634 | mCurrentToken = TOKEN_END;
635 | return;
636 | }
637 |
638 | // "("
639 | if (chars[mOffset] == '(') {
640 | ++mOffset;
641 | mCurrentToken = TOKEN_OPEN_PAREN;
642 | return;
643 | }
644 |
645 | // ")"
646 | if (chars[mOffset] == ')') {
647 | ++mOffset;
648 | mCurrentToken = TOKEN_CLOSE_PAREN;
649 | return;
650 | }
651 |
652 | // "?"
653 | if (chars[mOffset] == '?') {
654 | ++mOffset;
655 | mCurrentToken = TOKEN_VALUE;
656 | return;
657 | }
658 |
659 | // "=" and "=="
660 | if (chars[mOffset] == '=') {
661 | ++mOffset;
662 | mCurrentToken = TOKEN_COMPARE;
663 | if (mOffset < chars.length && chars[mOffset] == '=') {
664 | ++mOffset;
665 | }
666 | return;
667 | }
668 |
669 | // ">" and ">="
670 | if (chars[mOffset] == '>') {
671 | ++mOffset;
672 | mCurrentToken = TOKEN_COMPARE;
673 | if (mOffset < chars.length && chars[mOffset] == '=') {
674 | ++mOffset;
675 | }
676 | return;
677 | }
678 |
679 | // "<", "<=" and "<>"
680 | if (chars[mOffset] == '<') {
681 | ++mOffset;
682 | mCurrentToken = TOKEN_COMPARE;
683 | if (mOffset < chars.length
684 | && (chars[mOffset] == '=' || chars[mOffset] == '>')) {
685 | ++mOffset;
686 | }
687 | return;
688 | }
689 |
690 | // "!="
691 | if (chars[mOffset] == '!') {
692 | ++mOffset;
693 | mCurrentToken = TOKEN_COMPARE;
694 | if (mOffset < chars.length && chars[mOffset] == '=') {
695 | ++mOffset;
696 | return;
697 | }
698 | throw new IllegalArgumentException(
699 | "Unexpected character after !");
700 | }
701 |
702 | // columns and keywords
703 | // first look for anything that looks like an identifier or a
704 | // keyword
705 | // and then recognize the individual words.
706 | // no attempt is made at discarding sequences of underscores with no
707 | // alphanumeric
708 | // characters, even though it's not clear that they'd be legal
709 | // column names.
710 | if (isIdentifierStart(chars[mOffset])) {
711 | int startOffset = mOffset;
712 | ++mOffset;
713 | while (mOffset < chars.length
714 | && isIdentifierChar(chars[mOffset])) {
715 | ++mOffset;
716 | }
717 | String word = mSelection.substring(startOffset, mOffset);
718 | if (mOffset - startOffset <= 4) {
719 | if (word.equals("IS")) {
720 | mCurrentToken = TOKEN_IS;
721 | return;
722 | }
723 | if (word.equals("OR") || word.equals("AND")) {
724 | mCurrentToken = TOKEN_AND_OR;
725 | return;
726 | }
727 | if (word.equals("NULL")) {
728 | mCurrentToken = TOKEN_NULL;
729 | return;
730 | }
731 | }
732 | if (mAllowedColumns.contains(word)) {
733 | mCurrentToken = TOKEN_COLUMN;
734 | return;
735 | }
736 | throw new IllegalArgumentException(
737 | "unrecognized column or keyword");
738 | }
739 |
740 | // quoted strings
741 | if (chars[mOffset] == '\'') {
742 | ++mOffset;
743 | while (mOffset < chars.length) {
744 | if (chars[mOffset] == '\'') {
745 | if (mOffset + 1 < chars.length
746 | && chars[mOffset + 1] == '\'') {
747 | ++mOffset;
748 | } else {
749 | break;
750 | }
751 | }
752 | ++mOffset;
753 | }
754 | if (mOffset == chars.length) {
755 | throw new IllegalArgumentException("unterminated string");
756 | }
757 | ++mOffset;
758 | mCurrentToken = TOKEN_VALUE;
759 | return;
760 | }
761 |
762 | // anything we don't recognize
763 | throw new IllegalArgumentException("illegal character: "
764 | + chars[mOffset]);
765 | }
766 |
767 | private static final boolean isIdentifierStart(char c) {
768 | return c == '_' || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z');
769 | }
770 |
771 | private static final boolean isIdentifierChar(char c) {
772 | return c == '_' || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')
773 | || (c >= '0' && c <= '9');
774 | }
775 | }
776 |
777 | /**
778 | * Delete the given file from device and delete its row from the downloads
779 | * database.
780 | */
781 | /* package */static void deleteFile(ContentResolver resolver, long id,
782 | String path, String mimeType) {
783 | try {
784 | File file = new File(path);
785 | file.delete();
786 | } catch (Exception e) {
787 | Log.w(Constants.TAG, "file: '" + path + "' couldn't be deleted", e);
788 | }
789 | resolver.delete(Downloads.ALL_DOWNLOADS_CONTENT_URI, Downloads._ID
790 | + " = ? ", new String[] { String.valueOf(id) });
791 | }
792 | }
793 |
--------------------------------------------------------------------------------
/filedownload/src/main/java/com/richsjeson/filedownload/api/DownloadThread.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2008 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.richsjeson.filedownload.api;
18 |
19 | import java.io.File;
20 | import java.io.FileNotFoundException;
21 | import java.io.FileOutputStream;
22 | import java.io.IOException;
23 | import java.io.InputStream;
24 | import java.io.SyncFailedException;
25 | import java.net.URI;
26 | import java.net.URISyntaxException;
27 | import java.util.Locale;
28 | import android.content.ContentValues;
29 | import android.content.Context;
30 | import android.os.PowerManager;
31 | import android.os.Process;
32 | import android.text.TextUtils;
33 | import android.util.Log;
34 | import android.util.Pair;
35 |
36 | import okhttp3.OkHttpClient;
37 | import okhttp3.Request;
38 | import okhttp3.Response;
39 | import okhttp3.internal.http2.Header;
40 |
41 | import static com.richsjeson.filedownload.api.Constants.TAG;
42 |
43 | /**
44 | * Runs an actual download
45 | */
46 | public class DownloadThread extends Thread {
47 |
48 | private Context mContext;
49 | private DownloadInfo mInfo;
50 | private SystemFacade mSystemFacade;
51 |
52 | public DownloadThread(Context context, SystemFacade systemFacade,
53 | DownloadInfo info) {
54 | mContext = context;
55 | mSystemFacade = systemFacade;
56 | mInfo = info;
57 | }
58 |
59 | /**
60 | * Returns the user agent provided by the initiating app, or use the default
61 | * one
62 | */
63 | private String userAgent() {
64 | String userAgent = mInfo.mUserAgent;
65 | if (userAgent != null) {
66 | }
67 | if (userAgent == null) {
68 | userAgent = Constants.DEFAULT_USER_AGENT;
69 | }
70 | return userAgent;
71 | }
72 |
73 | /**
74 | * State for the entire run() method.
75 | */
76 | private static class State {
77 | public String mFilename;
78 | public FileOutputStream mStream;
79 | public String mMimeType;
80 | public boolean mCountRetry = false;
81 | public int mRetryAfter = 0;
82 | public int mRedirectCount = 0;
83 | public String mNewUri;
84 | public boolean mGotData = false;
85 | public String mRequestUri;
86 |
87 | public State(DownloadInfo info) {
88 | mMimeType = sanitizeMimeType(info.mMimeType);
89 | mRequestUri = info.mUri;
90 | mFilename = info.mFileName;
91 | }
92 | }
93 |
94 | /**
95 | * State within executeDownload()
96 | */
97 | private static class InnerState {
98 | public int mBytesSoFar = 0;
99 | public String mHeaderETag;
100 | public boolean mContinuingDownload = false;
101 | public String mHeaderContentLength;
102 | public String mHeaderContentDisposition;
103 | public String mHeaderContentLocation;
104 | public int mBytesNotified = 0;
105 | public long mTimeLastNotification = 0;
106 | }
107 |
108 | /**
109 | * Raised from methods called by run() to indicate that the current request
110 | * should be stopped immediately.
111 | *
112 | * Note the message passed to this exception will be logged and therefore
113 | * must be guaranteed not to contain any PII, meaning it generally can't
114 | * include any information about the request URI, headers, or destination
115 | * filename.
116 | */
117 | private class StopRequest extends Throwable {
118 | private static final long serialVersionUID = 1L;
119 |
120 | public int mFinalStatus;
121 |
122 | public StopRequest(int finalStatus, String message) {
123 | super(message);
124 | mFinalStatus = finalStatus;
125 | }
126 |
127 | public StopRequest(int finalStatus, String message, Throwable throwable) {
128 | super(message, throwable);
129 | mFinalStatus = finalStatus;
130 | }
131 | }
132 |
133 | /**
134 | * Raised from methods called by executeDownload() to indicate that the
135 | * download should be retried immediately.
136 | */
137 | private class RetryDownload extends Throwable {
138 | private static final long serialVersionUID = 1L;
139 | }
140 |
141 | /**
142 | * Executes the download in a separate thread
143 | */
144 | public void run() {
145 | Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
146 |
147 | State state = new State(mInfo);
148 | OkHttpClient client=null;
149 | PowerManager.WakeLock wakeLock = null;
150 | int finalStatus = Downloads.STATUS_UNKNOWN_ERROR;
151 |
152 | try {
153 | PowerManager pm = (PowerManager) mContext
154 | .getSystemService(Context.POWER_SERVICE);
155 | wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
156 | TAG);
157 | wakeLock.acquire();
158 |
159 | if (Constants.LOGV) {
160 | Log.v(TAG, "initiating download for " + mInfo.mUri);
161 | }
162 | client=new OkHttpClient();
163 |
164 | boolean finished = false;
165 | while (!finished) {
166 | Log.i(TAG, "Initiating request for download "
167 | + mInfo.mId);
168 | Request request = new Request.Builder().url(state.mRequestUri).build();
169 | try {
170 | executeDownload(state, client, request);
171 | finished = true;
172 | } catch (RetryDownload exc) {
173 | // fall through
174 | } finally {
175 | request = null;
176 | }
177 | }
178 |
179 | if (Constants.LOGV) {
180 | Log.v(TAG, "download completed for " + mInfo.mUri);
181 | }
182 | finalizeDestinationFile(state);
183 | finalStatus = Downloads.STATUS_SUCCESS;
184 | } catch (StopRequest error) {
185 | // remove the cause before printing, in case it contains PII
186 | Log.w(TAG, "Aborting request for download " + mInfo.mId
187 | + ": " + error.getMessage());
188 | finalStatus = error.mFinalStatus;
189 |
190 | // fall through to finally block
191 | } catch (Throwable ex) { // sometimes the socket code throws unchecked
192 | // exceptions
193 | Log.w(TAG, "Exception for id " + mInfo.mId + ": " + ex);
194 | finalStatus = Downloads.STATUS_UNKNOWN_ERROR;
195 | // falls through to the code that reports an error
196 | } finally {
197 | if (wakeLock != null) {
198 | wakeLock.release();
199 | wakeLock = null;
200 | }
201 | if (client != null) {
202 | client = null;
203 | }
204 | cleanupDestination(state, finalStatus);
205 | notifyDownloadCompleted(finalStatus, state.mCountRetry,
206 | state.mRetryAfter, state.mGotData, state.mFilename,
207 | state.mNewUri, state.mMimeType,finalStatus);
208 | mInfo.mHasActiveThread = false;
209 | }
210 | }
211 |
212 | /**
213 | * Fully execute a single download request - setup and send the request,
214 | * handle the response, and transfer the data to the destination file.
215 | */
216 | private void executeDownload(State state, OkHttpClient client,
217 | Request request) throws StopRequest, RetryDownload {
218 | InnerState innerState = new InnerState();
219 | byte data[] = new byte[Constants.BUFFER_SIZE];
220 |
221 | setupDestinationFile(state, innerState);
222 | addRequestHeaders(innerState, request);
223 |
224 | // check just before sending the request to avoid using an invalid
225 | // connection at all
226 | checkConnectivity(state);
227 |
228 | Response response = sendRequest(state, client, request);
229 | handleExceptionalStatus(state, innerState, response);
230 |
231 | if (Constants.LOGV) {
232 | Log.v(TAG, "received response for " + mInfo.mUri);
233 | }
234 |
235 | processResponseHeaders(state, innerState, response);
236 | InputStream entityStream = openResponseEntity(state, response);
237 | transferData(state, innerState, data, entityStream);
238 | }
239 |
240 | /**
241 | * Check if current connectivity is valid for this request.
242 | */
243 | private void checkConnectivity(State state) throws StopRequest {
244 | int networkUsable = mInfo.checkCanUseNetwork();
245 | if (networkUsable != DownloadInfo.NETWORK_OK) {
246 | int status = Downloads.STATUS_WAITING_FOR_NETWORK;
247 | if (networkUsable == DownloadInfo.NETWORK_UNUSABLE_DUE_TO_SIZE) {
248 | status = Downloads.STATUS_QUEUED_FOR_WIFI;
249 | } else if (networkUsable == DownloadInfo.NETWORK_RECOMMENDED_UNUSABLE_DUE_TO_SIZE) {
250 | status = Downloads.STATUS_QUEUED_FOR_WIFI;
251 | }
252 | throw new StopRequest(status,
253 | mInfo.getLogMessageForNetworkError(networkUsable));
254 | }
255 | }
256 |
257 | /**
258 | * Transfer as much data as possible from the HTTP response to the
259 | * destination file.
260 | *
261 | * @param data
262 | * buffer to use to read data
263 | * @param entityStream
264 | * stream for reading the HTTP response entity
265 | */
266 | private void transferData(State state, InnerState innerState, byte[] data,
267 | InputStream entityStream) throws StopRequest {
268 | for (;;) {
269 | int bytesRead = readFromResponse(state, innerState, data,
270 | entityStream);
271 | if (bytesRead == -1) { // success, end of stream already reached
272 | handleEndOfStream(state, innerState);
273 | return;
274 | }
275 |
276 | state.mGotData = true;
277 | writeDataToDestination(state, data, bytesRead);
278 | innerState.mBytesSoFar += bytesRead;
279 | reportProgress(state, innerState);
280 |
281 | if (Constants.LOGVV) {
282 | Log.v(TAG, "downloaded " + innerState.mBytesSoFar
283 | + " for " + mInfo.mUri);
284 | }
285 |
286 | checkPausedOrCanceled(state);
287 | }
288 | }
289 |
290 | /**
291 | * Called after a successful completion to take any necessary action on the
292 | * downloaded file.
293 | */
294 | private void finalizeDestinationFile(State state) throws StopRequest {
295 | syncDestination(state);
296 | }
297 |
298 | /**
299 | * Called just before the thread finishes, regardless of status, to take any
300 | * necessary action on the downloaded file.
301 | */
302 | private void cleanupDestination(State state, int finalStatus) {
303 | closeDestination(state);
304 | if (state.mFilename != null && Downloads.isStatusError(finalStatus)) {
305 | //文件被删除
306 | Log.i(TAG,"cleanupDestination 文件被删除");
307 | new File(state.mFilename).delete();
308 | state.mFilename = null;
309 | }
310 | }
311 |
312 | /**
313 | * Sync the destination file to storage.
314 | */
315 | private void syncDestination(State state) {
316 | FileOutputStream downloadedFileStream = null;
317 | try {
318 | downloadedFileStream = new FileOutputStream(state.mFilename, true);
319 | downloadedFileStream.getFD().sync();
320 | } catch (FileNotFoundException ex) {
321 | Log.w(TAG, "file " + state.mFilename + " not found: "
322 | + ex);
323 | } catch (SyncFailedException ex) {
324 | Log.w(TAG, "file " + state.mFilename + " sync failed: "
325 | + ex);
326 | } catch (IOException ex) {
327 | Log.w(TAG, "IOException trying to sync "
328 | + state.mFilename + ": " + ex);
329 | } catch (RuntimeException ex) {
330 | Log.w(TAG, "exception while syncing file: ", ex);
331 | } finally {
332 | if (downloadedFileStream != null) {
333 | try {
334 | downloadedFileStream.close();
335 | } catch (IOException ex) {
336 | Log.w(TAG,
337 | "IOException while closing synced file: ", ex);
338 | } catch (RuntimeException ex) {
339 | Log.w(TAG, "exception while closing file: ", ex);
340 | }
341 | }
342 | }
343 | }
344 |
345 | /**
346 | * Close the destination output stream.
347 | */
348 | private void closeDestination(State state) {
349 | try {
350 | // close the file
351 | if (state.mStream != null) {
352 | state.mStream.close();
353 | state.mStream = null;
354 | }
355 | } catch (IOException ex) {
356 | if (Constants.LOGV) {
357 | Log.v(TAG,
358 | "exception when closing the file after download : "
359 | + ex);
360 | }
361 | // nothing can really be done if the file can't be closed
362 | }
363 | }
364 |
365 | /**
366 | * Check if the download has been paused or canceled, stopping the request
367 | * appropriately if it has been.
368 | */
369 | private void checkPausedOrCanceled(State state) throws StopRequest {
370 | synchronized (mInfo) {
371 | if (mInfo.mControl == Downloads.CONTROL_PAUSED) {
372 | throw new StopRequest(Downloads.STATUS_PAUSED_BY_APP,
373 | "download paused by owner");
374 | }
375 | }
376 | if (mInfo.mStatus == Downloads.STATUS_CANCELED) {
377 | throw new StopRequest(Downloads.STATUS_CANCELED,
378 | "download canceled");
379 | }
380 | }
381 |
382 | /**
383 | * Report download progress through the database if necessary.
384 | */
385 | private void reportProgress(State state, InnerState innerState) {
386 | long now = mSystemFacade.currentTimeMillis();
387 | if (innerState.mBytesSoFar - innerState.mBytesNotified > Constants.MIN_PROGRESS_STEP
388 | && now - innerState.mTimeLastNotification > Constants.MIN_PROGRESS_TIME) {
389 | ContentValues values = new ContentValues();
390 | values.put(Downloads.COLUMN_CURRENT_BYTES, innerState.mBytesSoFar);
391 | mContext.getContentResolver().update(mInfo.getAllDownloadsUri(),
392 | values, null, null);
393 | innerState.mBytesNotified = innerState.mBytesSoFar;
394 | innerState.mTimeLastNotification = now;
395 | }
396 | }
397 |
398 | /**
399 | * Write a data buffer to the destination file.
400 | *
401 | * @param data
402 | * buffer containing the data to write
403 | * @param bytesRead
404 | * how many bytes to write from the buffer
405 | */
406 | private void writeDataToDestination(State state, byte[] data, int bytesRead)
407 | throws StopRequest {
408 | for (;;) {
409 | try {
410 | if (state.mStream == null) {
411 | state.mStream = new FileOutputStream(state.mFilename, true);
412 | }
413 | state.mStream.write(data, 0, bytesRead);
414 | if (mInfo.mDestination == Downloads.DESTINATION_EXTERNAL) {
415 | closeDestination(state);
416 | }
417 | return;
418 | } catch (IOException ex) {
419 | if (!Helpers.isExternalMediaMounted()) {
420 | throw new StopRequest(
421 | Downloads.STATUS_DEVICE_NOT_FOUND_ERROR,
422 | "external media not mounted while writing destination file");
423 | }
424 |
425 | long availableBytes = Helpers.getAvailableBytes(Helpers
426 | .getFilesystemRoot(state.mFilename));
427 | if (availableBytes < bytesRead) {
428 | throw new StopRequest(
429 | Downloads.STATUS_INSUFFICIENT_SPACE_ERROR,
430 | "insufficient space while writing destination file",
431 | ex);
432 | }
433 | throw new StopRequest(Downloads.STATUS_FILE_ERROR,
434 | "while writing destination file: " + ex.toString(), ex);
435 | }
436 | }
437 | }
438 |
439 | /**
440 | * Called when we've reached the end of the HTTP response stream, to update
441 | * the database and check for consistency.
442 | */
443 | private void handleEndOfStream(State state, InnerState innerState)
444 | throws StopRequest {
445 | ContentValues values = new ContentValues();
446 | values.put(Downloads.COLUMN_CURRENT_BYTES, innerState.mBytesSoFar);
447 | if (innerState.mHeaderContentLength == null) {
448 | values.put(Downloads.COLUMN_TOTAL_BYTES, innerState.mBytesSoFar);
449 | }
450 | mContext.getContentResolver().update(mInfo.getAllDownloadsUri(),
451 | values, null, null);
452 |
453 | boolean lengthMismatched = (innerState.mHeaderContentLength != null)
454 | && (innerState.mBytesSoFar != Integer
455 | .parseInt(innerState.mHeaderContentLength));
456 | if (lengthMismatched) {
457 | if (cannotResume(innerState)) {
458 | throw new StopRequest(Downloads.STATUS_CANNOT_RESUME,
459 | "mismatched content length");
460 | } else {
461 | throw new StopRequest(getFinalStatusForHttpError(state),
462 | "closed socket before end of file");
463 | }
464 | }
465 | }
466 |
467 | private boolean cannotResume(InnerState innerState) {
468 | return innerState.mBytesSoFar > 0 && !mInfo.mNoIntegrity
469 | && innerState.mHeaderETag == null;
470 | }
471 |
472 | /**
473 | * Read some data from the HTTP response stream, handling I/O errors.
474 | *
475 | * @param data
476 | * buffer to use to read data
477 | * @param entityStream
478 | * stream for reading the HTTP response entity
479 | * @return the number of bytes actually read or -1 if the end of the stream
480 | * has been reached
481 | */
482 | private int readFromResponse(State state, InnerState innerState,
483 | byte[] data, InputStream entityStream) throws StopRequest {
484 | try {
485 | return entityStream.read(data);
486 | } catch (IOException ex) {
487 | logNetworkState();
488 | ContentValues values = new ContentValues();
489 | values.put(Downloads.COLUMN_CURRENT_BYTES, innerState.mBytesSoFar);
490 | mContext.getContentResolver().update(mInfo.getAllDownloadsUri(),
491 | values, null, null);
492 | if (cannotResume(innerState)) {
493 | String message = "while reading response: " + ex.toString()
494 | + ", can't resume interrupted download with no ETag";
495 | throw new StopRequest(Downloads.STATUS_CANNOT_RESUME, message,
496 | ex);
497 | } else {
498 | throw new StopRequest(getFinalStatusForHttpError(state),
499 | "while reading response: " + ex.toString(), ex);
500 | }
501 | }
502 | }
503 |
504 | /**
505 | * Open a stream for the HTTP response entity, handling I/O errors.
506 | *
507 | * @return an InputStream to read the response entity
508 | */
509 | private InputStream openResponseEntity(State state, Response response)
510 | throws StopRequest {
511 | return response.body().byteStream();
512 | }
513 |
514 | private void logNetworkState() {
515 | if (Constants.LOGX) {
516 | Log.i(TAG,
517 | "Net "
518 | + (Helpers.isNetworkAvailable(mSystemFacade) ? "Up"
519 | : "Down"));
520 | }
521 | }
522 |
523 | /**
524 | * Read HTTP response headers and take appropriate action, including setting
525 | * up the destination file and updating the database.
526 | */
527 | private void processResponseHeaders(State state, InnerState innerState,
528 | Response response) throws StopRequest {
529 | if (innerState.mContinuingDownload) {
530 | // ignore response headers on resume requests
531 | return;
532 | }
533 |
534 | readResponseHeaders(state, innerState, response);
535 |
536 | try {
537 | state.mFilename = Helpers.generateSaveFile(
538 | mContext,
539 | mInfo.mUri,
540 | mInfo.mHint,
541 | innerState.mHeaderContentDisposition,
542 | innerState.mHeaderContentLocation,
543 | state.mMimeType,
544 | mInfo.mDestination,
545 | (innerState.mHeaderContentLength != null) ? Long
546 | .parseLong(innerState.mHeaderContentLength) : 0,
547 | mInfo.mIsPublicApi);
548 | } catch (Helpers.GenerateSaveFileError exc) {
549 | throw new StopRequest(exc.mStatus, exc.mMessage);
550 | }
551 | try {
552 | state.mStream = new FileOutputStream(state.mFilename);
553 | } catch (FileNotFoundException exc) {
554 | throw new StopRequest(Downloads.STATUS_FILE_ERROR,
555 | "while opening destination file: " + exc.toString(), exc);
556 | }
557 | if (Constants.LOGV) {
558 | Log.v(TAG, "writing " + mInfo.mUri + " to "
559 | + state.mFilename);
560 | }
561 |
562 | updateDatabaseFromHeaders(state, innerState);
563 | // check connectivity again now that we know the total size
564 | checkConnectivity(state);
565 | }
566 |
567 | /**
568 | * Update necessary database fields based on values of HTTP response headers
569 | * that have been read.
570 | */
571 | private void updateDatabaseFromHeaders(State state, InnerState innerState) {
572 | ContentValues values = new ContentValues();
573 | values.put(Downloads._DATA, state.mFilename);
574 | if (innerState.mHeaderETag != null) {
575 | values.put(Constants.ETAG, innerState.mHeaderETag);
576 | }
577 | if (state.mMimeType != null) {
578 | values.put(Downloads.COLUMN_MIME_TYPE, state.mMimeType);
579 | }
580 | values.put(Downloads.COLUMN_TOTAL_BYTES, mInfo.mTotalBytes);
581 | mContext.getContentResolver().update(mInfo.getAllDownloadsUri(),
582 | values, null, null);
583 | }
584 |
585 | /**
586 | * Read headers from the HTTP response and store them into local state.
587 | */
588 | private void readResponseHeaders(State state, InnerState innerState,
589 | Response response) throws StopRequest {
590 | String headerValue = response.networkResponse().header("Content-Disposition");
591 | if (headerValue != null) {
592 | innerState.mHeaderContentDisposition = headerValue;
593 | }
594 | headerValue = response.networkResponse().header("Content-Location");
595 | if (headerValue != null) {
596 | innerState.mHeaderContentLocation = headerValue;
597 | }
598 | if (state.mMimeType == null) {
599 | headerValue = response.networkResponse().request().header("Content-Type");
600 | if (headerValue != null) {
601 | state.mMimeType = sanitizeMimeType(headerValue);
602 | }
603 | }
604 | headerValue = response.networkResponse().header("ETag");
605 | if (headerValue != null) {
606 | innerState.mHeaderETag = headerValue;
607 | }
608 | String headerTransferEncoding = null;
609 | headerValue = response.networkResponse().header("Transfer-Encoding");
610 | if (headerValue != null) {
611 | headerTransferEncoding = headerValue;
612 | }
613 | if (headerTransferEncoding == null) {
614 | headerValue = response.networkResponse().header("Content-Length");
615 | if (headerValue != null) {
616 | innerState.mHeaderContentLength = headerValue;
617 | mInfo.mTotalBytes = Long.parseLong(innerState.mHeaderContentLength);
618 | }
619 | } else {
620 | // Ignore content-length with transfer-encoding - 2616 4.4 3
621 | if (Constants.LOGVV) {
622 | Log.v(TAG,
623 | "ignoring content-length because of xfer-encoding");
624 | }
625 | }
626 | if (Constants.LOGVV) {
627 | Log.v(TAG, "Content-Disposition: "
628 | + innerState.mHeaderContentDisposition);
629 | Log.v(TAG, "Content-Length: "
630 | + innerState.mHeaderContentLength);
631 | Log.v(TAG, "Content-Location: "
632 | + innerState.mHeaderContentLocation);
633 | Log.v(TAG, "Content-Type: " + state.mMimeType);
634 | Log.v(TAG, "ETag: " + innerState.mHeaderETag);
635 | Log.v(TAG, "Transfer-Encoding: " + headerTransferEncoding);
636 | }
637 |
638 | boolean noSizeInfo = innerState.mHeaderContentLength == null
639 | && (headerTransferEncoding == null || !headerTransferEncoding
640 | .equalsIgnoreCase("chunked"));
641 | if (!mInfo.mNoIntegrity && noSizeInfo) {
642 | throw new StopRequest(Downloads.STATUS_HTTP_DATA_ERROR,
643 | "can't know size of download, giving up");
644 | }
645 | }
646 |
647 | /**
648 | * Check the HTTP response status and handle anything unusual (e.g. not
649 | * 200/206).
650 | */
651 | private void handleExceptionalStatus(State state, InnerState innerState,
652 | Response response) throws StopRequest, RetryDownload {
653 | int statusCode = response.code();
654 | if (statusCode == 503 && mInfo.mNumFailed < Constants.MAX_RETRIES) {
655 | handleServiceUnavailable(state, response);
656 | }
657 | if (statusCode == 301 || statusCode == 302 || statusCode == 303
658 | || statusCode == 307) {
659 | handleRedirect(state, response, statusCode);
660 | }
661 |
662 | int expectedStatus = innerState.mContinuingDownload ? 206
663 | : Downloads.STATUS_SUCCESS;
664 | if (statusCode != expectedStatus) {
665 | handleOtherStatus(state, innerState, statusCode);
666 | }
667 | }
668 |
669 | /**
670 | * Handle a status that we don't know how to deal with properly.
671 | */
672 | private void handleOtherStatus(State state, InnerState innerState,
673 | int statusCode) throws StopRequest {
674 | int finalStatus;
675 | if (Downloads.isStatusError(statusCode)) {
676 | finalStatus = statusCode;
677 | throw new StopRequest(finalStatus, "http error " + statusCode);
678 | } else if (statusCode >= 300 && statusCode < 400) {
679 | finalStatus = Downloads.STATUS_UNHANDLED_REDIRECT;
680 | throw new StopRequest(finalStatus, "http error " + statusCode);
681 | } else if (innerState.mContinuingDownload
682 | && statusCode == Downloads.STATUS_SUCCESS) {
683 | finalStatus = Downloads.STATUS_CANNOT_RESUME;
684 | } else {
685 | finalStatus = Downloads.STATUS_UNHANDLED_HTTP_CODE;
686 | throw new StopRequest(finalStatus, "http error " + statusCode);
687 | }
688 | }
689 |
690 | /**
691 | * Handle a 3xx redirect status.
692 | */
693 | private void handleRedirect(State state, Response response,
694 | int statusCode) throws StopRequest, RetryDownload {
695 | if (Constants.LOGVV) {
696 | Log.v(TAG, "got HTTP redirect " + statusCode);
697 | }
698 | if (state.mRedirectCount >= Constants.MAX_REDIRECTS) {
699 | throw new StopRequest(Downloads.STATUS_TOO_MANY_REDIRECTS,
700 | "too many redirects");
701 | }
702 | String header = response.networkResponse().header("Location");
703 | if (header == null) {
704 | return;
705 | }
706 | if (Constants.LOGVV) {
707 | Log.v(TAG, "Location :" + header);
708 | }
709 |
710 | String newUri;
711 | try {
712 | newUri = new URI(mInfo.mUri).resolve(new URI(header))
713 | .toString();
714 | } catch (URISyntaxException ex) {
715 | if (Constants.LOGV) {
716 | Log.d(TAG,
717 | "Couldn't resolve redirect URI " + header
718 | + " for " + mInfo.mUri);
719 | }
720 | throw new StopRequest(Downloads.STATUS_HTTP_DATA_ERROR,
721 | "Couldn't resolve redirect URI");
722 | }
723 | ++state.mRedirectCount;
724 | state.mRequestUri = newUri;
725 | if (statusCode == 301 || statusCode == 303) {
726 | // use the new URI for all future requests (should a retry/resume be
727 | // necessary)
728 | state.mNewUri = newUri;
729 | }
730 | throw new RetryDownload();
731 | }
732 |
733 | /**
734 | * Handle a 503 Service Unavailable status by processing the Retry-After
735 | * header.
736 | */
737 | private void handleServiceUnavailable(State state, Response response)
738 | throws StopRequest {
739 | if (Constants.LOGVV) {
740 | Log.v(TAG, "got HTTP response code 503");
741 | }
742 | state.mCountRetry = true;
743 | String header= response.networkResponse().header("Retry-After");
744 | if (header != null) {
745 | try {
746 | if (Constants.LOGVV) {
747 | Log.v(TAG, "Retry-After :" + header);
748 | }
749 | state.mRetryAfter = Integer.parseInt(header);
750 | if (state.mRetryAfter < 0) {
751 | state.mRetryAfter = 0;
752 | } else {
753 | if (state.mRetryAfter < Constants.MIN_RETRY_AFTER) {
754 | state.mRetryAfter = Constants.MIN_RETRY_AFTER;
755 | } else if (state.mRetryAfter > Constants.MAX_RETRY_AFTER) {
756 | state.mRetryAfter = Constants.MAX_RETRY_AFTER;
757 | }
758 | state.mRetryAfter += Helpers.sRandom
759 | .nextInt(Constants.MIN_RETRY_AFTER + 1);
760 | state.mRetryAfter *= 1000;
761 | }
762 | } catch (NumberFormatException ex) {
763 | // ignored - retryAfter stays 0 in this case.
764 | }
765 | }
766 | throw new StopRequest(Downloads.STATUS_WAITING_TO_RETRY,
767 | "got 503 Service Unavailable, will retry later");
768 | }
769 |
770 | /**
771 | * Send the request to the server, handling any I/O exceptions.
772 | */
773 | private Response sendRequest(State state, OkHttpClient client,
774 | Request request) throws StopRequest {
775 | try {
776 | return client.newCall(request).execute();
777 | } catch (IllegalArgumentException ex) {
778 | throw new StopRequest(Downloads.STATUS_HTTP_DATA_ERROR,
779 | "while trying to execute request: " + ex.toString(), ex);
780 | } catch (IOException ex) {
781 | logNetworkState();
782 | throw new StopRequest(getFinalStatusForHttpError(state),
783 | "while trying to execute request: " + ex.toString(), ex);
784 | }
785 | }
786 |
787 | private int getFinalStatusForHttpError(State state) {
788 | if (!Helpers.isNetworkAvailable(mSystemFacade)) {
789 | return Downloads.STATUS_WAITING_FOR_NETWORK;
790 | } else if (mInfo.mNumFailed < Constants.MAX_RETRIES) {
791 | state.mCountRetry = true;
792 | return Downloads.STATUS_WAITING_TO_RETRY;
793 | } else {
794 | Log.w(TAG, "reached max retries for " + mInfo.mId);
795 | return Downloads.STATUS_HTTP_DATA_ERROR;
796 | }
797 | }
798 |
799 | /**
800 | * Prepare the destination file to receive data. If the file already exists,
801 | * we'll set up appropriately for resumption.
802 | */
803 | private void setupDestinationFile(State state, InnerState innerState)
804 | throws StopRequest {
805 | if (!TextUtils.isEmpty(state.mFilename)) { // only true if we've already
806 | // run a thread for this
807 | // download
808 | if (!Helpers.isFilenameValid(state.mFilename)) {
809 | // this should never happen
810 | throw new StopRequest(Downloads.STATUS_FILE_ERROR,
811 | "found invalid internal destination filename");
812 | }
813 | // We're resuming a download that got interrupted
814 | File f = new File(state.mFilename);
815 | Log.i(TAG,"文件操作");
816 | if (f.exists()) {
817 | long fileLength = f.length();
818 | if (fileLength == 0) {
819 | // The download hadn't actually started, we can restart from
820 | // scratch
821 | Log.i(TAG,"文件被删除");
822 | f.delete();
823 | state.mFilename = null;
824 | } else if (mInfo.mETag == null && !mInfo.mNoIntegrity) {
825 | // This should've been caught upon failure
826 | f.delete();
827 | Log.i(TAG,"文件被删除2");
828 | throw new StopRequest(Downloads.STATUS_CANNOT_RESUME,
829 | "Trying to resume a download that can't be resumed");
830 | } else {
831 | // All right, we'll be able to resume this download
832 | try {
833 | state.mStream = new FileOutputStream(state.mFilename,
834 | true);
835 | } catch (FileNotFoundException exc) {
836 | throw new StopRequest(Downloads.STATUS_FILE_ERROR,
837 | "while opening destination for resuming: "
838 | + exc.toString(), exc);
839 | }
840 | innerState.mBytesSoFar = (int) fileLength;
841 | Log.i(TAG,"innerState.mBytesSoFar:"+innerState.mBytesSoFar);
842 | if (mInfo.mTotalBytes != -1) {
843 | innerState.mHeaderContentLength = Long
844 | .toString(mInfo.mTotalBytes);
845 | }
846 | innerState.mHeaderETag = mInfo.mETag;
847 | innerState.mContinuingDownload = true;
848 | }
849 | }
850 | }
851 |
852 | if (state.mStream != null
853 | && mInfo.mDestination == Downloads.DESTINATION_EXTERNAL) {
854 | closeDestination(state);
855 | }
856 | }
857 |
858 | /**
859 | * Add custom headers for this download to the HTTP request.
860 | */
861 | private void addRequestHeaders(InnerState innerState, Request request) {
862 |
863 | Request.Builder requestBuilder=request.newBuilder();
864 |
865 | for (Pair header : mInfo.getHeaders()) {
866 | requestBuilder.addHeader(header.first, header.second);
867 | }
868 |
869 | if (innerState.mContinuingDownload) {
870 | if (innerState.mHeaderETag != null) {
871 | requestBuilder.addHeader("If-Match", innerState.mHeaderETag);
872 |
873 | }
874 | requestBuilder.addHeader("Range", "bytes=" + innerState.mBytesSoFar + "-"+mInfo.mTotalBytes);
875 | }
876 | requestBuilder.build();
877 | }
878 |
879 | /**
880 | * Stores information about the completed download, and notifies the
881 | * initiating application.
882 | */
883 | private void notifyDownloadCompleted(int status, boolean countRetry,
884 | int retryAfter, boolean gotData, String filename, String uri,
885 | String mimeType,int errorCode) {
886 | notifyThroughDatabase(status, countRetry, retryAfter, gotData,
887 | filename, uri, mimeType,errorCode);
888 | if (Downloads.isStatusCompleted(status)) {
889 | mInfo.sendIntentIfRequested();
890 | }
891 | }
892 |
893 | private void notifyThroughDatabase(int status, boolean countRetry,
894 | int retryAfter, boolean gotData, String filename, String uri,
895 | String mimeType,int mErrorCode) {
896 | ContentValues values = new ContentValues();
897 | values.put(Downloads.COLUMN_STATUS, status);
898 | values.put(Downloads._DATA, filename);
899 | values.put(Downloads.COLUMN_ERROR_CODE,mErrorCode);
900 | if (uri != null) {
901 | values.put(Downloads.COLUMN_URI, uri);
902 | }
903 | values.put(Downloads.COLUMN_MIME_TYPE, mimeType);
904 | values.put(Downloads.COLUMN_LAST_MODIFICATION,
905 | mSystemFacade.currentTimeMillis());
906 | values.put(Constants.RETRY_AFTER_X_REDIRECT_COUNT, retryAfter);
907 | if (!countRetry) {
908 | values.put(Constants.FAILED_CONNECTIONS, 0);
909 | } else if (gotData) {
910 | values.put(Constants.FAILED_CONNECTIONS, 1);
911 | } else {
912 | values.put(Constants.FAILED_CONNECTIONS, mInfo.mNumFailed + 1);
913 | }
914 |
915 | mContext.getContentResolver().update(mInfo.getAllDownloadsUri(),
916 | values, null, null);
917 | }
918 |
919 | /**
920 | * Clean up a mimeType string so it can be used to dispatch an intent to
921 | * view a downloaded asset.
922 | *
923 | * @param mimeType
924 | * either null or one or more mime types (semi colon separated).
925 | * @return null if mimeType was null. Otherwise a string which represents a
926 | * single mimetype in lowercase and with surrounding whitespaces
927 | * trimmed.
928 | */
929 | private static String sanitizeMimeType(String mimeType) {
930 | try {
931 | mimeType = mimeType.trim().toLowerCase(Locale.ENGLISH);
932 |
933 | final int semicolonIndex = mimeType.indexOf(';');
934 | if (semicolonIndex != -1) {
935 | mimeType = mimeType.substring(0, semicolonIndex);
936 | }
937 | return mimeType;
938 | } catch (NullPointerException npe) {
939 | return null;
940 | }
941 | }
942 | }
943 |
--------------------------------------------------------------------------------