Skip to content

Commit

Permalink
Background Uploading Example (#140)
Browse files Browse the repository at this point in the history
* Upload Service

* up

* up

* up

* up

* here is more things

* Add an isPaused method

* up

* Here we are. now to make the notifs

* log notified uploads

* add ui strings

* Now this is podracing

* making progress. get it because its a progress notificatin

* Ok that's all the notif logic

* Hook in upload service

* BackgroundUploadService -> UploadNotificationService

* Now we got one

* up

* Ok BG uploads are there

* Stop the service also

* some horribleness encountered

* Fixes some progress-reporting bugs

* one last fix

* better status notifs

* improve

* Done

* update
  • Loading branch information
daytime-em authored Jun 12, 2024
1 parent 93a9b39 commit 96964b7
Show file tree
Hide file tree
Showing 11 changed files with 370 additions and 49 deletions.
2 changes: 1 addition & 1 deletion app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ android {
compose true
}
composeOptions {
kotlinCompilerExtensionVersion '1.5.10'
kotlinCompilerExtensionVersion '1.5.14'
}
packagingOptions {
resources {
Expand Down
16 changes: 15 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,23 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<!--
todo: New/Optional Stuff from U and V
* Selected Photos Access
* Media Processing FG Service type (w/mandatory timeout)
-->

<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="29" />
android:maxSdkVersion="28" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<!-- todo dont need -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

<application
android:name=".UploadExampleApp"
Expand Down Expand Up @@ -51,6 +60,11 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service android:name=".UploadNotificationService"
android:exported="false"
android:foregroundServiceType="dataSync"
>
</service>
</application>

</manifest>
23 changes: 23 additions & 0 deletions app/src/main/java/com/mux/video/vod/demo/UploadExampleApp.kt
Original file line number Diff line number Diff line change
@@ -1,11 +1,34 @@
package com.mux.video.vod.demo

import android.annotation.TargetApi
import android.app.Application
import android.app.NotificationChannel
import android.app.NotificationManager
import android.os.Build
import com.mux.video.upload.MuxUploadSdk
import com.mux.video.upload.api.MuxUploadManager

class UploadExampleApp : Application() {

override fun onCreate() {
super.onCreate()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createNotificationChannel()
}
MuxUploadSdk.initialize(this)
if (MuxUploadManager.allUploadJobs().isNotEmpty()) {
UploadNotificationService.startCompat(this)
}
}

@TargetApi(Build.VERSION_CODES.O)
private fun createNotificationChannel() {
val channel = NotificationChannel(
UploadNotificationService.CHANNEL_UPLOAD_PROGRESS,
getString(R.string.notif_channel_name),
NotificationManager.IMPORTANCE_LOW
)
channel.description = getString(R.string.notif_channel_desc)
getSystemService(NotificationManager::class.java).createNotificationChannel(channel)
}
}
221 changes: 221 additions & 0 deletions app/src/main/java/com/mux/video/vod/demo/UploadNotificationService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
package com.mux.video.vod.demo

import android.annotation.SuppressLint
import android.annotation.TargetApi
import android.app.Service
import android.content.Context
import android.content.Intent
import android.content.pm.ServiceInfo
import android.os.Binder
import android.os.Build
import android.os.IBinder
import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.core.app.ServiceCompat
import com.mux.video.upload.api.MuxUpload
import com.mux.video.upload.api.MuxUploadManager
import com.mux.video.upload.api.UploadEventListener
import com.mux.video.upload.api.UploadStatus

/**
* Service that monitors ongoing [MuxUpload]s, showing progress notifications for them. This
* service will enter the foreground whenever there are uploads in progress and will exit foreground
* and stop itself when there are no more uploads in progress (ie, all have completed, paused, or
* failed)
*/
class UploadNotificationService : Service() {

companion object {
private const val TAG = "BackgroundUploadService"

const val ACTION_START = "start"
const val NOTIFICATION_FG = 200002
const val CHANNEL_UPLOAD_PROGRESS = "upload_progress"

fun startCompat(context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startImplApiO(context)
} else {
startImplLegacy(context)
}
}

@TargetApi(Build.VERSION_CODES.O)
private fun startImplApiO(context: Context) {
val startIntent = Intent(context, UploadNotificationService::class.java)
startIntent.action = ACTION_START
context.startForegroundService(startIntent)
}

private fun startImplLegacy(context: Context) {
val startIntent = Intent(context, UploadNotificationService::class.java)
startIntent.action = ACTION_START
context.startService(startIntent)
}
}

private var uploadListListener: UploadListListener? = null
// uploads tracked by this Service, regardless of state. cleared when the service is destroyed
private val uploadsByFile = mutableMapOf<String, MuxUpload>()

override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val action = intent?.action
if (action != ACTION_START) {
throw RuntimeException("Unknown action")
}

// can be commanded to start arbitrary number of times
if (uploadListListener == null) {
notify(MuxUploadManager.allUploadJobs())

val lis = UploadListListener()
this.uploadListListener = lis
MuxUploadManager.addUploadsUpdatedListener(lis)
}

return super.onStartCommand(intent, flags, startId)
}

override fun onBind(intent: Intent?): IBinder? {
return MyBinder()
}

override fun onDestroy() {
uploadListListener?.let { MuxUploadManager.removeUploadsUpdatedListener(it) }
}

private fun notifyWithCurrentUploads() = notify(this.uploadsByFile.values)

@SuppressLint("InlinedApi", "MissingPermission") // inline use of FOREGROUND_SERVICE
private fun notify(uploads: Collection<MuxUpload>) {
if (uploads.isEmpty()) {
// only notify if there are uploads being tracked (in-progress or finished)
return
}

val uploadsInProgress = uploads.filter { it.isRunning }
val uploadsCompleted = uploads.filter { it.isSuccessful }
val uploadsFailed = uploads.filter { it.error != null }

Log.v(TAG, "notify: uploadsInProgress: ${uploadsInProgress.size}")
Log.v(TAG, "notify: uploadsCompleted: ${uploadsCompleted.size}")
Log.v(TAG, "notify: uploadsFailed: ${uploadsFailed.size}")

val builder = NotificationCompat.Builder(this, CHANNEL_UPLOAD_PROGRESS)
builder.setSmallIcon(R.drawable.ic_launcher)
builder.setAutoCancel(false)
builder.setOngoing(true)

if (uploadsInProgress.isNotEmpty()) {
Log.d(TAG, "notifying progress")
if (uploadsInProgress.size == 1 && this.uploadsByFile.size == 1) {
// Special case: A single upload in progress, with a single upload requested
val upload = uploadsInProgress.first()
val kbUploaded = (upload.currentProgress.bytesUploaded / 1024).toInt()
val kbTotal = (upload.currentProgress.totalBytes / 1024).toInt()

Log.d(TAG, "upload state: ${upload.uploadStatus}")

builder.setProgress(kbTotal, kbUploaded, false)
builder.setContentText(
resources.getQuantityString(
R.plurals.notif_txt_uploading, 1, 1, 1
)
)
builder.setContentTitle(
resources.getQuantityString(
R.plurals.notif_title_uploading, 1, 1
)
)
} else {
// Multiple uploads requested simultaneously so we batch them into one
val totalKbUploaded = uploadsInProgress.sumOf { it.currentProgress.bytesUploaded / 1024 }
val totalKb = uploadsInProgress.sumOf { it.currentProgress.totalBytes / 1024 }

builder.setProgress(totalKb.toInt(),totalKbUploaded.toInt(), false)
builder.setContentText(
resources.getQuantityString(
R.plurals.notif_txt_uploading,
uploads.size,
uploadsInProgress.size,
)
)
builder.setContentTitle(
resources.getQuantityString(
R.plurals.notif_title_uploading, uploads.size,
uploads.size
)
)
}
} else if (uploadsFailed.isNotEmpty()) {
Log.i(TAG, "notifying Fail")
builder.setContentTitle(
resources.getQuantityString(
R.plurals.notif_title_failed,
uploadsFailed.size,
uploadsFailed.size
)
)
builder.setContentText(
resources.getQuantityString(
R.plurals.notif_txt_failed,
uploadsFailed.size,
uploadsFailed.size
)
)
} else if (uploadsCompleted.isNotEmpty()) {
Log.i(TAG, "notifying Complete")
builder.setContentText(getString(R.string.notif_txt_success))
builder.setContentTitle(
resources.getQuantityString(
R.plurals.notif_title_success,
uploadsCompleted.size,
uploadsCompleted.size,
)
)
}

// always startForeground even if we're about to detach (to update the notification)
ServiceCompat.startForeground(
this,
NOTIFICATION_FG,
builder.build(),
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
)

if (uploadsInProgress.isEmpty()) {
// we only need foreground/to even be running while uploads are actually running
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_DETACH)
stopSelf()
}
}

