diff options
3 files changed, 198 insertions, 12 deletions
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 41b90485..91932a6b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -143,6 +143,11 @@ android:enabled="true" android:label="@string/frost_notifications" android:permission="android.permission.BIND_JOB_SERVICE" /> + <service + android:name=".services.DownloadService" + android:enabled="true" + android:label="@string/frost_notifications" + android:permission="android.permission.WRITE_EXTERNAL_STORAGE" /> <receiver android:name=".services.UpdateReceiver" diff --git a/app/src/main/kotlin/com/pitchedapps/frost/services/DownloadService.kt b/app/src/main/kotlin/com/pitchedapps/frost/services/DownloadService.kt new file mode 100644 index 00000000..ee0c2027 --- /dev/null +++ b/app/src/main/kotlin/com/pitchedapps/frost/services/DownloadService.kt @@ -0,0 +1,184 @@ +package com.pitchedapps.frost.services + +import android.app.IntentService +import android.app.Notification +import android.app.PendingIntent +import android.app.Service +import android.content.Context +import android.content.Intent +import android.support.v4.app.NotificationCompat +import android.support.v4.app.NotificationManagerCompat +import android.support.v4.content.FileProvider +import ca.allanwang.kau.utils.copyFromInputStream +import ca.allanwang.kau.utils.string +import com.pitchedapps.frost.BuildConfig +import com.pitchedapps.frost.R +import com.pitchedapps.frost.utils.L +import com.pitchedapps.frost.utils.createMediaFile +import okhttp3.MediaType +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.ResponseBody +import okio.* +import org.jetbrains.anko.toast +import java.io.File + +/** + * Created by Allan Wang on 2017-08-08. + * + * Background file downloader + * All we are given is a link and a mime type + */ +class DownloadService : IntentService("FrostVideoDownloader") { + + companion object { + const val EXTRA_URL = "download_url" + private const val MAX_PROGRESS = 1000 + private const val DOWNLOAD_GROUP = "frost_downloads" + } + + val client: OkHttpClient by lazy { initClient() } + + val start = System.currentTimeMillis() + var totalSize = 0L + val downloaded = mutableSetOf<String>() + + private lateinit var notifBuilder: NotificationCompat.Builder + private var notifId: Int = -1 + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + if (intent != null && intent.flags == PendingIntent.FLAG_CANCEL_CURRENT) { + L.i("Cancelling download service") + cancelDownload() + return Service.START_NOT_STICKY + } + return super.onStartCommand(intent, flags, startId) + } + + override fun onHandleIntent(intent: Intent?) { + val url: String = intent?.getStringExtra(EXTRA_URL) ?: return + + if (downloaded.contains(url)) return + + val request: Request = Request.Builder() + .url(url) + .build() + + notifBuilder = frostNotification.quiet + notifId = Math.abs(url.hashCode() + System.currentTimeMillis().toInt()) + val cancelIntent = PendingIntent.getService(this, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT) + + notifBuilder.setContentTitle(string(R.string.downloading_video)) + .setCategory(Notification.CATEGORY_PROGRESS) + .setWhen(System.currentTimeMillis()) + .setProgress(MAX_PROGRESS, 0, false) + .setOngoing(true) + .addAction(R.drawable.ic_action_cancel, string(R.string.kau_cancel), cancelIntent) + .setGroup(DOWNLOAD_GROUP) + + client.newCall(request).execute().use { response -> + if (!response.isSuccessful) { + L.e("Video download failed") + toast("Video download failed") + return@use + } + + val stream = response.body()?.byteStream() ?: return@use + val extension = response.request().body()?.contentType()?.subtype() + val destination = createMediaFile(if (extension == null) "" else ".$extension") + destination.copyFromInputStream(stream) + + notifBuilder.setContentIntent(getPendingIntent(this, destination)) + notifBuilder.show() + } + } + + private fun NotificationCompat.Builder.show() { + NotificationManagerCompat.from(this@DownloadService).notify(DOWNLOAD_GROUP, notifId, build()) + } + + + private fun getPendingIntent(context: Context, file: File): PendingIntent { + val uri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".provider", file) + val type = context.contentResolver.getType(uri) + L.i("DownloadType: retrieved pending intent - $uri $type") + val intent = Intent(Intent.ACTION_VIEW, uri) + .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + .setDataAndType(uri, type) + return PendingIntent.getActivity(context, 0, intent, 0) + } + + /** + * Adds url to downloaded list and modifies the notif builder for the finished state + * Does not show the new notification + */ + private fun finishDownload(url: String) { + L.i("Video download finished", url) + downloaded.add(url) + notifBuilder.setContentTitle(string(R.string.downloaded_video)) + .setProgress(0, 0, false).setOngoing(false).setAutoCancel(true) + .apply { mActions.clear() } + } + + private fun cancelDownload() { + client.dispatcher().cancelAll() + NotificationManagerCompat.from(this).cancel(DOWNLOAD_GROUP, notifId) + } + + private fun onProgressUpdate(url: String, type: MediaType?, percentage: Float, done: Boolean) { + L.v("Download request progress $percentage", url) + notifBuilder.setProgress(MAX_PROGRESS, (percentage * MAX_PROGRESS).toInt(), false) + if (done) finishDownload(url) + notifBuilder.show() + } + + override fun onTaskRemoved(rootIntent: Intent?) { + super.onTaskRemoved(rootIntent) + } + + private fun initClient(): OkHttpClient = OkHttpClient.Builder() + .addNetworkInterceptor { chain -> + val original = chain.proceed(chain.request()) + val body = original.body() ?: return@addNetworkInterceptor original + if (body.contentLength() > 0L) totalSize += body.contentLength() + return@addNetworkInterceptor original.newBuilder() + .body(ProgressResponseBody( + original.request().url().toString(), + body, + this@DownloadService::onProgressUpdate)) + .build() + } + .build() + + private class ProgressResponseBody( + val url: String, + val responseBody: ResponseBody, + val listener: (url: String, type: MediaType?, percentage: Float, done: Boolean) -> Unit) : ResponseBody() { + + private val bufferedSource: BufferedSource by lazy { Okio.buffer(source(responseBody.source())) } + + override fun contentLength(): Long = responseBody.contentLength() + + override fun contentType(): MediaType? = responseBody.contentType() + + override fun source(): BufferedSource = bufferedSource + + private fun source(source: Source): Source = object : ForwardingSource(source) { + + private var totalBytesRead = 0L + + override fun read(sink: Buffer?, byteCount: Long): Long { + val bytesRead = super.read(sink, byteCount) + // read() returns the number of bytes read, or -1 if this source is exhausted. + totalBytesRead += if (bytesRead != -1L) bytesRead else 0 + listener( + url, + contentType(), + totalBytesRead.toFloat() / responseBody.contentLength(), + bytesRead == -1L + ) + return bytesRead + } + } + } +}
\ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/utils/Downloader.kt b/app/src/main/kotlin/com/pitchedapps/frost/utils/Downloader.kt index 60d709fb..566bffde 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/utils/Downloader.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/Downloader.kt @@ -13,6 +13,7 @@ import ca.allanwang.kau.utils.copyFromInputStream import ca.allanwang.kau.utils.string import com.pitchedapps.frost.BuildConfig import com.pitchedapps.frost.R +import com.pitchedapps.frost.services.DownloadService import com.pitchedapps.frost.services.frostNotification import com.pitchedapps.frost.services.getNotificationPendingCancelIntent import com.pitchedapps.frost.services.quiet @@ -22,7 +23,7 @@ import okhttp3.Request import okhttp3.ResponseBody import okio.* import org.jetbrains.anko.AnkoAsyncContext -import org.jetbrains.anko.doAsync +import org.jetbrains.anko.startService import java.io.File import java.io.IOException import java.lang.ref.WeakReference @@ -34,11 +35,10 @@ import java.lang.ref.WeakReference */ fun Context.frostDownload(url: String) { L.d("Received download request", "Download $url") - val type = if (url.contains("video")) DownloadType.VIDEO - else return L.d("Download request does not match any type") - kauRequestPermissions(PERMISSION_WRITE_EXTERNAL_STORAGE) { - granted, _ -> - if (granted) doAsync { frostDownloadImpl(url, type) } + kauRequestPermissions(PERMISSION_WRITE_EXTERNAL_STORAGE) { granted, _ -> + if (granted) +// doAsync { frostDownloadImpl(url, type) } + startService<DownloadService>(DownloadService.EXTRA_URL to url) } } @@ -85,11 +85,9 @@ private fun AnkoAsyncContext<Context>.frostDownloadImpl(url: String, type: Downl var client: OkHttpClient? = null client = OkHttpClient.Builder() - .addNetworkInterceptor { - chain -> + .addNetworkInterceptor { chain -> val original = chain.proceed(chain.request()) - return@addNetworkInterceptor original.newBuilder().body(ProgressResponseBody(original.body()!!) { - bytesRead, contentLength, done -> + return@addNetworkInterceptor original.newBuilder().body(ProgressResponseBody(original.body()!!) { bytesRead, contentLength, done -> //cancel request if context reference is now invalid if (weakRef.get() == null) { client?.cancel(url) @@ -106,8 +104,7 @@ private fun AnkoAsyncContext<Context>.frostDownloadImpl(url: String, type: Downl }).build() } .build() - client.newCall(request).execute().use { - response -> + client.newCall(request).execute().use { response -> if (!response.isSuccessful) throw IOException("Unexpected code $response") val stream = response.body()?.byteStream() if (stream != null) { |