diff --git a/CHANGELOG b/CHANGELOG index 5358e386..15ed0be0 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,12 @@ +Version 3.0.11/ Androidx version 3.1.5 +2. Notification sound fixes. +3. RxJava fixes. +4. Content Length fixes +5. Added NetworkType.UNMETERED +6. Lots of bug fixes. + +Thanks to everyone who contributed! + Version 3.0.10/ Androidx version 3.1.4 1. Improvements/Bug fixes to getting a download's content-length 2. FetchDatabaseManager interface improvements. It is now easier to create custom fetch databases. diff --git a/README.md b/README.md index 18708f02..41689b36 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ [![Build Status](https://travis-ci.org/tonyofrancis/Fetch.svg?branch=v2)](https://travis-ci.org/tonyofrancis/Fetch) -[ ![Download](https://api.bintray.com/packages/tonyofrancis/maven/fetch2/images/download.svg?version=3.0.10) ](https://bintray.com/tonyofrancis/maven/fetch2/3.0.10/link) +[ ![Download](https://api.bintray.com/packages/tonyofrancis/maven/fetch2/images/download.svg?version=3.0.11) ](https://bintray.com/tonyofrancis/maven/fetch2/3.0.11/link) [![Android Arsenal](https://img.shields.io/badge/Android%20Arsenal-Android%20Networking-blue.svg?style=flat)](https://android-arsenal.com/details/1/5196) [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/tonyofrancis/Fetch/blob/master/LICENSE) @@ -21,7 +21,7 @@ Features * Concurrent downloading support. * Ability to pause and resume downloads. * Set the priority of a download. -* Network specific downloading support. +* Network-specific downloading support. * Ability to retry failed downloads. * Ability to group downloads. * Easy progress and status tracking. @@ -43,6 +43,12 @@ add the following storage permissions to your application's manifest. For Androi ``` +Also, as you are going to use Internet to download files. We need to add the Internet access permissions +in the Manifest. + +```xml + +``` How to use Fetch ---------------- @@ -50,11 +56,11 @@ How to use Fetch Using Fetch is easy! Just add the Gradle dependency to your application's build.gradle file. ```java -implementation "com.tonyodev.fetch2:fetch2:3.0.10" +implementation "com.tonyodev.fetch2:fetch2:3.0.11" ``` Androidx use: ```java -implementation "androidx.tonyodev.fetch2:xfetch2:3.1.4" +implementation "androidx.tonyodev.fetch2:xfetch2:3.1.5" ``` Next, get an instance of Fetch and request a download. @@ -177,27 +183,27 @@ You can query Fetch for download information in several ways. ```java //Query all downloads fetch.getDownloads(new Func>() { - @Override + @Override public void call(List downloads) { - //Access all downloads here + //Access all downloads here } }); //Get all downloads with a status fetch.getDownloadsWithStatus(Status.DOWNLOADING, new Func>() { - @Override + @Override public void call(List downloads) { - //Access downloads that are downloading + //Access downloads that are downloading } }); // You can also access grouped downloads int groupId = 52687447745; fetch.getDownloadsInGroup(groupId, new Func>() { - @Override - public void call(List downloads) { - //Access grouped downloads - } + @Override + public void call(List downloads) { + //Access grouped downloads + } }); ``` @@ -216,15 +222,15 @@ Downloaders By default Fetch uses the HttpUrlConnection client via the HttpUrlConnectionDownloader to download requests. Add the following Gradle dependency to your application's build.gradle -to use the OkHttp Downloader instead. You can create your own custom downloaders +to use the OkHttp Downloader instead. You can create your custom downloaders if necessary. See the Java docs for details. ```java -implementation "com.tonyodev.fetch2okhttp:fetch2okhttp:3.0.10" +implementation "com.tonyodev.fetch2okhttp:fetch2okhttp:3.0.11" ``` Androidx use: ```java -implementation "androidx.tonyodev.fetch2okhttp:xfetch2okhttp:3.1.4" +implementation "androidx.tonyodev.fetch2okhttp:xfetch2okhttp:3.1.5" ``` Set the OkHttp Downloader for Fetch to use. @@ -232,9 +238,9 @@ Set the OkHttp Downloader for Fetch to use. OkHttpClient okHttpClient = new OkHttpClient.Builder().build(); FetchConfiguration fetchConfiguration = new FetchConfiguration.Builder(this) - .setDownloadConcurrentLimit(10) - .setHttpDownloader(new OkHttpDownloader(okHttpClient)) - .build(); + .setDownloadConcurrentLimit(10) + .setHttpDownloader(new OkHttpDownloader(okHttpClient)) + .build(); Fetch fetch = Fetch.Impl.getInstance(fetchConfiguration); ``` @@ -246,11 +252,11 @@ If you would like to take advantage of RxJava2 features when using Fetch, add the following gradle dependency to your application's build.gradle file. ```java -implementation "com.tonyodev.fetch2rx:fetch2rx:3.0.10" +implementation "com.tonyodev.fetch2rx:fetch2rx:3.0.11" ``` Androidx use: ```java -implementation "androidx.tonyodev.fetch2rx:xfetch2rx:3.1.4" +implementation "androidx.tonyodev.fetch2rx:xfetch2rx:3.1.5" ``` RxFetch makes it super easy to enqueue download requests and query downloads using rxJava2 functional methods. @@ -280,20 +286,20 @@ FetchFileServer Introducing the FetchFileServer. The FetchFileServer is a lightweight TCP File Server that acts like an HTTP file server designed specifically to share files between Android devices. You can host file resources -with the FetchFileServer on one device and have Fetch download Files from the server -on another device. See sample app for more information. Wiki on FetchFileServer will be +with the FetchFileServer on one device and have to Fetch download Files from the server +on another device. See the sample app for more information. Wiki on FetchFileServer will be added in the coming days. Start using FetchFileServer by adding the gradle dependency to your application's build.gradle file. ```java -implementation "com.tonyodev.fetch2fileserver:fetch2fileserver:3.0.10" +implementation "com.tonyodev.fetch2fileserver:fetch2fileserver:3.0.11" ``` Androidx use: ```java -implementation "androidx.tonyodev.fetch2fileserver:xfetch2fileserver:3.1.4" +implementation "androidx.tonyodev.fetch2fileserver:xfetch2fileserver:3.1.5" ``` -Start a FetchFileServer instance and add resource files that it can server to connected clients. +Start a FetchFileServer instance and add resource files that it can serve to connected clients. ```java public class TestActivity extends AppCompatActivity { @@ -399,11 +405,11 @@ Fetch1 Migration Migrate downloads from Fetch1 to Fetch2 using the migration assistant. Add the following gradle dependency to your application's build.gradle file. ```java -implementation "com.tonyodev.fetchmigrator:fetchmigrator:3.0.10" +implementation "com.tonyodev.fetchmigrator:fetchmigrator:3.0.11" ``` Androidx use: ```java -implementation "androidx.tonyodev.fetchmigrator:xfetchmigrator:3.1.4" +implementation "androidx.tonyodev.fetchmigrator:xfetchmigrator:3.1.5" ``` Then run the Migrator. @@ -450,7 +456,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, diff --git a/fetch2/src/main/java/com/tonyodev/fetch2/DefaultFetchNotificationManager.kt b/fetch2/src/main/java/com/tonyodev/fetch2/DefaultFetchNotificationManager.kt index 469d02f2..72377859 100644 --- a/fetch2/src/main/java/com/tonyodev/fetch2/DefaultFetchNotificationManager.kt +++ b/fetch2/src/main/java/com/tonyodev/fetch2/DefaultFetchNotificationManager.kt @@ -90,6 +90,7 @@ abstract class DefaultFetchNotificationManager(context: Context) : FetchNotifica .setContentTitle(context.getString(R.string.fetch_notification_default_channel_name)) .setContentText("") .setStyle(style) + .setOnlyAlertOnce(true) .setGroup(groupId.toString()) .setGroupSummary(true) return false @@ -303,6 +304,7 @@ abstract class DefaultFetchNotificationManager(context: Context) : FetchNotifica .setTimeoutAfter(DEFAULT_NOTIFICATION_TIMEOUT_AFTER_RESET) .setOngoing(false) .setGroup(groupId.toString()) + .setOnlyAlertOnce(true) .setSmallIcon(android.R.drawable.stat_sys_download_done) .mActions.clear() return notificationBuilder diff --git a/fetch2/src/main/java/com/tonyodev/fetch2/Error.kt b/fetch2/src/main/java/com/tonyodev/fetch2/Error.kt index fb31dd61..8561ae7d 100644 --- a/fetch2/src/main/java/com/tonyodev/fetch2/Error.kt +++ b/fetch2/src/main/java/com/tonyodev/fetch2/Error.kt @@ -128,9 +128,14 @@ enum class Error constructor( FAILED_TO_RENAME_FILE(29), /** - * Indicates that an error occured when pre allocating the needed space on the storage system for the download. + * Indicates that an error occurred when pre allocating the needed space on the storage system for the download. * */ - FILE_ALLOCATION_FAILED(30); + FILE_ALLOCATION_FAILED(30), + + /** + * Indicates that connection to "http" is not allowed by the OS and "https" is required. + * */ + HTTP_CONNECTION_NOT_ALLOWED(31); companion object { @@ -166,6 +171,7 @@ enum class Error constructor( 28 -> FAILED_TO_RENAME_INCOMPLETE_DOWNLOAD_FILE 29 -> FAILED_TO_RENAME_FILE 30 -> FILE_ALLOCATION_FAILED + 31 -> HTTP_CONNECTION_NOT_ALLOWED else -> UNKNOWN } } diff --git a/fetch2/src/main/java/com/tonyodev/fetch2/FetchErrorUtils.kt b/fetch2/src/main/java/com/tonyodev/fetch2/FetchErrorUtils.kt index e170f73c..380d5173 100644 --- a/fetch2/src/main/java/com/tonyodev/fetch2/FetchErrorUtils.kt +++ b/fetch2/src/main/java/com/tonyodev/fetch2/FetchErrorUtils.kt @@ -83,6 +83,8 @@ fun getErrorFromMessage(message: String?): Error { Error.FAILED_TO_RENAME_FILE } else if(message.contains(FILE_ALLOCATION_ERROR, true)) { Error.FILE_ALLOCATION_FAILED + } else if(message.contains(CLEAR_TEXT_NETWORK_VIOLATION, true)) { + Error.HTTP_CONNECTION_NOT_ALLOWED } else { Error.UNKNOWN } diff --git a/fetch2/src/main/java/com/tonyodev/fetch2/HttpUrlConnectionDownloader.kt b/fetch2/src/main/java/com/tonyodev/fetch2/HttpUrlConnectionDownloader.kt index cc9e23fc..58f1b007 100644 --- a/fetch2/src/main/java/com/tonyodev/fetch2/HttpUrlConnectionDownloader.kt +++ b/fetch2/src/main/java/com/tonyodev/fetch2/HttpUrlConnectionDownloader.kt @@ -46,6 +46,17 @@ open class HttpUrlConnectionDownloader @JvmOverloads constructor( return null } + private fun getCleanedHeaders(responseHeaders: MutableMap?>): MutableMap> { + val headers = mutableMapOf>() + for (responseHeader in responseHeaders) { + val key = responseHeader.key + if (key != null) { + headers[key] = responseHeader.value ?: emptyList() + } + } + return headers + } + override fun execute(request: Downloader.ServerRequest, interruptMonitor: InterruptMonitor): Downloader.Response? { CookieHandler.setDefault(cookieManager) var httpUrl = URL(request.url) @@ -56,12 +67,12 @@ open class HttpUrlConnectionDownloader @JvmOverloads constructor( client.addRequestProperty("Referer", referer) } client.connect() - var responseHeaders = getResponseHeaders(client.headerFields) + var responseHeaders = getCleanedHeaders(client.headerFields) var code = client.responseCode if ((code == HttpURLConnection.HTTP_MOVED_TEMP - || code == HttpURLConnection.HTTP_MOVED_PERM - || code == HttpURLConnection.HTTP_SEE_OTHER) && responseHeaders.containsKey("location")) { - httpUrl = URL(responseHeaders["location"]?.firstOrNull() ?: "") + || code == HttpURLConnection.HTTP_MOVED_PERM + || code == HttpURLConnection.HTTP_SEE_OTHER) && getHeaderValue(responseHeaders, "Location") != null) { + httpUrl = URL(getHeaderValue(responseHeaders, "Location") ?: "") client = httpUrl.openConnection() as HttpURLConnection onPreClientExecute(client, request) if (client.getRequestProperty("Referer") == null) { @@ -69,7 +80,7 @@ open class HttpUrlConnectionDownloader @JvmOverloads constructor( client.addRequestProperty("Referer", referer) } client.connect() - responseHeaders = getResponseHeaders(client.headerFields) + responseHeaders = getCleanedHeaders(client.headerFields) code = client.responseCode } var success = false @@ -86,8 +97,7 @@ open class HttpUrlConnectionDownloader @JvmOverloads constructor( errorResponseString = copyStreamToString(client.errorStream, false) } - val acceptsRanges = code == HttpURLConnection.HTTP_PARTIAL || - responseHeaders["accept-ranges"]?.firstOrNull() == "bytes" + val acceptsRanges = acceptRanges(code, responseHeaders) onServerResponse(request, Downloader.Response( code = code, @@ -96,7 +106,7 @@ open class HttpUrlConnectionDownloader @JvmOverloads constructor( byteStream = null, request = request, hash = hash, - responseHeaders = responseHeaders, + responseHeaders = client.headerFields, acceptsRanges = acceptsRanges, errorResponse = errorResponseString)) @@ -128,7 +138,7 @@ open class HttpUrlConnectionDownloader @JvmOverloads constructor( } override fun getContentHash(responseHeaders: MutableMap>): String { - return responseHeaders["content-md5"]?.firstOrNull() ?: "" + return getHeaderValue(responseHeaders, "Content-MD5") ?: "" } override fun close() { @@ -146,20 +156,6 @@ open class HttpUrlConnectionDownloader @JvmOverloads constructor( } } - private fun getResponseHeaders(responseHeaders: MutableMap>): MutableMap> { - val headers = mutableMapOf>() - val iterator = responseHeaders.iterator() - var entry: Map.Entry> - while (iterator.hasNext()) { - entry = iterator.next() - @Suppress("SENSELESS_COMPARISON") - if (entry.key != null) { - headers[entry.key.toLowerCase()] = entry.value - } - } - return headers - } - override fun getFileSlicingCount(request: Downloader.ServerRequest, contentLength: Long): Int? { return null } @@ -193,6 +189,9 @@ open class HttpUrlConnectionDownloader @JvmOverloads constructor( } override fun getRequestSupportedFileDownloaderTypes(request: Downloader.ServerRequest): Set { + if (fileDownloaderType == Downloader.FileDownloaderType.SEQUENTIAL) { + return mutableSetOf(fileDownloaderType) + } return try { getRequestSupportedFileDownloaderTypes(request, this) } catch (e: Exception) { @@ -212,4 +211,4 @@ open class HttpUrlConnectionDownloader @JvmOverloads constructor( var followsRedirect = true } -} \ No newline at end of file +} diff --git a/fetch2/src/main/java/com/tonyodev/fetch2/NetworkType.kt b/fetch2/src/main/java/com/tonyodev/fetch2/NetworkType.kt index df20567e..399305da 100644 --- a/fetch2/src/main/java/com/tonyodev/fetch2/NetworkType.kt +++ b/fetch2/src/main/java/com/tonyodev/fetch2/NetworkType.kt @@ -15,7 +15,10 @@ enum class NetworkType(val value: Int) { ALL(0), /** Indicates that a download can be downloaded only on wifi networks.*/ - WIFI_ONLY(1); + WIFI_ONLY(1), + + /** Indicates that a download can be downloaded only on an unmetered connection.*/ + UNMETERED(2); companion object { @@ -25,6 +28,7 @@ enum class NetworkType(val value: Int) { -1 -> GLOBAL_OFF 0 -> ALL 1 -> WIFI_ONLY + 2 -> UNMETERED else -> ALL } } diff --git a/fetch2/src/main/java/com/tonyodev/fetch2/downloader/DownloadManagerImpl.kt b/fetch2/src/main/java/com/tonyodev/fetch2/downloader/DownloadManagerImpl.kt index a1ae0c29..b0ac210b 100644 --- a/fetch2/src/main/java/com/tonyodev/fetch2/downloader/DownloadManagerImpl.kt +++ b/fetch2/src/main/java/com/tonyodev/fetch2/downloader/DownloadManagerImpl.kt @@ -109,6 +109,7 @@ class DownloadManagerImpl(private val httpDownloader: Downloader<*, *>, } finally { removeDownloadMappings(download) val intent = Intent(ACTION_QUEUE_BACKOFF_RESET) + intent.setPackage(context.packageName) intent.putExtra(EXTRA_NAMESPACE, namespace) context.sendBroadcast(intent) } @@ -257,7 +258,12 @@ class DownloadManagerImpl(private val httpDownloader: Downloader<*, *>, } private fun getFileDownloader(download: Download, downloader: Downloader<*, *>): FileDownloader { - val request = getRequestForDownload(download) + val originalRequest = getRequestForDownload(download) + val request = if (downloader.getHeadRequestMethodSupported(originalRequest)) { + getRequestForDownload(download, HEAD_REQUEST_METHOD) + } else { + originalRequest + } val supportedDownloadTypes = downloader.getRequestSupportedFileDownloaderTypes(request) return if (downloader.getRequestFileDownloaderType(request, supportedDownloadTypes) == Downloader.FileDownloaderType.SEQUENTIAL) { SequentialFileDownloaderImpl( diff --git a/fetch2/src/main/java/com/tonyodev/fetch2/downloader/ParallelFileDownloaderImpl.kt b/fetch2/src/main/java/com/tonyodev/fetch2/downloader/ParallelFileDownloaderImpl.kt index c23f1b80..afced933 100644 --- a/fetch2/src/main/java/com/tonyodev/fetch2/downloader/ParallelFileDownloaderImpl.kt +++ b/fetch2/src/main/java/com/tonyodev/fetch2/downloader/ParallelFileDownloaderImpl.kt @@ -196,6 +196,7 @@ class ParallelFileDownloaderImpl(private val initialDownload: Download, if (!interrupted && !terminated) { downloadInfo.etaInMilliSeconds = estimatedTimeRemainingInMilliseconds downloadInfo.downloadedBytesPerSecond = getAverageDownloadedBytesPerSecond() + val completedDownload = downloadInfo.copy() delegate?.onProgress( download = downloadInfo, etaInMilliSeconds = downloadInfo.etaInMilliSeconds, @@ -203,7 +204,7 @@ class ParallelFileDownloaderImpl(private val initialDownload: Download, downloadInfo.etaInMilliSeconds = -1 downloadInfo.downloadedBytesPerSecond = -1 delegate?.onComplete( - download = downloadInfo) + download = completedDownload) } } else { deleteAllInFolderForId(downloadInfo.id, fileTempDir) @@ -214,6 +215,7 @@ class ParallelFileDownloaderImpl(private val initialDownload: Download, if (!interrupted && !terminated) { downloadInfo.etaInMilliSeconds = estimatedTimeRemainingInMilliseconds downloadInfo.downloadedBytesPerSecond = getAverageDownloadedBytesPerSecond() + val completedDownload = downloadInfo.copy() delegate?.onProgress( download = downloadInfo, etaInMilliSeconds = downloadInfo.etaInMilliSeconds, @@ -221,7 +223,7 @@ class ParallelFileDownloaderImpl(private val initialDownload: Download, downloadInfo.etaInMilliSeconds = -1 downloadInfo.downloadedBytesPerSecond = -1 delegate?.onComplete( - download = downloadInfo) + download = completedDownload) } } } diff --git a/fetch2/src/main/java/com/tonyodev/fetch2/downloader/SequentialFileDownloaderImpl.kt b/fetch2/src/main/java/com/tonyodev/fetch2/downloader/SequentialFileDownloaderImpl.kt index ebb4c333..e623b3cf 100644 --- a/fetch2/src/main/java/com/tonyodev/fetch2/downloader/SequentialFileDownloaderImpl.kt +++ b/fetch2/src/main/java/com/tonyodev/fetch2/downloader/SequentialFileDownloaderImpl.kt @@ -297,6 +297,7 @@ class SequentialFileDownloaderImpl(private val initialDownload: Download, delegate?.onDownloadBlockUpdated(downloadInfo, downloadBlock, totalDownloadBlocks) downloadInfo.etaInMilliSeconds = estimatedTimeRemainingInMilliseconds downloadInfo.downloadedBytesPerSecond = getAverageDownloadedBytesPerSecond() + val completedDownload = downloadInfo.copy() delegate?.onProgress( download = downloadInfo, etaInMilliSeconds = downloadInfo.etaInMilliSeconds, @@ -304,7 +305,7 @@ class SequentialFileDownloaderImpl(private val initialDownload: Download, downloadInfo.etaInMilliSeconds = -1 downloadInfo.downloadedBytesPerSecond = -1 delegate?.onComplete( - download = downloadInfo) + download = completedDownload) } } else { throw FetchException(INVALID_CONTENT_HASH) @@ -315,6 +316,7 @@ class SequentialFileDownloaderImpl(private val initialDownload: Download, delegate?.onDownloadBlockUpdated(downloadInfo, downloadBlock, totalDownloadBlocks) downloadInfo.etaInMilliSeconds = estimatedTimeRemainingInMilliseconds downloadInfo.downloadedBytesPerSecond = getAverageDownloadedBytesPerSecond() + val completedDownload = downloadInfo.copy() delegate?.onProgress( download = downloadInfo, etaInMilliSeconds = downloadInfo.etaInMilliSeconds, @@ -322,7 +324,7 @@ class SequentialFileDownloaderImpl(private val initialDownload: Download, downloadInfo.etaInMilliSeconds = -1 downloadInfo.downloadedBytesPerSecond = -1 delegate?.onComplete( - download = downloadInfo) + download = completedDownload) } } } diff --git a/fetch2/src/main/java/com/tonyodev/fetch2/fetch/ListenerCoordinator.kt b/fetch2/src/main/java/com/tonyodev/fetch2/fetch/ListenerCoordinator.kt index c20e4662..d47e7dee 100644 --- a/fetch2/src/main/java/com/tonyodev/fetch2/fetch/ListenerCoordinator.kt +++ b/fetch2/src/main/java/com/tonyodev/fetch2/fetch/ListenerCoordinator.kt @@ -757,7 +757,7 @@ class ListenerCoordinator(val namespace: String, if (iterator != null) { while (iterator.hasNext()) { val reference = iterator.next() - if (reference.get() == fetchObservers) { + if (reference.get() == fetchObserver) { iterator.remove() break } diff --git a/fetch2/src/main/java/com/tonyodev/fetch2/provider/NetworkInfoProvider.kt b/fetch2/src/main/java/com/tonyodev/fetch2/provider/NetworkInfoProvider.kt index 4f87531a..3b08186d 100644 --- a/fetch2/src/main/java/com/tonyodev/fetch2/provider/NetworkInfoProvider.kt +++ b/fetch2/src/main/java/com/tonyodev/fetch2/provider/NetworkInfoProvider.kt @@ -11,6 +11,7 @@ import android.net.NetworkRequest import android.os.Build import com.tonyodev.fetch2.NetworkType import com.tonyodev.fetch2core.isNetworkAvailable +import com.tonyodev.fetch2core.isOnMeteredConnection import com.tonyodev.fetch2core.isOnWiFi import java.net.HttpURLConnection import java.net.URL @@ -102,6 +103,9 @@ class NetworkInfoProvider constructor(private val context: Context, if (networkType == NetworkType.WIFI_ONLY && context.isOnWiFi()) { return true } + if (networkType == NetworkType.UNMETERED && !context.isOnMeteredConnection()) { + return true + } if (networkType == NetworkType.ALL && context.isNetworkAvailable()) { return true } diff --git a/fetch2core/src/main/java/com/tonyodev/fetch2core/AndroidExtentions.kt b/fetch2core/src/main/java/com/tonyodev/fetch2core/AndroidExtentions.kt index 71dbfda5..b8c54438 100644 --- a/fetch2core/src/main/java/com/tonyodev/fetch2core/AndroidExtentions.kt +++ b/fetch2core/src/main/java/com/tonyodev/fetch2core/AndroidExtentions.kt @@ -4,6 +4,8 @@ package com.tonyodev.fetch2core import android.content.Context import android.net.ConnectivityManager +import android.net.NetworkInfo +import android.os.Build fun Context.isOnWiFi(): Boolean { @@ -16,6 +18,27 @@ fun Context.isOnWiFi(): Boolean { } } +fun Context.isOnMeteredConnection(): Boolean { + val cm = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + return if (Build.VERSION.SDK_INT >= 16) { + cm.isActiveNetworkMetered + } else { + val info: NetworkInfo = cm.activeNetworkInfo ?: return true + when (info.type) { + ConnectivityManager.TYPE_MOBILE, + ConnectivityManager.TYPE_MOBILE_DUN, + ConnectivityManager.TYPE_MOBILE_HIPRI, + ConnectivityManager.TYPE_MOBILE_MMS, + ConnectivityManager.TYPE_MOBILE_SUPL, + ConnectivityManager.TYPE_WIMAX -> true + ConnectivityManager.TYPE_WIFI, + ConnectivityManager.TYPE_BLUETOOTH, + ConnectivityManager.TYPE_ETHERNET -> false + else -> true + } + } +} + fun Context.isNetworkAvailable(): Boolean { val manager = this.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager val activeNetworkInfo = manager.activeNetworkInfo diff --git a/fetch2core/src/main/java/com/tonyodev/fetch2core/ErrorStrings.kt b/fetch2core/src/main/java/com/tonyodev/fetch2core/ErrorStrings.kt index d496f989..81e7af44 100644 --- a/fetch2core/src/main/java/com/tonyodev/fetch2core/ErrorStrings.kt +++ b/fetch2core/src/main/java/com/tonyodev/fetch2core/ErrorStrings.kt @@ -40,4 +40,5 @@ const val AWAIT_CALL_ON_UI_THREAD = "await_call_on_ui_thread" const val BLOCKING_CALL_ON_UI_THREAD = "blocking_call_on_ui_thread" const val FILE_CANNOT_BE_RENAMED = "file_cannot_be_renamed" const val FAILED_RENAME_FILE_ASSOCIATED_WITH_INCOMPLETE_DOWNLOAD = "cannot rename file associated with incomplete download" -const val FILE_ALLOCATION_ERROR = "file_allocation_error" \ No newline at end of file +const val FILE_ALLOCATION_ERROR = "file_allocation_error" +const val CLEAR_TEXT_NETWORK_VIOLATION = "Cleartext HTTP traffic to" \ No newline at end of file diff --git a/fetch2core/src/main/java/com/tonyodev/fetch2core/FetchCoreUtils.kt b/fetch2core/src/main/java/com/tonyodev/fetch2core/FetchCoreUtils.kt index 92913404..799ac812 100644 --- a/fetch2core/src/main/java/com/tonyodev/fetch2core/FetchCoreUtils.kt +++ b/fetch2core/src/main/java/com/tonyodev/fetch2core/FetchCoreUtils.kt @@ -2,12 +2,14 @@ package com.tonyodev.fetch2core +import android.annotation.SuppressLint import android.content.Context import android.net.Uri import java.io.* import java.math.BigInteger import java.net.CookieManager import java.net.CookiePolicy +import java.net.HttpURLConnection import java.security.DigestInputStream import java.security.MessageDigest import java.util.concurrent.TimeUnit @@ -18,20 +20,30 @@ const val GET_REQUEST_METHOD = "GET" const val HEAD_REQUEST_METHOD = "HEAD" +internal const val HEADER_ACCEPT_RANGE = "Accept-Ranges" + +internal const val HEADER_ACCEPT_RANGE_LEGACY = "accept-ranges" + +internal const val HEADER_ACCEPT_RANGE_COMPAT = "AcceptRanges" + internal const val HEADER_CONTENT_LENGTH = "content-length" internal const val HEADER_CONTENT_LENGTH_LEGACY = "Content-Length" internal const val HEADER_CONTENT_LENGTH_COMPAT = "ContentLength" -internal const val HEADER_CONTENT_RANGE = "content-range" - -internal const val HEADER_CONTENT_RANGE_LEGACY = "Content-Range" - internal const val HEADER_TRANSFER_ENCODING = "Transfer-Encoding" +internal const val HEADER_TRANSFER_LEGACY = "transfer-encoding" + internal const val HEADER_TRANSFER_ENCODING_COMPAT = "TransferEncoding" +internal const val HEADER_CONTENT_RANGE = "Content-Range" + +internal const val HEADER_CONTENT_RANGE_LEGACY = "content-range" + +internal const val HEADER_CONTENT_RANGE_COMPAT = "ContentRange" + fun calculateProgress(downloaded: Long, total: Long): Int { return when { total < 1 -> -1 @@ -196,9 +208,10 @@ fun getRangeForFetchFileServerRequest(range: String): Pair { return Pair(start, end) } +@Suppress("ControlFlowWithEmptyBody") fun getMd5String(bytes: ByteArray, start: Int = 0, length: Int = bytes.size): String { return try { - val buffer = ByteArray(8192) + val buffer = ByteArray(kotlin.io.DEFAULT_BUFFER_SIZE) val md = MessageDigest.getInstance("MD5") val inputStream = DigestInputStream(ByteArrayInputStream(bytes, start, length), md) inputStream.use { dis -> @@ -214,10 +227,11 @@ fun getMd5String(bytes: ByteArray, start: Int = 0, length: Int = bytes.size): St } } +@Suppress("ControlFlowWithEmptyBody") fun getFileMd5String(file: String): String? { val contentFile = File(file) return try { - val buffer = ByteArray(8192) + val buffer = ByteArray(kotlin.io.DEFAULT_BUFFER_SIZE) val md = MessageDigest.getInstance("MD5") val inputStream = DigestInputStream(FileInputStream(contentFile), md) inputStream.use { dis -> @@ -233,28 +247,19 @@ fun getFileMd5String(file: String): String? { } } -fun isParallelDownloadingSupported(responseHeaders: Map>): Boolean { - val transferEncoding = responseHeaders[HEADER_TRANSFER_ENCODING]?.firstOrNull() - ?: responseHeaders[HEADER_TRANSFER_ENCODING_COMPAT]?.firstOrNull() - ?: "" - return transferEncoding.takeIf { it != "chunked" }?.let { - responseHeaders.let { headers -> - when { - headers.containsKey(HEADER_CONTENT_LENGTH) -> headers[HEADER_CONTENT_LENGTH] - headers.containsKey(HEADER_CONTENT_LENGTH_LEGACY) -> headers[HEADER_CONTENT_LENGTH_LEGACY] - headers.containsKey(HEADER_CONTENT_LENGTH_COMPAT) -> headers[HEADER_CONTENT_LENGTH_COMPAT] - else -> null - }?.firstOrNull()?.toLongOrNull() - }.let { value -> value != null && value > -1 } - } ?: false +fun isParallelDownloadingSupported(code: Int, headers: Map>): Boolean { + return acceptRanges(code, headers) } -fun getRequestSupportedFileDownloaderTypes(request: Downloader.ServerRequest, downloader: Downloader<*, *>): Set { +fun getRequestSupportedFileDownloaderTypes( + request: Downloader.ServerRequest, + downloader: Downloader<*, *> +): Set { val fileDownloaderTypeSet = mutableSetOf(Downloader.FileDownloaderType.SEQUENTIAL) return try { val response = downloader.execute(request, getSimpleInterruptMonitor()) if (response != null) { - if (isParallelDownloadingSupported(response.responseHeaders)) { + if (isParallelDownloadingSupported(response.code, response.responseHeaders)) { fileDownloaderTypeSet.add(Downloader.FileDownloaderType.PARALLEL) } downloader.disconnect(response) @@ -265,10 +270,69 @@ fun getRequestSupportedFileDownloaderTypes(request: Downloader.ServerRequest, do } } +@SuppressLint("DefaultLocale") +fun acceptRanges( + code: Int, + headers: Map> +): Boolean { + val acceptRangeValue = getHeaderValue( + headers, + HEADER_ACCEPT_RANGE, + HEADER_ACCEPT_RANGE_LEGACY, + HEADER_ACCEPT_RANGE_COMPAT + ) + val transferValue = getHeaderValue( + headers, + HEADER_TRANSFER_ENCODING, + HEADER_TRANSFER_LEGACY, + HEADER_TRANSFER_ENCODING_COMPAT + ) + val contentLength = getContentLengthFromHeader(headers, -1L) + val acceptsRanges = code == HttpURLConnection.HTTP_PARTIAL || acceptRangeValue == "bytes" + return (contentLength > -1L && acceptsRanges) || (contentLength > -1L && transferValue?.toLowerCase() != "chunked") +} + +fun getContentLengthFromHeader(headers: Map>, defaultValue: Long): Long { + val contentRange = getHeaderValue( + headers, + HEADER_CONTENT_RANGE, + HEADER_CONTENT_RANGE_LEGACY, + HEADER_CONTENT_RANGE_COMPAT + ) + val lastIndexOf = contentRange?.lastIndexOf("/") + var contentLength = -1L + if (lastIndexOf != null && lastIndexOf != -1 && lastIndexOf < contentRange.length) { + contentLength = contentRange.substring(lastIndexOf + 1).toLongOrNull() ?: -1L + } + if (contentLength == -1L) { + contentLength = getHeaderValue( + headers, + HEADER_CONTENT_LENGTH, + HEADER_CONTENT_LENGTH_LEGACY, + HEADER_CONTENT_LENGTH_COMPAT + )?.toLongOrNull() ?: defaultValue + } + return contentLength +} + +fun getHeaderValue( + headers: Map>, + vararg keys: String +): String? { + for (key in keys) { + val value = headers[key]?.firstOrNull() + if (!value.isNullOrBlank()) { + return value + } + } + return null +} + fun getRequestContentLength(request: Downloader.ServerRequest, downloader: Downloader<*, *>): Long { return try { val response = downloader.execute(request, getSimpleInterruptMonitor()) - val contentLength = response?.contentLength ?: -1L + val headers = response?.responseHeaders ?: emptyMap() + val contentLength = getContentLengthFromHeader(headers, -1L) if (response != null) { downloader.disconnect(response) } @@ -352,30 +416,4 @@ fun copyStreamToString(inputStream: InputStream?, closeStream: Boolean = true): fun getSimpleInterruptMonitor() = object : InterruptMonitor { override val isInterrupted: Boolean get() = false -} - -fun getContentLengthFromHeader(responseHeaders: Map>, defaultValue: Long): Long { - // responseHeaders declared with Not-Null keys and Not-Null values - // remove unnecessary conversions here - when { - responseHeaders.containsKey(HEADER_CONTENT_LENGTH) -> responseHeaders[HEADER_CONTENT_LENGTH] - // HTTP/1.1 compat - responseHeaders.containsKey(HEADER_CONTENT_LENGTH_LEGACY) -> responseHeaders[HEADER_CONTENT_LENGTH_LEGACY] - else -> null - }?.firstOrNull()?.toLongOrNull()?.takeIf { it > 0 }?.also { - return it - } - when { - responseHeaders.containsKey(HEADER_CONTENT_RANGE) -> responseHeaders[HEADER_CONTENT_RANGE] - // HTTP/1.1 compat - responseHeaders.containsKey(HEADER_CONTENT_RANGE_LEGACY) -> responseHeaders[HEADER_CONTENT_RANGE_LEGACY] - else -> null - }?.firstOrNull()?.also { value -> - value.lastIndexOf("/") - .takeIf { it != -1 && it < value.length.minus(1) } - ?.let { index -> value.substring(index + 1).toLongOrNull() } - ?.takeIf { size -> size > 0 } - ?.also { size -> return size } - } - return defaultValue } \ No newline at end of file diff --git a/fetch2core/src/main/java/com/tonyodev/fetch2core/FileResource.kt b/fetch2core/src/main/java/com/tonyodev/fetch2core/FileResource.kt index 7123ca23..6a497966 100644 --- a/fetch2core/src/main/java/com/tonyodev/fetch2core/FileResource.kt +++ b/fetch2core/src/main/java/com/tonyodev/fetch2core/FileResource.kt @@ -17,7 +17,7 @@ class FileResource : Parcelable, Serializable { /** Content Length */ var length: Long = 0L - /** Absolute File Path */ + /** The File Path. Can be a file or uri. */ var file: String = "" /** Unique Short name of the File Resource. diff --git a/fetch2core/src/main/java/com/tonyodev/fetch2core/StorageResolverHelper.kt b/fetch2core/src/main/java/com/tonyodev/fetch2core/StorageResolverHelper.kt index ba74873f..1b0e63e8 100644 --- a/fetch2core/src/main/java/com/tonyodev/fetch2core/StorageResolverHelper.kt +++ b/fetch2core/src/main/java/com/tonyodev/fetch2core/StorageResolverHelper.kt @@ -134,7 +134,7 @@ fun deleteFile(filePath: String, context: Context): Boolean { when { uri.scheme == "file" -> { val file = File(uri.path) - if (file.canWrite() && file.exists()) deleteFile(file) else context.contentResolver.delete(uri, null, null) > 0 + if (file.canWrite() && file.exists()) deleteFile(file) else false } uri.scheme == "content" -> { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT diff --git a/fetch2fileserver/src/main/java/com/tonyodev/fetch2fileserver/FetchFileServer.kt b/fetch2fileserver/src/main/java/com/tonyodev/fetch2fileserver/FetchFileServer.kt index f737231a..133d4745 100644 --- a/fetch2fileserver/src/main/java/com/tonyodev/fetch2fileserver/FetchFileServer.kt +++ b/fetch2fileserver/src/main/java/com/tonyodev/fetch2fileserver/FetchFileServer.kt @@ -95,6 +95,7 @@ interface FetchFileServer { private var fileResourceDatabaseName = "LibFetchFileServerDatabaseLib.db" private var progressReportingInMillis = DEFAULT_PROGRESS_REPORTING_INTERVAL_IN_MILLISECONDS private var persistentConnectionTimeout = DEFAULT_PERSISTENT_TIME_OUT_IN_MILLISECONDS + private var fileResolver: FileResolver = object: FileResolver(context.applicationContext) {} /** Set Custom Server Socket * @param serverSocket @@ -194,6 +195,14 @@ interface FetchFileServer { return this } + /** + * Sets the file resolver that will be used to open [FileResource.file] resources. + * */ + fun setFileResolver(fileResolver: FileResolver): Builder { + this.fileResolver = fileResolver + return this + } + /** Build the FetchFileServer Instance. * @return new Fetch File Server instance. * */ @@ -207,7 +216,9 @@ interface FetchFileServer { fetchFileServerDelegate = fileServerDelegate, fetchTransferListener = transferListener, progressReportingInMillis = progressReportingInMillis, - persistentTimeoutInMillis = persistentConnectionTimeout) + persistentTimeoutInMillis = persistentConnectionTimeout, + fileResolver = fileResolver + ) } } diff --git a/fetch2fileserver/src/main/java/com/tonyodev/fetch2fileserver/FetchFileServerImpl.kt b/fetch2fileserver/src/main/java/com/tonyodev/fetch2fileserver/FetchFileServerImpl.kt index ce53d8f9..0c324049 100644 --- a/fetch2fileserver/src/main/java/com/tonyodev/fetch2fileserver/FetchFileServerImpl.kt +++ b/fetch2fileserver/src/main/java/com/tonyodev/fetch2fileserver/FetchFileServerImpl.kt @@ -30,7 +30,8 @@ class FetchFileServerImpl(context: Context, private val fetchFileServerDelegate: FetchFileServerDelegate?, private val fetchTransferListener: FetchTransferListener?, private val progressReportingInMillis: Long, - private val persistentTimeoutInMillis: Long) : FetchFileServer { + private val persistentTimeoutInMillis: Long, + private val fileResolver: FileResolver) : FetchFileServer { private val lock = Any() private val uuid = UUID.randomUUID().toString() @@ -106,7 +107,9 @@ class FetchFileServerImpl(context: Context, logger = logger, ioHandler = ioHandler, progressReportingInMillis = progressReportingInMillis, - persistentTimeoutInMillis = persistentTimeoutInMillis) + persistentTimeoutInMillis = persistentTimeoutInMillis, + fileResolver = fileResolver + ) try { fileResourceProviderMap[fileResourceProvider.id] = fileResourceProvider fileResourceProvider.execute() @@ -153,7 +156,7 @@ class FetchFileServerImpl(context: Context, catalogFileResourceInfo.id = FileRequest.CATALOG_ID val catalogMap = mutableMapOf() catalogMap["data"] = catalog - catalogFileResourceInfo.extras = JSONObject(catalogMap).toString() + catalogFileResourceInfo.extras = JSONObject(catalogMap as Map<*, *>).toString() catalogFileResourceInfo.name = FileRequest.CATALOG_NAME catalogFileResourceInfo.file = FileRequest.CATALOG_FILE return catalogFileResourceInfo.toFileResource() diff --git a/fetch2fileserver/src/main/java/com/tonyodev/fetch2fileserver/FileResolver.kt b/fetch2fileserver/src/main/java/com/tonyodev/fetch2fileserver/FileResolver.kt new file mode 100644 index 00000000..0487783e --- /dev/null +++ b/fetch2fileserver/src/main/java/com/tonyodev/fetch2fileserver/FileResolver.kt @@ -0,0 +1,132 @@ +package com.tonyodev.fetch2fileserver + +import android.content.ContentResolver +import android.content.Context +import android.net.Uri +import android.os.ParcelFileDescriptor +import com.tonyodev.fetch2core.* +import com.tonyodev.fetch2core.server.FileRequest +import java.io.* + +/** + * Used to provide [InputResourceWrapper] for [FileResource] + * */ +abstract class FileResolver(context: Context) { + + protected val appContext: Context = context.applicationContext + protected val contentResolver: ContentResolver = appContext.contentResolver + + /** + * Returns the [InputResourceWrapper] for the catalog file + * */ + open fun getCatalogInputWrapper(catalog: ByteArray, request: FileRequest, fileResource: FileResource): InputResourceWrapper { + return object : InputResourceWrapper() { + + private val inputStream = ByteArrayInputStream(catalog, request.rangeStart.toInt(), fileResource.length.toInt()) + + override fun read(byteArray: ByteArray, offSet: Int, length: Int): Int { + return inputStream.read(byteArray, offSet, length) + } + + override fun setReadOffset(offset: Long) { + inputStream.skip(offset) + } + + override fun close() { + inputStream.close() + } + } + } + + + /** + * Returns the [InputResourceWrapper] for a [FileResource]. Override this method if your [FileResource.file] + * is not a traditional File but Uri, etc. + * */ + @Throws(IOException::class) + open fun getInputWrapper(fileResource: FileResource): InputResourceWrapper { + val filePath = fileResource.file + return if (isUriPath(filePath)) { + getUriInputResourceWrapper(fileResource) + } else { + getFileInputResourceWrapper(fileResource) + } + } + + private fun getUriInputResourceWrapper(fileResource: FileResource): InputResourceWrapper { + val fileUri = Uri.parse(fileResource.file) + return when (fileUri.scheme) { + "content" -> { + val parcelFileDescriptor = contentResolver.openFileDescriptor(fileUri, "w") + if (parcelFileDescriptor == null) { + throw FileNotFoundException("$fileUri $FILE_NOT_FOUND") + } else { + createUriInputResourceWrapper(FileInputStream(parcelFileDescriptor.fileDescriptor), parcelFileDescriptor) + } + } + "file" -> { + val file = File(fileUri.path) + if (file.exists() && file.canWrite()) { + createUriInputResourceWrapper(FileInputStream(file), null) + } else { + val parcelFileDescriptor = contentResolver.openFileDescriptor(fileUri, "w") + if (parcelFileDescriptor == null) { + throw FileNotFoundException("$fileUri $FILE_NOT_FOUND") + } else { + createUriInputResourceWrapper(FileInputStream(parcelFileDescriptor.fileDescriptor), parcelFileDescriptor) + } + } + } + else -> { + throw FileNotFoundException("$fileUri $FILE_NOT_FOUND") + } + } + } + + private fun createUriInputResourceWrapper( + fileInputStream: FileInputStream, + fileDescriptor: ParcelFileDescriptor? = null + ): InputResourceWrapper { + return object : InputResourceWrapper() { + + private val wrapperParcelFileDescriptor = fileDescriptor + private val wrapperFileInputStream = fileInputStream + + init { + wrapperFileInputStream.channel.position(0) + } + + override fun read(byteArray: ByteArray, offSet: Int, length: Int): Int { + return wrapperFileInputStream.read(byteArray, offSet, length) + } + + override fun setReadOffset(offset: Long) { + wrapperFileInputStream.channel.position(offset) + } + + override fun close() { + wrapperFileInputStream.close() + } + } + } + + private fun getFileInputResourceWrapper(fileResource: FileResource): InputResourceWrapper { + return object : InputResourceWrapper() { + + val randomAccessFile = RandomAccessFile(fileResource.file, "r") + + override fun read(byteArray: ByteArray, offSet: Int, length: Int): Int { + return randomAccessFile.read(byteArray, offSet, length) + } + + override fun setReadOffset(offset: Long) { + randomAccessFile.seek(offset) + } + + override fun close() { + randomAccessFile.close() + } + } + } + +} \ No newline at end of file diff --git a/fetch2fileserver/src/main/java/com/tonyodev/fetch2fileserver/provider/FetchFileResourceProvider.kt b/fetch2fileserver/src/main/java/com/tonyodev/fetch2fileserver/provider/FetchFileResourceProvider.kt index bada416d..3ffb1cc0 100644 --- a/fetch2fileserver/src/main/java/com/tonyodev/fetch2fileserver/provider/FetchFileResourceProvider.kt +++ b/fetch2fileserver/src/main/java/com/tonyodev/fetch2fileserver/provider/FetchFileResourceProvider.kt @@ -8,8 +8,7 @@ import com.tonyodev.fetch2core.server.FileResponse.CREATOR.CLOSE_CONNECTION import com.tonyodev.fetch2core.server.FileResponse.CREATOR.OPEN_CONNECTION import com.tonyodev.fetch2core.server.FileResourceTransporter import com.tonyodev.fetch2core.server.FetchFileResourceTransporter -import java.io.ByteArrayInputStream -import java.io.RandomAccessFile +import com.tonyodev.fetch2fileserver.FileResolver import java.net.HttpURLConnection import java.net.Socket import java.util.* @@ -20,7 +19,8 @@ class FetchFileResourceProvider(private val client: Socket, private val logger: FetchLogger, private val ioHandler: Handler, private val progressReportingInMillis: Long, - private val persistentTimeoutInMillis: Long) : FileResourceProvider { + private val persistentTimeoutInMillis: Long, + private val fileResolver: FileResolver) : FileResourceProvider { override val id = UUID.randomUUID().toString() private val lock = Any() @@ -81,39 +81,9 @@ class FetchFileResourceProvider(private val client: Socket, val catalog = fileResource.extras.getString("data", "{}").toByteArray(Charsets.UTF_8) fileResource.length = if (request.rangeEnd == -1L) catalog.size.toLong() else request.rangeEnd fileResource.md5 = getMd5String(catalog) - inputResourceWrapper = object : InputResourceWrapper() { - - private val inputStream = ByteArrayInputStream(catalog, request.rangeStart.toInt(), fileResource.length.toInt()) - - override fun read(byteArray: ByteArray, offSet: Int, length: Int): Int { - return inputStream.read(byteArray, offSet, length) - } - - override fun setReadOffset(offset: Long) { - inputStream.skip(offset) - } - - override fun close() { - inputStream.close() - } - } + inputResourceWrapper = fileResolver.getCatalogInputWrapper(catalog, request, fileResource) } else { - inputResourceWrapper = object : InputResourceWrapper() { - - val randomAccessFile = RandomAccessFile(fileResource.file, "r") - - override fun read(byteArray: ByteArray, offSet: Int, length: Int): Int { - return randomAccessFile.read(byteArray, offSet, length) - } - - override fun setReadOffset(offset: Long) { - randomAccessFile.seek(offset) - } - - override fun close() { - randomAccessFile.close() - } - } + inputResourceWrapper = fileResolver.getInputWrapper(fileResource) inputResourceWrapper?.setReadOffset(request.rangeStart) } } diff --git a/fetch2okhttp/src/main/java/com/tonyodev/fetch2okhttp/OkHttpDownloader.kt b/fetch2okhttp/src/main/java/com/tonyodev/fetch2okhttp/OkHttpDownloader.kt index 564d73bf..206cebd7 100644 --- a/fetch2okhttp/src/main/java/com/tonyodev/fetch2okhttp/OkHttpDownloader.kt +++ b/fetch2okhttp/src/main/java/com/tonyodev/fetch2okhttp/OkHttpDownloader.kt @@ -1,7 +1,6 @@ package com.tonyodev.fetch2okhttp import com.tonyodev.fetch2core.* -import okhttp3.Headers import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response @@ -50,19 +49,6 @@ open class OkHttpDownloader @JvmOverloads constructor( return okHttpRequestBuilder.build() } - private fun getResponseHeaders(okResponseHeaders: Headers): MutableMap> { - val headers = mutableMapOf>() - for (i in 0 until okResponseHeaders.size()) { - val key = okResponseHeaders.name(i) - @Suppress("SENSELESS_COMPARISON") - if (key != null) { - val values = okResponseHeaders.values(key) - headers[key.toLowerCase()] = values - } - } - return headers - } - private fun getRedirectedServerRequest(oldRequest: Downloader.ServerRequest, redirectUrl: String): Downloader.ServerRequest { return Downloader.ServerRequest( id = oldRequest.id, @@ -88,13 +74,13 @@ open class OkHttpDownloader @JvmOverloads constructor( .build() } var okHttpResponse = client.newCall(okHttpRequest).execute() - var responseHeaders = getResponseHeaders(okHttpResponse.headers()) - var code = okHttpResponse.code() + var responseHeaders = okHttpResponse.headers.toMultimap() + var code = okHttpResponse.code if ((code == HttpURLConnection.HTTP_MOVED_TEMP || code == HttpURLConnection.HTTP_MOVED_PERM - || code == HttpURLConnection.HTTP_SEE_OTHER) && responseHeaders.containsKey("location")) { + || code == HttpURLConnection.HTTP_SEE_OTHER) && getHeaderValue(responseHeaders, "Location") != null) { okHttpRequest = onPreClientExecute(client, getRedirectedServerRequest(request, - responseHeaders["location"]?.firstOrNull() ?: "")) + getHeaderValue(responseHeaders, "Location") ?: "")) if (okHttpRequest.header("Referer") == null) { val referer = getRefererFromUrl(request.url) okHttpRequest = okHttpRequest.newBuilder() @@ -102,26 +88,22 @@ open class OkHttpDownloader @JvmOverloads constructor( .build() } okHttpResponse = client.newCall(okHttpRequest).execute() - responseHeaders = getResponseHeaders(okHttpResponse.headers()) - code = okHttpResponse.code() + responseHeaders = okHttpResponse.headers.toMultimap() + code = okHttpResponse.code } + val success = okHttpResponse.isSuccessful - var contentLength = getContentLengthFromHeader(responseHeaders, -1) - val byteStream: InputStream? = okHttpResponse.body()?.byteStream() + val contentLength = getContentLengthFromHeader(responseHeaders, -1L) + val byteStream: InputStream? = okHttpResponse.body?.byteStream() val errorResponseString: String? = if (!success) { copyStreamToString(byteStream, false) } else { null } - val hash = getContentHash(responseHeaders) - - if (contentLength < 1) { - contentLength = responseHeaders["content-length"]?.firstOrNull()?.toLong() ?: -1L - } + val hash = getContentHash(responseHeaders.toMutableMap()) - val acceptsRanges = code == HttpURLConnection.HTTP_PARTIAL || - responseHeaders["accept-ranges"]?.firstOrNull() == "bytes" + val acceptsRanges = acceptRanges(code, responseHeaders) onServerResponse(request, Downloader.Response( code = code, @@ -150,7 +132,7 @@ open class OkHttpDownloader @JvmOverloads constructor( } override fun getContentHash(responseHeaders: MutableMap>): String { - return responseHeaders["content-md5"]?.firstOrNull() ?: "" + return getHeaderValue(responseHeaders, "Content-MD5") ?: "" } override fun disconnect(response: Downloader.Response) { @@ -209,6 +191,9 @@ open class OkHttpDownloader @JvmOverloads constructor( } override fun getRequestSupportedFileDownloaderTypes(request: Downloader.ServerRequest): Set { + if (fileDownloaderType == Downloader.FileDownloaderType.SEQUENTIAL) { + return mutableSetOf(fileDownloaderType) + } return try { getRequestSupportedFileDownloaderTypes(request, this) } catch (e: Exception) { diff --git a/fetch2rx/src/main/java/com/tonyodev/fetch2rx/RxFetchImpl.kt b/fetch2rx/src/main/java/com/tonyodev/fetch2rx/RxFetchImpl.kt index fa6d7c9b..cd38c49c 100644 --- a/fetch2rx/src/main/java/com/tonyodev/fetch2rx/RxFetchImpl.kt +++ b/fetch2rx/src/main/java/com/tonyodev/fetch2rx/RxFetchImpl.kt @@ -389,10 +389,10 @@ open class RxFetchImpl(override val namespace: String, } } - override fun removeAllInGroupWithStatus(id: Int, status: List): Convertible> { + override fun removeAllInGroupWithStatus(id: Int, statuses: List): Convertible> { return synchronized(lock) { throwExceptionIfClosed() - Flowable.just(Pair(id, status)) + Flowable.just(Pair(id, statuses)) .subscribeOn(scheduler) .flatMap { throwExceptionIfClosed() @@ -658,7 +658,11 @@ open class RxFetchImpl(override val namespace: String, listenerCoordinator.mainListener.onQueued(download, false) } } - Flowable.just(download) + if (download != null) { + Flowable.just(download) + } else { + throw FetchException(REQUEST_DOES_NOT_EXIST) + } } .observeOn(uiScheduler) .toConvertible() @@ -824,7 +828,11 @@ open class RxFetchImpl(override val namespace: String, .flatMap { throwExceptionIfClosed() val download = fetchHandler.getDownload(id) - Flowable.just(download) + if (download != null) { + Flowable.just(download) + } else { + throw FetchException(REQUEST_DOES_NOT_EXIST) + } } .observeOn(uiScheduler) .toConvertible() diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e54d5950..16315bf9 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,4 +1,4 @@ -#Mon Apr 22 08:01:27 EDT 2019 +#Sat Jun 06 11:30:23 EDT 2020 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME diff --git a/sampleApp/src/main/java/com/tonyodev/fetchapp/Data.java b/sampleApp/src/main/java/com/tonyodev/fetchapp/Data.java index 479aa1d3..d87fac54 100644 --- a/sampleApp/src/main/java/com/tonyodev/fetchapp/Data.java +++ b/sampleApp/src/main/java/com/tonyodev/fetchapp/Data.java @@ -15,10 +15,10 @@ public final class Data { public static final String[] sampleUrls = new String[]{ "http://speedtest.ftp.otenet.gr/files/test100Mb.db", - "http://download.blender.org/peach/bigbuckbunny_movies/BigBuckBunny_640x360.m4v", + "https://download.blender.org/peach/bigbuckbunny_movies/big_buck_bunny_720p_stereo.avi", "http://media.mongodb.org/zips.json", "http://www.exampletonyotest/some/unknown/123/Errorlink.txt", - "http://storage.googleapis.com/ix_choosemuse/uploads/2016/02/android-logo.png", + "https://upload.wikimedia.org/wikipedia/commons/thumb/8/82/Android_logo_2019.svg/687px-Android_logo_2019.svg.png", "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"}; private Data() { diff --git a/versions.gradle b/versions.gradle index 500bc48f..31558bd2 100644 --- a/versions.gradle +++ b/versions.gradle @@ -1,7 +1,7 @@ ext { - kotlin_version = '1.3.41' - okhttp_version = '3.12.3' - okhttp_url_version = '3.11.0' + kotlin_version = '1.3.72' + okhttp_version = '4.7.2' + okhttp_url_version = '4.7.2' android_support_version = '28.0.0' constraint_layout_version = "1.1.3" room_version = '1.1.1' @@ -9,14 +9,14 @@ ext { espresso_version = '3.0.1' test_runner_version = '1.0.1' library_min_version = '14' - library_compile_version = 28 - library_target_version = 28 - library_build_tools_version = "28.0.3" + library_compile_version = 29 + library_target_version = 29 + library_build_tools_version = "29.0.3" gradle_tools_version = '3.4.1' - rxJava2_version = "2.2.10" + rxJava2_version = "2.2.19" rxAndroid2_version = "2.1.1" timber_version = "4.7.1" - novoda_bintray_version = "0.9" - library_version = "3.0.10" - library_version_code = 73 + novoda_bintray_version = "0.9.2" + library_version = "3.0.11" + library_version_code = 76 } \ No newline at end of file