private fun updateCurrentUploads(incomingUploads: List<MuxUpload>) {
// listen to status of new uploads
incomingUploads
.filter { !this.uploadsByFile.containsKey(it.videoFile.path) }
.forEach {
this.uploadsByFile[it.videoFile.path] = it
it.setStatusListener(UploadStatusListener())
}
}

private inner class UploadListListener : UploadEventListener<List<MuxUpload>> {
override fun onEvent(event: List<MuxUpload>) {
val service = this@UploadNotificationService
service.updateCurrentUploads(event)
service.notifyWithCurrentUploads()
}
}

private inner class UploadStatusListener : UploadEventListener<UploadStatus> {
override fun onEvent(event: UploadStatus) {
val service = this@UploadNotificationService
service.notifyWithCurrentUploads()
}
}

private inner class MyBinder : Binder() {
fun getService(): UploadNotificationService = this@UploadNotificationService
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ object ImaginaryBackend {
// note: You shouldn't do basic auth with hard-coded keys in a real app
private fun basicCredential(): String = Credentials.basic(ACCESS_TOKEN_ID, ACCESS_TOKEN_SECRET)

private const val ACCESS_TOKEN_ID = "YOUR TOKEN ID HERE"
private const val ACCESS_TOKEN_SECRET = "YOUR TOKEN SECRET HERE"
private const val ACCESS_TOKEN_ID = "YOUR ACCESS TOKEN ID HERE"
private const val ACCESS_TOKEN_SECRET = "YOUR ACCESS TOKEN SECRET HERE"
}

private interface ImaginaryWebapp {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.mux.video.upload.api.MuxUpload
import com.mux.video.upload.api.UploadStatus
import com.mux.video.vod.demo.R
import com.mux.video.vod.demo.upload.CreateUploadActivity
import com.mux.video.vod.demo.upload.CreateUploadCta
Expand Down Expand Up @@ -175,10 +176,13 @@ private fun ListItemContent(upload: MuxUpload) {
}
}

val uploadState = remember { mutableStateOf(upload.uploadStatus) }
upload.setStatusListener { uploadState.value = it }

ListThumbnail(bitmap = bitmap)
if (upload.isSuccessful) {
if (uploadState.value.isSuccessful()) {
DoneOverlay()
} else if (upload.error != null) {
} else if (uploadState.value.getError() != null) {
ErrorOverlay(modifier = Modifier.fillMaxSize())
} else if (upload.isRunning) {
ProgressOverlay(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package com.mux.video.vod.demo.upload.viewmodel
import android.app.Application
import android.database.Cursor
import android.graphics.Bitmap
import android.media.MediaMetadataRetriever
import android.net.Uri
import android.os.Build
import android.provider.MediaStore
Expand All @@ -13,6 +12,7 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import com.mux.video.upload.api.MuxUpload
import com.mux.video.vod.demo.UploadNotificationService
import com.mux.video.vod.demo.backend.ImaginaryBackend
import com.mux.video.vod.demo.upload.model.MediaStoreVideo
import com.mux.video.vod.demo.upload.model.extractThumbnail
Expand Down Expand Up @@ -67,14 +67,15 @@ class CreateUploadViewModel(private val app: Application) : AndroidViewModel(app
).build()
// Force restart when creating brand new uploads (because we're making new Direct uploads)
.start(forceRestart = true)

UploadNotificationService.startCompat(app)
}
}

/**
* In order to upload a file from the device's media store, the file must be copied into the app's
* temp directory. (Technically we could stream it from the source, but this prevents the other
* app from modifying the file if we pause the upload for a long time or whatever)
* TODO<em> Is this something that should go in the SDK? This is a common workflow
*/
@Throws
private suspend fun copyIntoTempFile(contentUri: Uri): File {
Expand Down
Loading

0 comments on commit 96964b7

Please sign in to comment.