diff --git a/app/build.gradle b/app/build.gradle
index f012f715..89bab66a 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -38,7 +38,7 @@ android {
compose true
}
composeOptions {
- kotlinCompilerExtensionVersion '1.5.10'
+ kotlinCompilerExtensionVersion '1.5.14'
}
packagingOptions {
resources {
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index f0e072e7..72052409 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -2,6 +2,12 @@
+
+
+ android:maxSdkVersion="28" />
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/com/mux/video/vod/demo/UploadExampleApp.kt b/app/src/main/java/com/mux/video/vod/demo/UploadExampleApp.kt
index 828c7ea6..b603c1d4 100644
--- a/app/src/main/java/com/mux/video/vod/demo/UploadExampleApp.kt
+++ b/app/src/main/java/com/mux/video/vod/demo/UploadExampleApp.kt
@@ -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)
}
}
diff --git a/app/src/main/java/com/mux/video/vod/demo/UploadNotificationService.kt b/app/src/main/java/com/mux/video/vod/demo/UploadNotificationService.kt
new file mode 100644
index 00000000..5b7842f0
--- /dev/null
+++ b/app/src/main/java/com/mux/video/vod/demo/UploadNotificationService.kt
@@ -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()
+
+ 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) {
+ 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) {
+ // 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> {
+ override fun onEvent(event: List) {
+ val service = this@UploadNotificationService
+ service.updateCurrentUploads(event)
+ service.notifyWithCurrentUploads()
+ }
+ }
+
+ private inner class UploadStatusListener : UploadEventListener {
+ override fun onEvent(event: UploadStatus) {
+ val service = this@UploadNotificationService
+ service.notifyWithCurrentUploads()
+ }
+ }
+
+ private inner class MyBinder : Binder() {
+ fun getService(): UploadNotificationService = this@UploadNotificationService
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/mux/video/vod/demo/backend/ImaginaryBackend.kt b/app/src/main/java/com/mux/video/vod/demo/backend/ImaginaryBackend.kt
index c8570760..d02bfe34 100644
--- a/app/src/main/java/com/mux/video/vod/demo/backend/ImaginaryBackend.kt
+++ b/app/src/main/java/com/mux/video/vod/demo/backend/ImaginaryBackend.kt
@@ -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 {
diff --git a/app/src/main/java/com/mux/video/vod/demo/upload/screen/UploadListScreen.kt b/app/src/main/java/com/mux/video/vod/demo/upload/screen/UploadListScreen.kt
index 39d5f7b5..429657a7 100644
--- a/app/src/main/java/com/mux/video/vod/demo/upload/screen/UploadListScreen.kt
+++ b/app/src/main/java/com/mux/video/vod/demo/upload/screen/UploadListScreen.kt
@@ -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
@@ -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(
diff --git a/app/src/main/java/com/mux/video/vod/demo/upload/viewmodel/CreateUploadViewModel.kt b/app/src/main/java/com/mux/video/vod/demo/upload/viewmodel/CreateUploadViewModel.kt
index a0588743..ae710302 100644
--- a/app/src/main/java/com/mux/video/vod/demo/upload/viewmodel/CreateUploadViewModel.kt
+++ b/app/src/main/java/com/mux/video/vod/demo/upload/viewmodel/CreateUploadViewModel.kt
@@ -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
@@ -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
@@ -67,6 +67,8 @@ 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)
}
}
@@ -74,7 +76,6 @@ class CreateUploadViewModel(private val app: Application) : AndroidViewModel(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 Is this something that should go in the SDK? This is a common workflow
*/
@Throws
private suspend fun copyIntoTempFile(contentUri: Uri): File {
diff --git a/app/src/main/java/com/mux/video/vod/demo/upload/viewmodel/UploadListViewModel.kt b/app/src/main/java/com/mux/video/vod/demo/upload/viewmodel/UploadListViewModel.kt
index 89d45b22..faed87a8 100644
--- a/app/src/main/java/com/mux/video/vod/demo/upload/viewmodel/UploadListViewModel.kt
+++ b/app/src/main/java/com/mux/video/vod/demo/upload/viewmodel/UploadListViewModel.kt
@@ -1,6 +1,7 @@
package com.mux.video.vod.demo.upload.viewmodel
import android.app.Application
+import android.util.Log
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
@@ -21,8 +22,9 @@ class UploadListViewModel(app: Application) : AndroidViewModel(app) {
private val uploadMap = mutableMapOf()
private val listUpdateListener: UploadEventListener> by lazy {
- UploadEventListener { newUploads ->
- newUploads.forEach { uploadMap[it.videoFile] = it }
+ UploadEventListener { uploads ->
+ //uploadMap.forEach { entry -> entry.value.clearListeners() }
+ observeUploads(uploads)
updateUiData(uploadMap.values.toList())
}
}
@@ -48,12 +50,7 @@ class UploadListViewModel(app: Application) : AndroidViewModel(app) {
}
private fun observeUploads(recentUploads: List) {
- recentUploads.forEach { upload ->
- upload.setProgressListener {
- uploadMap[upload.videoFile] = upload
- updateUiData(uploadMap.values.toList())
- }
- } // recentUploads.forEach
+ recentUploads.forEach { upload -> uploadMap[upload.videoFile] = upload }
}
private fun updateUiData(list: List) {
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index edfc851e..395b05bf 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,9 +1,35 @@
- Mux Video VOD Upload SDK
+ Mux Upload Demo
Mux Upload
Plain-View Mux Upload Example
Settings
Pause Button
Create New Upload
Upload!
-
\ No newline at end of file
+
+ Upload status notifications
+ Progress Notifications for uploads moving in the background
+
+ Successfully uploaded %1$d videos
+
+ - Uploading video
+ - Uploading %1$d videos
+
+
+ - Upload Failed
+ - %1$d uploads failed
+
+
+ - Upload succeeded
+ - Successfully uploaded %1$d videos
+
+
+ - Uploading your video
+ - Uploading videos.. (%1$d to go)
+
+
+ - Upload Failed
+ - %1$d uploads failed
+
+
+
diff --git a/library/src/main/java/com/mux/video/upload/api/MuxUpload.kt b/library/src/main/java/com/mux/video/upload/api/MuxUpload.kt
index c6c5bb0c..d94c2786 100644
--- a/library/src/main/java/com/mux/video/upload/api/MuxUpload.kt
+++ b/library/src/main/java/com/mux/video/upload/api/MuxUpload.kt
@@ -1,6 +1,7 @@
package com.mux.video.upload.api
import android.net.Uri
+import android.util.Log
import androidx.annotation.MainThread
import com.mux.video.upload.MuxUploadSdk
import com.mux.video.upload.api.MuxUpload.Builder
@@ -9,6 +10,7 @@ import com.mux.video.upload.internal.MaximumResolution
import com.mux.video.upload.internal.UploadInfo
import com.mux.video.upload.internal.update
import kotlinx.coroutines.*
+import kotlinx.coroutines.flow.distinctUntilChangedBy
import java.io.File
/**
@@ -64,6 +66,11 @@ class MuxUpload private constructor(
*/
val isRunning get() = uploadInfo.isRunning()
+ /**
+ * True when the upload is paused by [pause], false otherwise
+ */
+ val isPaused get() = currentStatus is UploadStatus.UploadPaused
+
/**
* If the upload has failed, gets the error associated with the failure
*/
@@ -73,22 +80,25 @@ class MuxUpload private constructor(
/**
* True if the upload was successful, false otherwise
*/
- val isSuccessful get() = _successful
+ val isSuccessful get() = uploadInfo.statusFlow?.value?.isSuccessful() ?: _successful
private var _successful: Boolean = false
private var resultListener: UploadEventListener>? = null
private var progressListener: UploadEventListener