diff options
Diffstat (limited to 'app/src/main/kotlin/com/pitchedapps/frost/utils')
-rw-r--r-- | app/src/main/kotlin/com/pitchedapps/frost/utils/Downloader.kt | 163 | ||||
-rw-r--r-- | app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt | 26 |
2 files changed, 188 insertions, 1 deletions
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/utils/Downloader.kt b/app/src/main/kotlin/com/pitchedapps/frost/utils/Downloader.kt new file mode 100644 index 00000000..35f69bca --- /dev/null +++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/Downloader.kt @@ -0,0 +1,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 + } + } +}
\ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt b/app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt index cc3ea52e..496a6b5b 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt @@ -2,8 +2,10 @@ package com.pitchedapps.frost.utils import android.app.Activity import android.content.Context +import android.content.Intent import android.graphics.Color import android.graphics.drawable.ColorDrawable +import android.net.Uri import android.support.annotation.StringRes import android.support.design.internal.SnackbarContentLayout import android.support.design.widget.Snackbar @@ -22,8 +24,11 @@ import com.pitchedapps.frost.BuildConfig import com.pitchedapps.frost.R import com.pitchedapps.frost.activities.* import com.pitchedapps.frost.dbflow.CookieModel +import com.pitchedapps.frost.facebook.FACEBOOK_COM import com.pitchedapps.frost.facebook.FbTab import com.pitchedapps.frost.facebook.formattedFbUrl +import java.io.IOException +import java.util.* /** * Created by Allan Wang on 2017-06-03. @@ -145,4 +150,23 @@ fun Activity.frostNavigationBar() { navigationBarColor = if (Prefs.tintNavBar) Prefs.headerColor else Color.BLACK } -fun <T> RequestBuilder<T>.withRoundIcon() = apply(RequestOptions().transform(CircleCrop()))!!
\ No newline at end of file +fun <T> RequestBuilder<T>.withRoundIcon() = apply(RequestOptions().transform(CircleCrop()))!! + +@Throws(IOException::class) +fun createMediaFile(extension: String) = createMediaFile("Frost", extension) + +@Throws(IOException::class) +fun Context.createPrivateMediaFile(extension: String) = createPrivateMediaFile("Frost", extension) + +/** + * Tries to send the uri to the proper activity via an intent + * @returns {@code true} if activity is resolved, {@code false} otherwise + */ +fun Context.resolveActivityForUri(uri: Uri): Boolean { + if (uri.toString().contains(FACEBOOK_COM) && !uri.toString().contains("intent:")) return false //ignore response as we will be triggering ourself + val intent = Intent(Intent.ACTION_VIEW, uri) + if (intent.resolveActivity(packageManager) == null) return false + startActivity(intent) + return true +} + |