Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Background Uploading Example #140

Merged
merged 30 commits into from
Jun 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
a3bd08a
Upload Service
daytime-em Jun 5, 2024
16fcc3f
up
daytime-em Jun 10, 2024
2f6b3f0
up
daytime-em Jun 10, 2024
371753a
up
daytime-em Jun 10, 2024
fe71894
up
daytime-em Jun 10, 2024
fe9451e
here is more things
daytime-em Jun 10, 2024
480d6c0
Add an isPaused method
daytime-em Jun 10, 2024
61664f5
up
daytime-em Jun 10, 2024
db66d5e
Here we are. now to make the notifs
daytime-em Jun 10, 2024
0defaa1
log notified uploads
daytime-em Jun 11, 2024
0e16b1d
add ui strings
daytime-em Jun 11, 2024
b32240a
Now this is podracing
daytime-em Jun 11, 2024
2453f1d
making progress. get it because its a progress notificatin
daytime-em Jun 11, 2024
7f4ed49
Ok that's all the notif logic
daytime-em Jun 11, 2024
3cbfea8
Hook in upload service
daytime-em Jun 12, 2024
b64f48c
BackgroundUploadService -> UploadNotificationService
daytime-em Jun 12, 2024
95cf251
Now we got one
daytime-em Jun 12, 2024
763c33f
up
daytime-em Jun 12, 2024
d6f43c4
Ok BG uploads are there
daytime-em Jun 12, 2024
175d704
Stop the service also
daytime-em Jun 12, 2024
92ea4e0
Merge remote-tracking branch 'origin/releases/v1.0.0' into bg-uploads
daytime-em Jun 12, 2024
f950e88
some horribleness encountered
daytime-em Jun 12, 2024
704768f
Fixes some progress-reporting bugs
daytime-em Jun 12, 2024
bef3135
one last fix
daytime-em Jun 12, 2024
4cd20e2
Merge branch 'releases/v1.0.0' into bg-uploads
daytime-em Jun 12, 2024
bad7942
better status notifs
daytime-em Jun 12, 2024
2711b68
improve
daytime-em Jun 12, 2024
c6d5813
Merge branch 'bg-uploads' of github.com:muxinc/mux-vod-upload-android…
daytime-em Jun 12, 2024
a4667eb
Done
daytime-em Jun 12, 2024
a1883bd
update
daytime-em Jun 12, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading