aboutsummaryrefslogtreecommitdiff
path: root/app/src/main/kotlin/com/pitchedapps/frost/utils/Downloader.kt
blob: 35f69bca84bc767e64715b798ca638e5f6aad966 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
package com.pitchedapps.frost.utils

import android.app.Notification
import android.app.PendingIntent
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.permissions.PERMISSION_WRITE_EXTERNAL_STORAGE
import ca.allanwang.kau.permissions.kauRequestPermissions
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.frostConfig
import com.pitchedapps.frost.services.frostNotification
import com.pitchedapps.frost.services.getNotificationPendingCancelIntent
import okhttp3.MediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.ResponseBody
import okio.*
import org.jetbrains.anko.AnkoAsyncContext
import org.jetbrains.anko.doAsync
import java.io.File
import java.io.IOException
import java.lang.ref.WeakReference

/**
 * Created by Allan Wang on 2017-08-04.
 *
 * With reference to the <a href="https://github.com/square/okhttp/blob/master/samples/guide/src/main/java/okhttp3/recipes/Progress.java">OkHttp3 sample</a>
 */
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) }
    }
}

private const val MAX_PROGRESS = 1000
//private val DOWNLOAD_GROUP: String? = null
private const val DOWNLOAD_GROUP = "frost_downloads"

private enum class DownloadType(val downloadingRes: Int, val downloadedRes: Int) {
    VIDEO(R.string.downloading_video, R.string.downloaded_video),
    FILE(R.string.downloading_file, R.string.downloaded_file);

    fun getPendingIntent(context: Context, file: File): PendingIntent {
        val uri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".provider", file)
        val type = context.contentResolver.getType(uri)
        L.d("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)
    }
}

private fun AnkoAsyncContext<Context>.frostDownloadImpl(url: String, type: DownloadType) {
    L.d("Starting download request")
    val notifId = Math.abs(url.hashCode() + System.currentTimeMillis().toInt())
    var notifBuilderAttempt: NotificationCompat.Builder? = null
    weakRef.get()?.apply {
        notifBuilderAttempt = frostNotification
                .setContentTitle(string(type.downloadingRes))
                .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), getNotificationPendingCancelIntent(DOWNLOAD_GROUP, notifId))
                .setGroup(DOWNLOAD_GROUP)
    }
    val notifBuilder = notifBuilderAttempt ?: return
    notifBuilder.show(weakRef, notifId)

    val request: Request = Request.Builder()
            .url(url)
            .tag(url)
            .build()

    var client: OkHttpClient? = null
    client = OkHttpClient.Builder()
            .addNetworkInterceptor {
                chain ->
                val original = chain.proceed(chain.request())
                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)
                    }
                    val ctx = weakRef.get() ?: return@ProgressResponseBody client?.cancel(url) ?: Unit
                    val percentage = bytesRead.toFloat() / contentLength.toFloat() * MAX_PROGRESS
                    L.v("Download request progress: $percentage")
                    notifBuilder.setProgress(MAX_PROGRESS, percentage.toInt(), false)
                    if (done) {
                        notifBuilder.setFinished(ctx, type)
                        L.d("Download request finished")
                    }
                    notifBuilder.show(weakRef, notifId)
                }).build()
            }
            .build()
    client.newCall(request).execute().use {
        response ->
        if (!response.isSuccessful) throw IOException("Unexpected code $response")
        val stream = response.body()?.byteStream()
        if (stream != null) {
            val destination = createMediaFile(".mp4")
            destination.copyFromInputStream(stream)
            weakRef.get()?.apply {
                notifBuilder.setContentIntent(type.getPendingIntent(this, destination))
                notifBuilder.show(weakRef, notifId)
            }
        }
    }
}

private fun NotificationCompat.Builder.setFinished(context: Context, type: DownloadType)
        = setContentTitle(context.string(type.downloadedRes))
        .setProgress(0, 0, false).setOngoing(false).setAutoCancel(true)
        .apply { mActions.clear() }

private fun OkHttpClient.cancel(url: String) {
    val call = dispatcher().runningCalls().firstOrNull { it.request().tag() == url }
    if (call != null && !call.isCanceled) call.cancel()
}

private fun NotificationCompat.Builder.show(weakRef: WeakReference<Context>, notifId: Int) {
    val c = weakRef.get() ?: return
    NotificationManagerCompat.from(c).notify(DOWNLOAD_GROUP, notifId, build().frostConfig())
}

private class ProgressResponseBody(
        val responseBody: ResponseBody,
        val listener: (bytesRead: Long, contentLength: Long, 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(totalBytesRead, responseBody.contentLength(), bytesRead == -1L)
            return bytesRead
        }
    }
}