aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAllan Wang <me@allanwang.ca>2017-09-16 21:40:03 -0400
committerGitHub <noreply@github.com>2017-09-16 21:40:03 -0400
commit86838453bb1d712cd9ebe580333e188a02b60f5a (patch)
treef3ce71a01e37f2a86ade1fb4efd7fe41941fa077
parente1835f066903a3ff69215e49c8cf0320657ebce0 (diff)
downloadfrost-86838453bb1d712cd9ebe580333e188a02b60f5a.tar.gz
frost-86838453bb1d712cd9ebe580333e188a02b60f5a.tar.bz2
frost-86838453bb1d712cd9ebe580333e188a02b60f5a.zip
Feature/video downloader (#270)
* Cherry pick old branch * Revert to intent service
-rw-r--r--app/src/main/AndroidManifest.xml5
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/services/DownloadService.kt184
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/utils/Downloader.kt21
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) {