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

Releases/v1.0.0 #141

Merged
merged 9 commits into from
Jun 13, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
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.9'
kotlinCompilerExtensionVersion '1.5.14'
}
packagingOptions {
resources {
Expand Down
14 changes: 13 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,21 @@
<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" />

<application
android:name=".UploadExampleApp"
Expand Down Expand Up @@ -51,6 +58,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)
}
}
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