66 | * Any database updates important enough to initiate tasks should always be
67 | * delivered through {@link Context#startService(Intent)}.
68 | */
69 | public class DownloadService extends Service {
70 | // TODO: migrate WakeLock from individual DownloadThreads out into
71 | // DownloadReceiver to protect our entire workflow.
72 |
73 | private static final boolean DEBUG_LIFECYCLE = true;
74 |
75 | private AlarmManager mAlarmManager;
76 |
77 | /** Observer to get notified when the content observer's data changes */
78 | private DownloadManagerContentObserver mObserver;
79 |
80 | /** Class to handle Notification Manager updates */
81 | private DownloadNotifier mNotifier;
82 |
83 | private static final int CLEANUP_JOB_ID = 1;
84 | private static final long CLEANUP_JOB_PERIOD = 1000 * 60 * 60 * 24; // one day
85 |
86 | /**
87 | * The Service's view of the list of downloads, mapping download IDs to the corresponding info
88 | * object. This is kept independently from the content provider, and the Service only initiates
89 | * downloads based on this data, so that it can deal with situation where the data in the
90 | * content provider changes or disappears.
91 | */
92 | @GuardedBy("mDownloads")
93 | private final Map
75 | * To know if a download is successful, we need to know either the final content
76 | * length to expect, or the transfer to be chunked. To resume an interrupted
77 | * download, we need an ETag.
78 | *
79 | * Failed network requests are retried several times before giving up. Local
80 | * disk errors fail immediately and are not retried.
81 | */
82 | public class DownloadThread implements Runnable {
83 |
84 | // TODO: bind each download to a specific network interface to avoid state
85 | // checking races once we have ConnectivityManager API
86 |
87 | // TODO: add support for saving to content://
88 |
89 | private static final int HTTP_REQUESTED_RANGE_NOT_SATISFIABLE = 416;
90 | private static final int HTTP_TEMP_REDIRECT = 307;
91 |
92 | private static final int DEFAULT_TIMEOUT = (int) (20 * SECOND_IN_MILLIS);
93 |
94 | private final Context mContext;
95 | private final DownloadNotifier mNotifier;
96 |
97 | private final long mId;
98 |
99 | /**
100 | * Info object that should be treated as read-only. Any potentially mutated
101 | * fields are tracked in {@link #mInfoDelta}. If a field exists in
102 | * {@link #mInfoDelta}, it must not be read from {@link #mInfo}.
103 | */
104 | private final DownloadInfo mInfo;
105 | private final DownloadInfoDelta mInfoDelta;
106 |
107 | private volatile boolean mPolicyDirty;
108 |
109 | /**
110 | * Local changes to {@link DownloadInfo}. These are kept local to avoid
111 | * racing with the thread that updates based on change notifications.
112 | */
113 | private class DownloadInfoDelta {
114 | public String mUri;
115 | public String mFileName;
116 | public String mMimeType;
117 | public int mStatus;
118 | public int mNumFailed;
119 | public int mRetryAfter;
120 | public long mTotalBytes;
121 | public long mCurrentBytes;
122 | public String mETag;
123 |
124 | public String mErrorMsg;
125 |
126 | public DownloadInfoDelta(DownloadInfo info) {
127 | mUri = info.mUri;
128 | mFileName = info.mFileName;
129 | mMimeType = info.mMimeType;
130 | mStatus = info.mStatus;
131 | mNumFailed = info.mNumFailed;
132 | mRetryAfter = info.mRetryAfter;
133 | mTotalBytes = info.mTotalBytes;
134 | mCurrentBytes = info.mCurrentBytes;
135 | mETag = info.mETag;
136 | }
137 |
138 | private ContentValues buildContentValues() {
139 | final ContentValues values = new ContentValues();
140 |
141 | values.put(Downloads.Impl.COLUMN_URI, mUri);
142 | values.put(Downloads.Impl._DATA, mFileName);
143 | values.put(Downloads.Impl.COLUMN_MIME_TYPE, mMimeType);
144 | values.put(Downloads.Impl.COLUMN_STATUS, mStatus);
145 | values.put(Downloads.Impl.COLUMN_FAILED_CONNECTIONS, mNumFailed);
146 | values.put(Constants.RETRY_AFTER_X_REDIRECT_COUNT, mRetryAfter);
147 | values.put(Downloads.Impl.COLUMN_TOTAL_BYTES, mTotalBytes);
148 | values.put(Downloads.Impl.COLUMN_CURRENT_BYTES, mCurrentBytes);
149 | values.put(Constants.ETAG, mETag);
150 |
151 | values.put(Downloads.Impl.COLUMN_LAST_MODIFICATION, System.currentTimeMillis());
152 | values.put(Downloads.Impl.COLUMN_ERROR_MSG, mErrorMsg);
153 |
154 | return values;
155 | }
156 |
157 | /**
158 | * Blindly push update of current delta values to provider.
159 | */
160 | public void writeToDatabase() {
161 | mContext.getContentResolver().update(mInfo.getAllDownloadsUri(), buildContentValues(),
162 | null, null);
163 | }
164 |
165 | /**
166 | * Push update of current delta values to provider, asserting strongly
167 | * that we haven't been paused or deleted.
168 | */
169 | public void writeToDatabaseOrThrow() throws StopRequestException {
170 | if (mContext.getContentResolver().update(mInfo.getAllDownloadsUri(),
171 | buildContentValues(), Downloads.Impl.COLUMN_DELETED + " == '0'", null) == 0) {
172 | throw new StopRequestException(STATUS_CANCELED, "Download deleted or missing!");
173 | }
174 | }
175 | }
176 |
177 | /**
178 | * Flag indicating if we've made forward progress transferring file data
179 | * from a remote server.
180 | */
181 | private boolean mMadeProgress = false;
182 |
183 | /**
184 | * Details from the last time we pushed a database update.
185 | */
186 | private long mLastUpdateBytes = 0;
187 | private long mLastUpdateTime = 0;
188 |
189 | private int mNetworkType = ConnectManager.TYPE_NONE;
190 |
191 | /** Historical bytes/second speed of this download. */
192 | private long mSpeed;
193 | /** Time when current sample started. */
194 | private long mSpeedSampleStart;
195 | /** Bytes transferred since current sample started. */
196 | private long mSpeedSampleBytes;
197 |
198 | public DownloadThread(Context context, DownloadNotifier notifier,
199 | DownloadInfo info) {
200 | mContext = context;
201 | mNotifier = notifier;
202 |
203 | mId = info.mId;
204 | mInfo = info;
205 | mInfoDelta = new DownloadInfoDelta(info);
206 | }
207 |
208 | @Override
209 | public void run() {
210 | Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
211 |
212 | // Skip when download already marked as finished; this download was
213 | // probably started again while racing with UpdateThread.
214 | if (DownloadInfo.queryDownloadStatus(mContext.getContentResolver(), mId)
215 | == STATUS_SUCCESS) {
216 | logDebug("Already finished; skipping");
217 | return;
218 | }
219 |
220 | PowerManager.WakeLock wakeLock = null;
221 | final PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
222 |
223 | try {
224 | wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, Constants.TAG);
225 | wakeLock.acquire();
226 |
227 | // while performing download, register for rules updates
228 |
229 | logDebug("Starting");
230 |
231 | // Remember which network this download started on; used to
232 | // determine if errors were due to network changes.
233 | final NetworkInfo info = ConnectManager.getActiveNetworkInfo(mContext, mInfo.mUid);
234 | if (info != null) {
235 | mNetworkType = info.getType();
236 | }
237 |
238 | executeDownload();
239 |
240 | mInfoDelta.mStatus = STATUS_SUCCESS;
241 |
242 | // If we just finished a chunked file, record total size
243 | if (mInfoDelta.mTotalBytes == -1) {
244 | mInfoDelta.mTotalBytes = mInfoDelta.mCurrentBytes;
245 | }
246 |
247 | } catch (StopRequestException e) {
248 | mInfoDelta.mStatus = e.getFinalStatus();
249 | mInfoDelta.mErrorMsg = e.getMessage();
250 |
251 | logWarning("Stop requested with status "
252 | + Downloads.Impl.statusToString(mInfoDelta.mStatus) + ": "
253 | + mInfoDelta.mErrorMsg);
254 |
255 | // Nobody below our level should request retries, since we handle
256 | // failure counts at this level.
257 | if (mInfoDelta.mStatus == STATUS_WAITING_TO_RETRY) {
258 | throw new IllegalStateException("Execution should always throw final error codes");
259 | }
260 |
261 | // Some errors should be retryable, unless we fail too many times.
262 | if (isStatusRetryable(mInfoDelta.mStatus)) {
263 | if (mMadeProgress) {
264 | mInfoDelta.mNumFailed = 1;
265 | } else {
266 | mInfoDelta.mNumFailed += 1;
267 | }
268 |
269 | if (mInfoDelta.mNumFailed < Constants.MAX_RETRIES) {
270 | final NetworkInfo info = ConnectManager.getActiveNetworkInfo(mContext, mInfo.mUid);
271 | if (info != null && info.getType() == mNetworkType && info.isConnected()) {
272 | // Underlying network is still intact, use normal backoff
273 | mInfoDelta.mStatus = STATUS_WAITING_TO_RETRY;
274 | } else {
275 | // Network changed, retry on any next available
276 | mInfoDelta.mStatus = STATUS_WAITING_FOR_NETWORK;
277 | }
278 |
279 | if ((mInfoDelta.mETag == null && mMadeProgress)) {
280 | // However, if we wrote data and have no ETag to verify
281 | // contents against later, we can't actually resume.
282 | mInfoDelta.mStatus = STATUS_CANNOT_RESUME;
283 | }
284 | }
285 | }
286 |
287 | } catch (Throwable t) {
288 | mInfoDelta.mStatus = STATUS_UNKNOWN_ERROR;
289 | mInfoDelta.mErrorMsg = t.toString();
290 |
291 | logError("Failed: " + mInfoDelta.mErrorMsg, t);
292 |
293 | } finally {
294 | logDebug("Finished with status " + Downloads.Impl.statusToString(mInfoDelta.mStatus));
295 |
296 | mNotifier.notifyDownloadSpeed(mId, 0);
297 |
298 | finalizeDestination();
299 |
300 | mInfoDelta.writeToDatabase();
301 |
302 | if (Downloads.Impl.isStatusCompleted(mInfoDelta.mStatus)) {
303 | mInfo.sendIntentIfRequested();
304 | }
305 |
306 | if (wakeLock != null) {
307 | wakeLock.release();
308 | wakeLock = null;
309 | }
310 | }
311 | }
312 |
313 | /**
314 | * Fully execute a single download request. Setup and send the request,
315 | * handle the response, and transfer the data to the destination file.
316 | */
317 | private void executeDownload() throws StopRequestException {
318 | final boolean resuming = mInfoDelta.mCurrentBytes != 0;
319 |
320 | logDebug("resuming; mCurrentBytes is " + mInfoDelta.mCurrentBytes);
321 |
322 | URL url;
323 | try {
324 | // TODO: migrate URL sanity checking into client side of API
325 | url = new URL(mInfoDelta.mUri);
326 | } catch (MalformedURLException e) {
327 | throw new StopRequestException(STATUS_BAD_REQUEST, e);
328 | }
329 |
330 | int redirectionCount = 0;
331 | while (redirectionCount++ < Constants.MAX_REDIRECTS) {
332 |
333 | // Open connection and follow any redirects until we have a useful
334 | // response with body.
335 | HttpURLConnection conn = null;
336 | try {
337 | checkConnectivity();
338 | conn = (HttpURLConnection) url.openConnection();
339 | conn.setInstanceFollowRedirects(false);
340 | conn.setConnectTimeout(DEFAULT_TIMEOUT);
341 | conn.setReadTimeout(DEFAULT_TIMEOUT);
342 |
343 | addRequestHeaders(conn, resuming);
344 |
345 | final int responseCode = conn.getResponseCode();
346 | switch (responseCode) {
347 | case HTTP_OK:
348 | if (resuming) {
349 | throw new StopRequestException(
350 | STATUS_CANNOT_RESUME, "Expected partial, but received OK");
351 | }
352 | parseOkHeaders(conn);
353 | transferData(conn);
354 | return;
355 |
356 | case HTTP_PARTIAL:
357 | if (!resuming) {
358 | throw new StopRequestException(
359 | STATUS_CANNOT_RESUME, "Expected OK, but received partial");
360 | }
361 |
362 | logDebug("resuming; received partial ");
363 |
364 | transferData(conn);
365 | return;
366 |
367 | case HTTP_MOVED_PERM:
368 | case HTTP_MOVED_TEMP:
369 | case HTTP_SEE_OTHER:
370 | case HTTP_TEMP_REDIRECT:
371 | final String location = conn.getHeaderField("Location");
372 | url = new URL(url, location);
373 | if (responseCode == HTTP_MOVED_PERM) {
374 | // Push updated URL back to database
375 | mInfoDelta.mUri = url.toString();
376 | }
377 | continue;
378 |
379 | case HTTP_PRECON_FAILED:
380 | throw new StopRequestException(
381 | STATUS_CANNOT_RESUME, "Precondition failed");
382 |
383 | case HTTP_REQUESTED_RANGE_NOT_SATISFIABLE:
384 | throw new StopRequestException(
385 | STATUS_CANNOT_RESUME, "Requested range not satisfiable");
386 |
387 | case HTTP_UNAVAILABLE:
388 | parseUnavailableHeaders(conn);
389 | throw new StopRequestException(
390 | HTTP_UNAVAILABLE, conn.getResponseMessage());
391 |
392 | case HTTP_INTERNAL_ERROR:
393 | throw new StopRequestException(
394 | HTTP_INTERNAL_ERROR, conn.getResponseMessage());
395 |
396 | default:
397 | StopRequestException.throwUnhandledHttpError(
398 | responseCode, conn.getResponseMessage());
399 | }
400 |
401 | } catch (IOException e) {
402 | if (e instanceof ProtocolException
403 | && e.getMessage().startsWith("Unexpected status line")) {
404 | throw new StopRequestException(STATUS_UNHANDLED_HTTP_CODE, e);
405 | } else {
406 | // Trouble with low-level sockets
407 | throw new StopRequestException(STATUS_HTTP_DATA_ERROR, e);
408 | }
409 |
410 | } finally {
411 | if (conn != null) conn.disconnect();
412 | }
413 | }
414 |
415 | throw new StopRequestException(STATUS_TOO_MANY_REDIRECTS, "Too many redirects");
416 | }
417 |
418 | /**
419 | * Transfer data from the given connection to the destination file.
420 | */
421 | private void transferData(HttpURLConnection conn) throws StopRequestException {
422 |
423 | // To detect when we're really finished, we either need a length, closed
424 | // connection, or chunked encoding.
425 | final boolean hasLength = mInfoDelta.mTotalBytes != -1;
426 | final boolean isConnectionClose = "close".equalsIgnoreCase(
427 | conn.getHeaderField("Connection"));
428 | final boolean isEncodingChunked = "chunked".equalsIgnoreCase(
429 | conn.getHeaderField("Transfer-Encoding"));
430 |
431 | final boolean finishKnown = hasLength || isConnectionClose || isEncodingChunked;
432 | if (!finishKnown) {
433 | throw new StopRequestException(
434 | STATUS_CANNOT_RESUME, "can't know size of download, giving up");
435 | }
436 |
437 | ParcelFileDescriptor outPfd = null;
438 | FileDescriptor outFd = null;
439 | InputStream in = null;
440 | OutputStream out = null;
441 | try {
442 | try {
443 | in = conn.getInputStream();
444 | } catch (IOException e) {
445 | throw new StopRequestException(STATUS_HTTP_DATA_ERROR, e);
446 | }
447 |
448 | try {
449 | Uri uri = mInfo.getAllDownloadsUri();
450 |
451 | logDebug("openFileDescriptor " + uri.toString());
452 |
453 | outPfd = mContext.getContentResolver()
454 | .openFileDescriptor(uri, "rw+");
455 | outFd = outPfd.getFileDescriptor();
456 | out = new ParcelFileDescriptor.AutoCloseOutputStream(outPfd);
457 | } catch (Exception e) {
458 | throw new StopRequestException(STATUS_FILE_ERROR, e);
459 | }
460 |
461 | // Start streaming data, periodically watch for pause/cancel
462 | // commands and checking disk space as needed.
463 | transferData(in, out, outFd);
464 |
465 | } finally {
466 |
467 | IoUtils.closeQuietly(in);
468 |
469 | try {
470 | if (out != null) out.flush();
471 | if (outFd != null) outFd.sync();
472 | } catch (IOException e) {
473 | } finally {
474 | IoUtils.closeQuietly(out);
475 | }
476 | }
477 | }
478 |
479 | /**
480 | * Transfer as much data as possible from the HTTP response to the
481 | * destination file.
482 | */
483 | private void transferData(InputStream in, OutputStream out, FileDescriptor outFd)
484 | throws StopRequestException {
485 | final byte buffer[] = new byte[Constants.BUFFER_SIZE];
486 | while (true) {
487 | checkPausedOrCanceled();
488 |
489 | int len = -1;
490 | try {
491 | len = in.read(buffer);
492 | } catch (IOException e) {
493 | throw new StopRequestException(
494 | STATUS_HTTP_DATA_ERROR, "Failed reading response: " + e, e);
495 | }
496 |
497 | if (len == -1) {
498 | break;
499 | }
500 |
501 | try {
502 | // When streaming, ensure space before each write
503 | if (mInfoDelta.mTotalBytes == -1) {
504 |
505 | if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
506 | final long curSize = Os.fstat(outFd).st_size;
507 | final long newBytes = (mInfoDelta.mCurrentBytes + len) - curSize;
508 | }
509 | StorageUtils.ensureAvailableSpace(mContext, outFd/*,newBytes*/);
510 | }
511 |
512 | out.write(buffer, 0, len);
513 |
514 | mMadeProgress = true;
515 | mInfoDelta.mCurrentBytes += len;
516 |
517 | updateProgress(outFd);
518 |
519 | } catch (Exception e) {
520 | throw new StopRequestException(STATUS_FILE_ERROR, e);
521 | }
522 | }
523 |
524 | // Finished without error; verify length if known
525 | if (mInfoDelta.mTotalBytes != -1 && mInfoDelta.mCurrentBytes != mInfoDelta.mTotalBytes) {
526 | throw new StopRequestException(STATUS_HTTP_DATA_ERROR, "Content length mismatch");
527 | }
528 | }
529 |
530 | /**
531 | * Called just before the thread finishes, regardless of status, to take any
532 | * necessary action on the downloaded file.
533 | */
534 | private void finalizeDestination() {
535 | if (Downloads.Impl.isStatusError(mInfoDelta.mStatus)) {
536 | // When error, free up any disk space
537 | try {
538 | final ParcelFileDescriptor target = mContext.getContentResolver()
539 | .openFileDescriptor(mInfo.getAllDownloadsUri(), "rw");
540 | try {
541 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
542 | Os.ftruncate(target.getFileDescriptor(), 0);
543 | }
544 | } catch (Exception ignored) {
545 | } finally {
546 | IoUtils.closeQuietly(target);
547 | }
548 | } catch (FileNotFoundException ignored) {
549 | }
550 |
551 | // Delete if local file
552 | if (mInfoDelta.mFileName != null) {
553 | new File(mInfoDelta.mFileName).delete();
554 | mInfoDelta.mFileName = null;
555 | }
556 |
557 | } else if (Downloads.Impl.isStatusSuccess(mInfoDelta.mStatus)) {
558 | // When success, open access if local file
559 | if (mInfoDelta.mFileName != null) {
560 | //chmod 644
561 | if (mInfo.mDestination != Downloads.Impl.DESTINATION_FILE_URI) {
562 | try {
563 | // Move into final resting place, if needed
564 | final File before = new File(mInfoDelta.mFileName);
565 | final File beforeDir = Helpers.getRunningDestinationDirectory(
566 | mContext, mInfo.mDestination);
567 | final File afterDir = Helpers.getSuccessDestinationDirectory(
568 | mContext, mInfo.mDestination);
569 | if (!beforeDir.equals(afterDir)
570 | && before.getParentFile().equals(beforeDir)) {
571 | final File after = new File(afterDir, before.getName());
572 | if (before.renameTo(after)) {
573 | mInfoDelta.mFileName = after.getAbsolutePath();
574 | }
575 | }
576 | } catch (IOException ignored) {
577 | }
578 | }
579 | }
580 | }
581 | }
582 |
583 | /**
584 | * Check if current connectivity is valid for this request.
585 | */
586 | private void checkConnectivity() throws StopRequestException {
587 | // checking connectivity will apply current policy
588 | mPolicyDirty = false;
589 |
590 | final NetworkState networkUsable = mInfo.checkCanUseNetwork(mInfoDelta.mTotalBytes);
591 | if (networkUsable != NetworkState.OK) {
592 | int status = STATUS_WAITING_FOR_NETWORK;
593 | if (networkUsable == NetworkState.UNUSABLE_DUE_TO_SIZE) {
594 | status = Downloads.Impl.STATUS_QUEUED_FOR_WIFI;
595 | mInfo.notifyPauseDueToSize(true);
596 | } else if (networkUsable == NetworkState.RECOMMENDED_UNUSABLE_DUE_TO_SIZE) {
597 | status = Downloads.Impl.STATUS_QUEUED_FOR_WIFI;
598 | mInfo.notifyPauseDueToSize(false);
599 | }
600 | throw new StopRequestException(status, networkUsable.name());
601 | }
602 | }
603 |
604 | /**
605 | * Check if the download has been paused or canceled, stopping the request
606 | * appropriately if it has been.
607 | */
608 | private void checkPausedOrCanceled() throws StopRequestException {
609 | synchronized (mInfo) {
610 | if (mInfo.mControl == Downloads.Impl.CONTROL_PAUSED) {
611 | throw new StopRequestException(
612 | Downloads.Impl.STATUS_PAUSED_BY_APP, "download paused by owner");
613 | }
614 | if (mInfo.mStatus == STATUS_CANCELED || mInfo.mDeleted) {
615 | throw new StopRequestException(STATUS_CANCELED, "download canceled");
616 | }
617 | }
618 |
619 | // if policy has been changed, trigger connectivity check
620 | if (mPolicyDirty) {
621 | checkConnectivity();
622 | }
623 | }
624 |
625 | /**
626 | * Report download progress through the database if necessary.
627 | */
628 | private void updateProgress(FileDescriptor outFd) throws IOException, StopRequestException {
629 | final long now = SystemClock.elapsedRealtime();
630 | final long currentBytes = mInfoDelta.mCurrentBytes;
631 |
632 | final long sampleDelta = now - mSpeedSampleStart;
633 | if (sampleDelta > 500) {
634 | final long sampleSpeed = ((currentBytes - mSpeedSampleBytes) * 1000)
635 | / sampleDelta;
636 |
637 | if (mSpeed == 0) {
638 | mSpeed = sampleSpeed;
639 | } else {
640 | mSpeed = ((mSpeed * 3) + sampleSpeed) / 4;
641 | }
642 |
643 | // Only notify once we have a full sample window
644 | if (mSpeedSampleStart != 0) {
645 | mNotifier.notifyDownloadSpeed(mId, mSpeed);
646 | }
647 |
648 | mSpeedSampleStart = now;
649 | mSpeedSampleBytes = currentBytes;
650 | }
651 |
652 | final long bytesDelta = currentBytes - mLastUpdateBytes;
653 | final long timeDelta = now - mLastUpdateTime;
654 | if (bytesDelta > Constants.MIN_PROGRESS_STEP && timeDelta > Constants.MIN_PROGRESS_TIME) {
655 | // fsync() to ensure that current progress has been flushed to disk,
656 | // so we can always resume based on latest database information.
657 | outFd.sync();
658 |
659 | mInfoDelta.writeToDatabaseOrThrow();
660 |
661 | mLastUpdateBytes = currentBytes;
662 | mLastUpdateTime = now;
663 | }
664 | }
665 |
666 | /**
667 | * Process response headers from first server response. This derives its
668 | * filename, size, and ETag.
669 | */
670 | private void parseOkHeaders(HttpURLConnection conn) throws StopRequestException {
671 | if (mInfoDelta.mFileName == null) {
672 | final String contentDisposition = conn.getHeaderField("Content-Disposition");
673 | final String contentLocation = conn.getHeaderField("Content-Location");
674 |
675 | try {
676 | mInfoDelta.mFileName = Helpers.generateSaveFile(mContext, mInfoDelta.mUri,
677 | mInfo.mHint, contentDisposition, contentLocation, mInfoDelta.mMimeType,
678 | mInfo.mDestination);
679 | } catch (IOException e) {
680 | throw new StopRequestException(
681 | STATUS_FILE_ERROR, "Failed to generate filename: " + e);
682 | }
683 | }
684 |
685 | if (mInfoDelta.mMimeType == null) {
686 | mInfoDelta.mMimeType = StorageUtils.normalizeMimeType(conn.getContentType());
687 | }
688 |
689 | final String transferEncoding = conn.getHeaderField("Transfer-Encoding");
690 | if (transferEncoding == null) {
691 | mInfoDelta.mTotalBytes = getHeaderFieldLong(conn, "Content-Length", -1);
692 | } else {
693 | mInfoDelta.mTotalBytes = -1;
694 | }
695 |
696 | mInfoDelta.mETag = conn.getHeaderField("ETag");
697 |
698 | mInfoDelta.writeToDatabaseOrThrow();
699 |
700 | // Check connectivity again now that we know the total size
701 | checkConnectivity();
702 | }
703 |
704 | private void parseUnavailableHeaders(HttpURLConnection conn) {
705 | long retryAfter = conn.getHeaderFieldInt("Retry-After", -1);
706 | if (retryAfter < 0) {
707 | retryAfter = 0;
708 | } else {
709 | if (retryAfter < Constants.MIN_RETRY_AFTER) {
710 | retryAfter = Constants.MIN_RETRY_AFTER;
711 | } else if (retryAfter > Constants.MAX_RETRY_AFTER) {
712 | retryAfter = Constants.MAX_RETRY_AFTER;
713 | }
714 | retryAfter += Helpers.sRandom.nextInt(Constants.MIN_RETRY_AFTER + 1);
715 | }
716 |
717 | mInfoDelta.mRetryAfter = (int) (retryAfter * SECOND_IN_MILLIS);
718 | }
719 |
720 | /**
721 | * Add custom headers for this download to the HTTP request.
722 | */
723 | private void addRequestHeaders(HttpURLConnection conn, boolean resuming) {
724 | for (Pair Type: TEXT Owner can Init/Read Type: TEXT Owner can Init/Read/Write Type: BOOLEAN Owner can Init Type: TEXT Owner can Init Type: TEXT Owner can Read Type: TEXT Owner can Init/Read Type: INTEGER Owner can Init Type: INTEGER Owner can Init/Read/Write Type: INTEGER Owner can Read Type: INTEGER Owner can Read Type: BIGINT Owner can Read Type: TEXT Owner can Init/Read Type: TEXT Owner can Init/Read Type: TEXT Owner can Init Type: TEXT Owner can Init Type: TEXT Owner can Init Type: TEXT Owner can Init Type: INTEGER Owner can Read Type: INTEGER Owner can Read Type: INTEGER Owner can Init Type: TEXT Owner can Init/Read/Write Type: TEXT Owner can Init/Read/Write Type: BOOLEAN Owner can Init/Read Type: INTEGER Owner can Init/Read Type: BOOLEAN Owner can Init/Read Type: BOOLEAN Owner can Init/Read Type: INTEGER Owner can Init/Read Type: BOOLEAN Type: BOOLEAN Owner can Read Type: TEXT Owner can Read Type: TEXT Type: TEXT Type: INT
440 | * 1xx: informational
441 | * 2xx: success
442 | * 3xx: redirects (not used by the download manager)
443 | * 4xx: client errors
444 | * 5xx: server errors
445 | */
446 |
447 | /**
448 | * Returns whether the status is informational (i.e. 1xx).
449 | */
450 | public static boolean isStatusInformational(int status) {
451 | return (status >= 100 && status < 200);
452 | }
453 |
454 | /**
455 | * Returns whether the status is a success (i.e. 2xx).
456 | */
457 | public static boolean isStatusSuccess(int status) {
458 | return (status >= 200 && status < 300);
459 | }
460 |
461 | /**
462 | * Returns whether the status is an error (i.e. 4xx or 5xx).
463 | */
464 | public static boolean isStatusError(int status) {
465 | return (status >= 400 && status < 600);
466 | }
467 |
468 | /**
469 | * Returns whether the status is a client error (i.e. 4xx).
470 | */
471 | public static boolean isStatusClientError(int status) {
472 | return (status >= 400 && status < 500);
473 | }
474 |
475 | /**
476 | * Returns whether the status is a server error (i.e. 5xx).
477 | */
478 | public static boolean isStatusServerError(int status) {
479 | return (status >= 500 && status < 600);
480 | }
481 |
482 | /**
483 | * this method determines if a notification should be displayed for a
484 | * given {@link #COLUMN_VISIBILITY} value
485 | * @param visibility the value of {@link #COLUMN_VISIBILITY}.
486 | * @return true if the notification should be displayed. false otherwise.
487 | */
488 | public static boolean isNotificationToBeDisplayed(int visibility) {
489 | return visibility == DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED ||
490 | visibility == DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_ONLY_COMPLETION;
491 | }
492 |
493 | /**
494 | * Returns whether the download has completed (either with success or
495 | * error).
496 | */
497 | public static boolean isStatusCompleted(int status) {
498 | return (status >= 200 && status < 300) || (status >= 400 && status < 600);
499 | }
500 |
501 | /**
502 | * This download hasn't stated yet
503 | */
504 | public static final int STATUS_PENDING = 190;
505 |
506 | /**
507 | * This download has started
508 | */
509 | public static final int STATUS_RUNNING = 192;
510 |
511 | /**
512 | * This download has been paused by the owning app.
513 | */
514 | public static final int STATUS_PAUSED_BY_APP = 193;
515 |
516 | /**
517 | * This download encountered some network error and is waiting before retrying the request.
518 | */
519 | public static final int STATUS_WAITING_TO_RETRY = 194;
520 |
521 | /**
522 | * This download is waiting for network connectivity to proceed.
523 | */
524 | public static final int STATUS_WAITING_FOR_NETWORK = 195;
525 |
526 | /**
527 | * This download exceeded a size limit for mobile networks and is waiting for a Wi-Fi
528 | * connection to proceed.
529 | */
530 | public static final int STATUS_QUEUED_FOR_WIFI = 196;
531 |
532 | /**
533 | * This download couldn't be completed due to insufficient storage
534 | * space. Typically, this is because the SD card is full.
535 | */
536 | public static final int STATUS_INSUFFICIENT_SPACE_ERROR = 198;
537 |
538 | /**
539 | * This download couldn't be completed because no external storage
540 | * device was found. Typically, this is because the SD card is not
541 | * mounted.
542 | */
543 | public static final int STATUS_DEVICE_NOT_FOUND_ERROR = 199;
544 |
545 | /**
546 | * This download has successfully completed.
547 | * Warning: there might be other status values that indicate success
548 | * in the future.
549 | * Use isSucccess() to capture the entire category.
550 | */
551 | public static final int STATUS_SUCCESS = 200;
552 |
553 | /**
554 | * This request couldn't be parsed. This is also used when processing
555 | * requests with unknown/unsupported URI schemes.
556 | */
557 | public static final int STATUS_BAD_REQUEST = 400;
558 |
559 | /**
560 | * This download can't be performed because the content type cannot be
561 | * handled.
562 | */
563 | public static final int STATUS_NOT_ACCEPTABLE = 406;
564 |
565 | /**
566 | * This download cannot be performed because the length cannot be
567 | * determined accurately. This is the code for the HTTP error "Length
568 | * Required", which is typically used when making requests that require
569 | * a content length but don't have one, and it is also used in the
570 | * client when a response is received whose length cannot be determined
571 | * accurately (therefore making it impossible to know when a download
572 | * completes).
573 | */
574 | public static final int STATUS_LENGTH_REQUIRED = 411;
575 |
576 | /**
577 | * This download was interrupted and cannot be resumed.
578 | * This is the code for the HTTP error "Precondition Failed", and it is
579 | * also used in situations where the client doesn't have an ETag at all.
580 | */
581 | public static final int STATUS_PRECONDITION_FAILED = 412;
582 |
583 | /**
584 | * The lowest-valued error status that is not an actual HTTP status code.
585 | */
586 | public static final int MIN_ARTIFICIAL_ERROR_STATUS = 488;
587 |
588 | /**
589 | * The requested destination file already exists.
590 | */
591 | public static final int STATUS_FILE_ALREADY_EXISTS_ERROR = 488;
592 |
593 | /**
594 | * Some possibly transient error occurred, but we can't resume the download.
595 | */
596 | public static final int STATUS_CANNOT_RESUME = 489;
597 |
598 | /**
599 | * This download was canceled
600 | */
601 | public static final int STATUS_CANCELED = 490;
602 |
603 | /**
604 | * This download has completed with an error.
605 | * Warning: there will be other status values that indicate errors in
606 | * the future. Use isStatusError() to capture the entire category.
607 | */
608 | public static final int STATUS_UNKNOWN_ERROR = 491;
609 |
610 | /**
611 | * This download couldn't be completed because of a storage issue.
612 | * Typically, that's because the filesystem is missing or full.
613 | * Use the more specific {@link #STATUS_INSUFFICIENT_SPACE_ERROR}
614 | * and {@link #STATUS_DEVICE_NOT_FOUND_ERROR} when appropriate.
615 | */
616 | public static final int STATUS_FILE_ERROR = 492;
617 |
618 | /**
619 | * This download couldn't be completed because of an HTTP
620 | * redirect response that the download manager couldn't
621 | * handle.
622 | */
623 | public static final int STATUS_UNHANDLED_REDIRECT = 493;
624 |
625 | /**
626 | * This download couldn't be completed because of an
627 | * unspecified unhandled HTTP code.
628 | */
629 | public static final int STATUS_UNHANDLED_HTTP_CODE = 494;
630 |
631 | /**
632 | * This download couldn't be completed because of an
633 | * error receiving or processing data at the HTTP level.
634 | */
635 | public static final int STATUS_HTTP_DATA_ERROR = 495;
636 |
637 | /**
638 | * This download couldn't be completed because of an
639 | * HttpException while setting up the request.
640 | */
641 | public static final int STATUS_HTTP_EXCEPTION = 496;
642 |
643 | /**
644 | * This download couldn't be completed because there were
645 | * too many redirects.
646 | */
647 | public static final int STATUS_TOO_MANY_REDIRECTS = 497;
648 |
649 | /**
650 | * This download has failed because requesting application has been
651 | * blocked by NetworkPolicyManager.
652 | *
653 | * @hide
654 | * @deprecated since behavior now uses
655 | * {@link #STATUS_WAITING_FOR_NETWORK}
656 | */
657 | @Deprecated
658 | public static final int STATUS_BLOCKED = 498;
659 |
660 | /** {@hide} */
661 | public static String statusToString(int status) {
662 | switch (status) {
663 | case STATUS_PENDING: return "PENDING";
664 | case STATUS_RUNNING: return "RUNNING";
665 | case STATUS_PAUSED_BY_APP: return "PAUSED_BY_APP";
666 | case STATUS_WAITING_TO_RETRY: return "WAITING_TO_RETRY";
667 | case STATUS_WAITING_FOR_NETWORK: return "WAITING_FOR_NETWORK";
668 | case STATUS_QUEUED_FOR_WIFI: return "QUEUED_FOR_WIFI";
669 | case STATUS_INSUFFICIENT_SPACE_ERROR: return "INSUFFICIENT_SPACE_ERROR";
670 | case STATUS_DEVICE_NOT_FOUND_ERROR: return "DEVICE_NOT_FOUND_ERROR";
671 | case STATUS_SUCCESS: return "SUCCESS";
672 | case STATUS_BAD_REQUEST: return "BAD_REQUEST";
673 | case STATUS_NOT_ACCEPTABLE: return "NOT_ACCEPTABLE";
674 | case STATUS_LENGTH_REQUIRED: return "LENGTH_REQUIRED";
675 | case STATUS_PRECONDITION_FAILED: return "PRECONDITION_FAILED";
676 | case STATUS_FILE_ALREADY_EXISTS_ERROR: return "FILE_ALREADY_EXISTS_ERROR";
677 | case STATUS_CANNOT_RESUME: return "CANNOT_RESUME";
678 | case STATUS_CANCELED: return "CANCELED";
679 | case STATUS_UNKNOWN_ERROR: return "UNKNOWN_ERROR";
680 | case STATUS_FILE_ERROR: return "FILE_ERROR";
681 | case STATUS_UNHANDLED_REDIRECT: return "UNHANDLED_REDIRECT";
682 | case STATUS_UNHANDLED_HTTP_CODE: return "UNHANDLED_HTTP_CODE";
683 | case STATUS_HTTP_DATA_ERROR: return "HTTP_DATA_ERROR";
684 | case STATUS_HTTP_EXCEPTION: return "HTTP_EXCEPTION";
685 | case STATUS_TOO_MANY_REDIRECTS: return "TOO_MANY_REDIRECTS";
686 | case STATUS_BLOCKED: return "BLOCKED";
687 | default: return Integer.toString(status);
688 | }
689 | }
690 |
691 | /**
692 | * This download is visible but only shows in the notifications
693 | * while it's in progress.
694 | */
695 | public static final int VISIBILITY_VISIBLE = DownloadManager.Request.VISIBILITY_VISIBLE;
696 |
697 | /**
698 | * This download is visible and shows in the notifications while
699 | * in progress and after completion.
700 | */
701 | public static final int VISIBILITY_VISIBLE_NOTIFY_COMPLETED =
702 | DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED;
703 |
704 | /**
705 | * This download doesn't show in the UI or in the notifications.
706 | */
707 | public static final int VISIBILITY_HIDDEN = DownloadManager.Request.VISIBILITY_HIDDEN;
708 |
709 | /**
710 | * Constants related to HTTP request headers associated with each download.
711 | */
712 | public static class RequestHeaders {
713 | public static final String HEADERS_DB_TABLE = "request_headers";
714 | public static final String COLUMN_DOWNLOAD_ID = "download_id";
715 | public static final String COLUMN_HEADER = "header";
716 | public static final String COLUMN_VALUE = "value";
717 |
718 | /**
719 | * Path segment to add to a download URI to retrieve request headers
720 | */
721 | public static final String URI_SEGMENT = "headers";
722 |
723 | /**
724 | * Prefix for ContentValues keys that contain HTTP header lines, to be passed to
725 | * DownloadProvider.insert().
726 | */
727 | public static final String INSERT_KEY_PREFIX = "http_header_";
728 | }
729 | }
730 |
731 | /**
732 | * Query where clause for general querying.
733 | */
734 | private static final String QUERY_WHERE_CLAUSE = Impl.COLUMN_NOTIFICATION_PACKAGE + "=? AND "
735 | + Impl.COLUMN_NOTIFICATION_CLASS + "=?";
736 |
737 | /**
738 | * Delete all the downloads for a package/class pair.
739 | */
740 | public static final void removeAllDownloadsByPackage(
741 | Context context, String notification_package, String notification_class) {
742 | context.getContentResolver().delete(Impl.CONTENT_URI, QUERY_WHERE_CLAUSE,
743 | new String[] { notification_package, notification_class });
744 | }
745 | }
746 |
--------------------------------------------------------------------------------
/DownloadManager/src/main/java/com/limpoxe/downloads/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.limpoxe.downloads;
18 |
19 | import android.content.Context;
20 | import android.net.Uri;
21 | import android.os.Environment;
22 | import android.os.SystemClock;
23 | import android.util.Log;
24 | import android.webkit.MimeTypeMap;
25 |
26 | import java.io.File;
27 | import java.io.IOException;
28 | import java.util.Random;
29 | import java.util.Set;
30 | import java.util.regex.Matcher;
31 | import java.util.regex.Pattern;
32 |
33 | /**
34 | * Some helper functions for the download manager
35 | */
36 | public class Helpers {
37 | public static Random sRandom = new Random(SystemClock.uptimeMillis());
38 |
39 | /** Regex used to parse content-disposition headers */
40 | private static final Pattern CONTENT_DISPOSITION_PATTERN =
41 | Pattern.compile("attachment;\\s*filename\\s*=\\s*\"([^\"]*)\"");
42 |
43 | private static final Object sUniqueLock = new Object();
44 |
45 | private Helpers() {
46 | }
47 |
48 | /*
49 | * Parse the Content-Disposition HTTP Header. The format of the header
50 | * is defined here: http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html
51 | * This header provides a filename for content that is going to be
52 | * downloaded to the file system. We only support the attachment type.
53 | */
54 | private static String parseContentDisposition(String contentDisposition) {
55 | try {
56 | Matcher m = CONTENT_DISPOSITION_PATTERN.matcher(contentDisposition);
57 | if (m.find()) {
58 | return m.group(1);
59 | }
60 | } catch (IllegalStateException ex) {
61 | // This function is defined as returning null when it can't parse the header
62 | }
63 | return null;
64 | }
65 |
66 | /**
67 | * Creates a filename (where the file should be saved) from info about a download.
68 | * This file will be touched to reserve it.
69 | */
70 | static String generateSaveFile(Context context, String url, String hint,
71 | String contentDisposition, String contentLocation, String mimeType, int destination)
72 | throws IOException {
73 |
74 | final File parent;
75 | final File[] parentTest;
76 | String name = null;
77 |
78 | if (destination == Downloads.Impl.DESTINATION_FILE_URI) {
79 | final File file = new File(Uri.parse(hint).getPath());
80 | parent = file.getParentFile().getAbsoluteFile();
81 | parentTest = new File[] { parent };
82 | name = file.getName();
83 | } else {
84 | parent = getRunningDestinationDirectory(context, destination);
85 | parentTest = new File[] {
86 | parent,
87 | getSuccessDestinationDirectory(context, destination)
88 | };
89 | name = chooseFilename(url, hint, contentDisposition, contentLocation);
90 | }
91 |
92 | // Ensure target directories are ready
93 | for (File test : parentTest) {
94 | if (!(test.isDirectory() || test.mkdirs())) {
95 | throw new IOException("Failed to create parent for " + test);
96 | }
97 | }
98 |
99 | final String prefix;
100 | final String suffix;
101 | final int dotIndex = name.lastIndexOf('.');
102 | final boolean missingExtension = dotIndex < 0;
103 | if (destination == Downloads.Impl.DESTINATION_FILE_URI) {
104 | // Destination is explicitly set - do not change the extension
105 | if (missingExtension) {
106 | prefix = name;
107 | suffix = "";
108 | } else {
109 | prefix = name.substring(0, dotIndex);
110 | suffix = name.substring(dotIndex);
111 | }
112 | } else {
113 | // Split filename between base and extension
114 | // Add an extension if filename does not have one
115 | if (missingExtension) {
116 | prefix = name;
117 | suffix = chooseExtensionFromMimeType(mimeType, true);
118 | } else {
119 | prefix = name.substring(0, dotIndex);
120 | suffix = chooseExtensionFromFilename(mimeType, destination, name, dotIndex);
121 | }
122 | }
123 |
124 | synchronized (sUniqueLock) {
125 | name = generateAvailableFilenameLocked(parentTest, prefix, suffix);
126 |
127 | // Claim this filename inside lock to prevent other threads from
128 | // clobbering us. We're not paranoid enough to use O_EXCL.
129 | final File file = new File(parent, name);
130 | file.createNewFile();
131 | return file.getAbsolutePath();
132 | }
133 | }
134 |
135 | private static String chooseFilename(String url, String hint, String contentDisposition,
136 | String contentLocation) {
137 | String filename = null;
138 |
139 | // First, try to use the hint from the application, if there's one
140 | if (filename == null && hint != null && !hint.endsWith("/")) {
141 | if (Constants.LOGVV) {
142 | Log.v(Constants.TAG, "getting filename from hint");
143 | }
144 | int index = hint.lastIndexOf('/') + 1;
145 | if (index > 0) {
146 | filename = hint.substring(index);
147 | } else {
148 | filename = hint;
149 | }
150 | }
151 |
152 | // If we couldn't do anything with the hint, move toward the content disposition
153 | if (filename == null && contentDisposition != null) {
154 | filename = parseContentDisposition(contentDisposition);
155 | if (filename != null) {
156 | if (Constants.LOGVV) {
157 | Log.v(Constants.TAG, "getting filename from content-disposition");
158 | }
159 | int index = filename.lastIndexOf('/') + 1;
160 | if (index > 0) {
161 | filename = filename.substring(index);
162 | }
163 | }
164 | }
165 |
166 | // If we still have nothing at this point, try the content location
167 | if (filename == null && contentLocation != null) {
168 | String decodedContentLocation = Uri.decode(contentLocation);
169 | if (decodedContentLocation != null
170 | && !decodedContentLocation.endsWith("/")
171 | && decodedContentLocation.indexOf('?') < 0) {
172 | if (Constants.LOGVV) {
173 | Log.v(Constants.TAG, "getting filename from content-location");
174 | }
175 | int index = decodedContentLocation.lastIndexOf('/') + 1;
176 | if (index > 0) {
177 | filename = decodedContentLocation.substring(index);
178 | } else {
179 | filename = decodedContentLocation;
180 | }
181 | }
182 | }
183 |
184 | // If all the other http-related approaches failed, use the plain uri
185 | if (filename == null) {
186 | String decodedUrl = Uri.decode(url);
187 | if (decodedUrl != null
188 | && !decodedUrl.endsWith("/") && decodedUrl.indexOf('?') < 0) {
189 | int index = decodedUrl.lastIndexOf('/') + 1;
190 | if (index > 0) {
191 | if (Constants.LOGVV) {
192 | Log.v(Constants.TAG, "getting filename from uri");
193 | }
194 | filename = decodedUrl.substring(index);
195 | }
196 | }
197 | }
198 |
199 | // Finally, if couldn't get filename from URI, get a generic filename
200 | if (filename == null) {
201 | if (Constants.LOGVV) {
202 | Log.v(Constants.TAG, "using default filename");
203 | }
204 | filename = Constants.DEFAULT_DL_FILENAME;
205 | }
206 |
207 | // The VFAT file system is assumed as target for downloads.
208 | // Replace invalid characters according to the specifications of VFAT.
209 | filename = StorageUtils.buildValidFatFilename(filename);
210 |
211 | return filename;
212 | }
213 |
214 | private static String chooseExtensionFromMimeType(String mimeType, boolean useDefaults) {
215 | String extension = null;
216 | if (mimeType != null) {
217 | extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType);
218 | if (extension != null) {
219 | if (Constants.LOGVV) {
220 | Log.v(Constants.TAG, "adding extension from type");
221 | }
222 | extension = "." + extension;
223 | } else {
224 | if (Constants.LOGVV) {
225 | Log.v(Constants.TAG, "couldn't find extension for " + mimeType);
226 | }
227 | }
228 | }
229 | if (extension == null) {
230 | if (mimeType != null && mimeType.toLowerCase().startsWith("text/")) {
231 | if (mimeType.equalsIgnoreCase("text/html")) {
232 | if (Constants.LOGVV) {
233 | Log.v(Constants.TAG, "adding default html extension");
234 | }
235 | extension = Constants.DEFAULT_DL_HTML_EXTENSION;
236 | } else if (useDefaults) {
237 | if (Constants.LOGVV) {
238 | Log.v(Constants.TAG, "adding default text extension");
239 | }
240 | extension = Constants.DEFAULT_DL_TEXT_EXTENSION;
241 | }
242 | } else if (useDefaults) {
243 | if (Constants.LOGVV) {
244 | Log.v(Constants.TAG, "adding default binary extension");
245 | }
246 | extension = Constants.DEFAULT_DL_BINARY_EXTENSION;
247 | }
248 | }
249 | return extension;
250 | }
251 |
252 | private static String chooseExtensionFromFilename(String mimeType, int destination,
253 | String filename, int lastDotIndex) {
254 | String extension = null;
255 | if (mimeType != null) {
256 | // Compare the last segment of the extension against the mime type.
257 | // If there's a mismatch, discard the entire extension.
258 | String typeFromExt = MimeTypeMap.getSingleton().getMimeTypeFromExtension(
259 | filename.substring(lastDotIndex + 1));
260 | if (typeFromExt == null || !typeFromExt.equalsIgnoreCase(mimeType)) {
261 | extension = chooseExtensionFromMimeType(mimeType, false);
262 | if (extension != null) {
263 | if (Constants.LOGVV) {
264 | Log.v(Constants.TAG, "substituting extension from type");
265 | }
266 | } else {
267 | if (Constants.LOGVV) {
268 | Log.v(Constants.TAG, "couldn't find extension for " + mimeType);
269 | }
270 | }
271 | }
272 | }
273 | if (extension == null) {
274 | if (Constants.LOGVV) {
275 | Log.v(Constants.TAG, "keeping extension");
276 | }
277 | extension = filename.substring(lastDotIndex);
278 | }
279 | return extension;
280 | }
281 |
282 | private static boolean isFilenameAvailableLocked(File[] parents, String name) {
283 | if (Constants.RECOVERY_DIRECTORY.equalsIgnoreCase(name)) return false;
284 |
285 | for (File parent : parents) {
286 | if (new File(parent, name).exists()) {
287 | return false;
288 | }
289 | }
290 |
291 | return true;
292 | }
293 |
294 | private static String generateAvailableFilenameLocked(
295 | File[] parents, String prefix, String suffix) throws IOException {
296 | String name = prefix + suffix;
297 | if (isFilenameAvailableLocked(parents, name)) {
298 | return name;
299 | }
300 |
301 | /*
302 | * This number is used to generate partially randomized filenames to avoid
303 | * collisions.
304 | * It starts at 1.
305 | * The next 9 iterations increment it by 1 at a time (up to 10).
306 | * The next 9 iterations increment it by 1 to 10 (random) at a time.
307 | * The next 9 iterations increment it by 1 to 100 (random) at a time.
308 | * ... Up to the point where it increases by 100000000 at a time.
309 | * (the maximum value that can be reached is 1000000000)
310 | * As soon as a number is reached that generates a filename that doesn't exist,
311 | * that filename is used.
312 | * If the filename coming in is [base].[ext], the generated filenames are
313 | * [base]-[sequence].[ext].
314 | */
315 | int sequence = 1;
316 | for (int magnitude = 1; magnitude < 1000000000; magnitude *= 10) {
317 | for (int iteration = 0; iteration < 9; ++iteration) {
318 | name = prefix + Constants.FILENAME_SEQUENCE_SEPARATOR + sequence + suffix;
319 | if (isFilenameAvailableLocked(parents, name)) {
320 | return name;
321 | }
322 | sequence += sRandom.nextInt(magnitude) + 1;
323 | }
324 | }
325 |
326 | throw new IOException("Failed to generate an available filename");
327 | }
328 |
329 | public static File getRunningDestinationDirectory(Context context, int destination)
330 | throws IOException {
331 | return getDestinationDirectory(context, destination, true);
332 | }
333 |
334 | public static File getSuccessDestinationDirectory(Context context, int destination)
335 | throws IOException {
336 | return getDestinationDirectory(context, destination, false);
337 | }
338 |
339 | private static File getDestinationDirectory(Context context, int destination, boolean running)
340 | throws IOException {
341 | switch (destination) {
342 | case Downloads.Impl.DESTINATION_CACHE_PARTITION:
343 | case Downloads.Impl.DESTINATION_CACHE_PARTITION_PURGEABLE:
344 | case Downloads.Impl.DESTINATION_CACHE_PARTITION_NOROAMING:
345 | if (running) {
346 | return context.getFilesDir();
347 | } else {
348 | return context.getCacheDir();
349 | }
350 | case Downloads.Impl.DESTINATION_EXTERNAL:
351 | final File target = new File(
352 | Environment.getExternalStorageDirectory(), Environment.DIRECTORY_DOWNLOADS);
353 | if (!target.isDirectory() && target.mkdirs()) {
354 | throw new IOException("unable to create external downloads directory");
355 | }
356 | return target;
357 |
358 | default:
359 | throw new IllegalStateException("unexpected destination: " + destination);
360 | }
361 | }
362 |
363 | /**
364 | * Checks whether this looks like a legitimate selection parameter
365 | */
366 | public static void validateSelection(String selection, Set
56 | *
62 | */
63 | public static void ensureAvailableSpace(Context context, FileDescriptor fd)
64 | throws IOException, StopRequestException {
65 | //TODO
66 | Log.w("StorageUtil", "todo should check space here");
67 | }
68 |
69 | /**
70 | * Return number of available bytes on the filesystem backing the given
71 | * {@link FileDescriptor}, minus any {@link #RESERVED_BYTES} buffer.
72 | */
73 | private static long getAvailableBytes(FileDescriptor fd) throws IOException {
74 | try {
75 | //TODO only Sdcard check??
76 | String sdcardDir = Environment.getExternalStorageDirectory().getPath();
77 | StatFs stat = new StatFs(sdcardDir);
78 | long bytesAvailable = 0;
79 | if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN_MR2) {
80 | bytesAvailable = (long)stat.getBlockSizeLong() * (long)stat.getAvailableBlocksLong();
81 | } else {
82 | bytesAvailable = (long)stat.getBlockSize() * (long)stat.getAvailableBlocks();
83 | }
84 | return bytesAvailable - RESERVED_BYTES;
85 | } catch (Exception e) {
86 | throw new IOException("getAvailableBytes IOException");
87 | }
88 | }
89 |
90 | public static String normalizeMimeType(String type) {
91 | if (type == null) {
92 | return null;
93 | }
94 |
95 | type = type.trim().toLowerCase(Locale.ROOT);
96 |
97 | final int semicolonIndex = type.indexOf(';');
98 | if (semicolonIndex != -1) {
99 | type = type.substring(0, semicolonIndex);
100 | }
101 | return type;
102 | }
103 |
104 | public static String buildValidExtFilename(String name) {
105 | if (TextUtils.isEmpty(name) || ".".equals(name) || "..".equals(name)) {
106 | return "(invalid)";
107 | }
108 | final StringBuilder res = new StringBuilder(name.length());
109 | for (int i = 0; i < name.length(); i++) {
110 | final char c = name.charAt(i);
111 | if (isValidExtFilenameChar(c)) {
112 | res.append(c);
113 | } else {
114 | res.append('_');
115 | }
116 | }
117 | return res.toString();
118 | }
119 |
120 | private static boolean isValidExtFilenameChar(char c) {
121 | switch (c) {
122 | case '\0':
123 | case '/':
124 | return false;
125 | default:
126 | return true;
127 | }
128 | }
129 |
130 | public static String buildValidFatFilename(String name) {
131 | if (TextUtils.isEmpty(name) || ".".equals(name) || "..".equals(name)) {
132 | return "(invalid)";
133 | }
134 | final StringBuilder res = new StringBuilder(name.length());
135 | for (int i = 0; i < name.length(); i++) {
136 | final char c = name.charAt(i);
137 | if (isValidFatFilenameChar(c)) {
138 | res.append(c);
139 | } else {
140 | res.append('_');
141 | }
142 | }
143 | return res.toString();
144 | }
145 |
146 | private static boolean isValidFatFilenameChar(char c) {
147 | if ((0x00 <= c && c <= 0x1f)) {
148 | return false;
149 | }
150 | switch (c) {
151 | case '"':
152 | case '*':
153 | case '/':
154 | case ':':
155 | case '<':
156 | case '>':
157 | case '?':
158 | case '\\':
159 | case '|':
160 | case 0x7F:
161 | return false;
162 | default:
163 | return true;
164 | }
165 | }
166 |
167 | /**
168 | * Check if given filename is valid for a FAT filesystem.
169 | */
170 | public static boolean isValidFatFilename(String name) {
171 | return (name != null) && name.equals(buildValidFatFilename(name));
172 | }
173 |
174 | public static String trimFilename(String str, int maxBytes) throws UnsupportedEncodingException {
175 | final StringBuilder res = new StringBuilder(str);
176 | trimFilename(res, maxBytes);
177 | return res.toString();
178 | }
179 |
180 | private static void trimFilename(StringBuilder res, int maxBytes) throws UnsupportedEncodingException {
181 | byte[] raw = res.toString().getBytes("UTF-8");
182 | if (raw.length > maxBytes) {
183 | maxBytes -= 3;
184 | while (raw.length > maxBytes) {
185 | res.deleteCharAt(res.length() / 2);
186 | raw = res.toString().getBytes("UTF-8");
187 | }
188 | res.insert(res.length() / 2, "...");
189 | }
190 | }
191 | }
192 |
--------------------------------------------------------------------------------
/DownloadManager/src/main/java/com/limpoxe/downloads/utils/ConnectManager.java:
--------------------------------------------------------------------------------
1 | package com.limpoxe.downloads.utils;
2 |
3 | import android.content.Context;
4 | import android.net.ConnectivityManager;
5 | import android.net.NetworkInfo;
6 | import android.util.Log;
7 |
8 | import com.limpoxe.downloads.Constants;
9 |
10 | /**
11 | * Created by cailiming on 16/12/14.
12 | */
13 |
14 | public class ConnectManager {
15 | public static final int TYPE_NONE = -1;
16 | public static final int TYPE_MOBILE = 0;
17 | public static final int TYPE_WIFI = 1;
18 | public static final int TYPE_MOBILE_MMS = 2;
19 | public static final int TYPE_MOBILE_SUPL = 3;
20 | public static final int TYPE_MOBILE_DUN = 4;
21 | public static final int TYPE_MOBILE_HIPRI = 5;
22 | public static final int TYPE_WIMAX = 6;
23 | public static final int TYPE_BLUETOOTH = 7;
24 | public static final int TYPE_DUMMY = 8;
25 | public static final int TYPE_ETHERNET = 9;
26 | public static final int TYPE_MOBILE_FOTA = 10;
27 | public static final int TYPE_MOBILE_IMS = 11;
28 | public static final int TYPE_MOBILE_CBS = 12;
29 | public static final int TYPE_WIFI_P2P = 13;
30 | public static final int TYPE_MOBILE_IA = 14;
31 | public static final int TYPE_MOBILE_EMERGENCY = 15;
32 | public static final int TYPE_PROXY = 16;
33 | public static final int TYPE_VPN = 17;
34 |
35 | public static final int MAX_RADIO_TYPE = TYPE_VPN;
36 | public static final int MAX_NETWORK_TYPE = TYPE_VPN;
37 |
38 | public static boolean isNetworkTypeValid(int networkType) {
39 | return networkType >= 0 && networkType <= MAX_NETWORK_TYPE;
40 | }
41 |
42 | public static String getNetworkTypeName(int type) {
43 | switch (type) {
44 | case TYPE_MOBILE:
45 | return "MOBILE";
46 | case TYPE_WIFI:
47 | return "WIFI";
48 | case TYPE_MOBILE_MMS:
49 | return "MOBILE_MMS";
50 | case TYPE_MOBILE_SUPL:
51 | return "MOBILE_SUPL";
52 | case TYPE_MOBILE_DUN:
53 | return "MOBILE_DUN";
54 | case TYPE_MOBILE_HIPRI:
55 | return "MOBILE_HIPRI";
56 | case TYPE_WIMAX:
57 | return "WIMAX";
58 | case TYPE_BLUETOOTH:
59 | return "BLUETOOTH";
60 | case TYPE_DUMMY:
61 | return "DUMMY";
62 | case TYPE_ETHERNET:
63 | return "ETHERNET";
64 | case TYPE_MOBILE_FOTA:
65 | return "MOBILE_FOTA";
66 | case TYPE_MOBILE_IMS:
67 | return "MOBILE_IMS";
68 | case TYPE_MOBILE_CBS:
69 | return "MOBILE_CBS";
70 | case TYPE_WIFI_P2P:
71 | return "WIFI_P2P";
72 | case TYPE_MOBILE_IA:
73 | return "MOBILE_IA";
74 | case TYPE_MOBILE_EMERGENCY:
75 | return "MOBILE_EMERGENCY";
76 | case TYPE_PROXY:
77 | return "PROXY";
78 | case TYPE_VPN:
79 | return "VPN";
80 | default:
81 | return Integer.toString(type);
82 | }
83 | }
84 |
85 | public static boolean isNetworkTypeMobile(int networkType) {
86 | switch (networkType) {
87 | case TYPE_MOBILE:
88 | case TYPE_MOBILE_MMS:
89 | case TYPE_MOBILE_SUPL:
90 | case TYPE_MOBILE_DUN:
91 | case TYPE_MOBILE_HIPRI:
92 | case TYPE_MOBILE_FOTA:
93 | case TYPE_MOBILE_IMS:
94 | case TYPE_MOBILE_CBS:
95 | case TYPE_MOBILE_IA:
96 | case TYPE_MOBILE_EMERGENCY:
97 | return true;
98 | default:
99 | return false;
100 | }
101 | }
102 |
103 | public static boolean isNetworkTypeWifi(int networkType) {
104 | switch (networkType) {
105 | case TYPE_WIFI:
106 | case TYPE_WIFI_P2P:
107 | return true;
108 | default:
109 | return false;
110 | }
111 | }
112 |
113 | public static NetworkInfo getActiveNetworkInfo(Context context, int uid) {
114 | ConnectivityManager connectivity =
115 | (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
116 | if (connectivity == null) {
117 | Log.w(Constants.TAG, "couldn't get connectivity manager");
118 | return null;
119 | }
120 |
121 | final NetworkInfo activeInfo = connectivity.getActiveNetworkInfo();
122 | if (activeInfo == null && Constants.LOGVV) {
123 | Log.v(Constants.TAG, "network is not available");
124 | }
125 | return activeInfo;
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/DownloadManager/src/main/java/com/limpoxe/downloads/utils/GuardedBy.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2012 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.limpoxe.downloads.utils;
18 |
19 | import java.lang.annotation.ElementType;
20 | import java.lang.annotation.Retention;
21 | import java.lang.annotation.RetentionPolicy;
22 | import java.lang.annotation.Target;
23 |
24 | /**
25 | * Annotation type used to mark a method or field that can only be accessed when
26 | * holding the referenced lock.
27 | */
28 | @Target({ ElementType.FIELD, ElementType.METHOD })
29 | @Retention(RetentionPolicy.CLASS)
30 | public @interface GuardedBy {
31 | String value();
32 | }
33 |
--------------------------------------------------------------------------------
/DownloadManager/src/main/java/com/limpoxe/downloads/utils/IoUtils.java:
--------------------------------------------------------------------------------
1 | package com.limpoxe.downloads.utils;
2 |
3 | public final class IoUtils {
4 | private IoUtils() {
5 | }
6 |
7 | /**
8 | * Closes 'closeable', ignoring any checked exceptions. Does nothing if 'closeable' is null.
9 | */
10 | public static void closeQuietly(AutoCloseable closeable) {
11 | if (closeable != null) {
12 | try {
13 | closeable.close();
14 | } catch (RuntimeException rethrown) {
15 | throw rethrown;
16 | } catch (Exception ignored) {
17 | }
18 | }
19 | }
20 |
21 |
22 | }
23 |
--------------------------------------------------------------------------------
/DownloadManager/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |