2018-04-14 06:37:21 +10:00
|
|
|
package com.keylesspalace.tusky.service
|
|
|
|
|
2022-08-17 04:08:03 +10:00
|
|
|
import android.app.Notification
|
2018-04-14 06:37:21 +10:00
|
|
|
import android.app.NotificationChannel
|
|
|
|
import android.app.NotificationManager
|
|
|
|
import android.app.PendingIntent
|
|
|
|
import android.app.Service
|
2018-06-29 06:22:29 +10:00
|
|
|
import android.content.ClipData
|
|
|
|
import android.content.ClipDescription
|
2018-04-14 06:37:21 +10:00
|
|
|
import android.content.Context
|
|
|
|
import android.content.Intent
|
|
|
|
import android.os.Build
|
|
|
|
import android.os.IBinder
|
|
|
|
import android.os.Parcelable
|
2022-04-29 04:37:31 +10:00
|
|
|
import android.util.Log
|
2022-08-17 04:08:03 +10:00
|
|
|
import androidx.annotation.StringRes
|
2018-12-18 01:25:35 +11:00
|
|
|
import androidx.core.app.NotificationCompat
|
|
|
|
import androidx.core.app.ServiceCompat
|
2022-05-31 04:03:40 +10:00
|
|
|
import at.connyduck.calladapter.networkresult.fold
|
2022-08-17 04:08:03 +10:00
|
|
|
import com.keylesspalace.tusky.MainActivity
|
2018-04-14 06:37:21 +10:00
|
|
|
import com.keylesspalace.tusky.R
|
2018-05-27 18:22:12 +10:00
|
|
|
import com.keylesspalace.tusky.appstore.EventHub
|
|
|
|
import com.keylesspalace.tusky.appstore.StatusComposedEvent
|
2019-10-03 05:28:12 +10:00
|
|
|
import com.keylesspalace.tusky.appstore.StatusScheduledEvent
|
2021-01-22 04:57:09 +11:00
|
|
|
import com.keylesspalace.tusky.components.drafts.DraftHelper
|
2022-03-10 06:50:23 +11:00
|
|
|
import com.keylesspalace.tusky.components.notifications.NotificationHelper
|
2018-04-14 06:37:21 +10:00
|
|
|
import com.keylesspalace.tusky.db.AccountManager
|
|
|
|
import com.keylesspalace.tusky.di.Injectable
|
2019-08-23 04:30:08 +10:00
|
|
|
import com.keylesspalace.tusky.entity.NewPoll
|
|
|
|
import com.keylesspalace.tusky.entity.NewStatus
|
2018-04-14 06:37:21 +10:00
|
|
|
import com.keylesspalace.tusky.entity.Status
|
|
|
|
import com.keylesspalace.tusky.network.MastodonApi
|
|
|
|
import dagger.android.AndroidInjection
|
2021-06-25 05:23:29 +10:00
|
|
|
import kotlinx.coroutines.CoroutineScope
|
|
|
|
import kotlinx.coroutines.Dispatchers
|
2022-04-29 04:37:31 +10:00
|
|
|
import kotlinx.coroutines.Job
|
2021-06-25 05:23:29 +10:00
|
|
|
import kotlinx.coroutines.SupervisorJob
|
2022-04-03 01:15:18 +11:00
|
|
|
import kotlinx.coroutines.delay
|
2021-06-25 05:23:29 +10:00
|
|
|
import kotlinx.coroutines.launch
|
2021-03-21 22:42:28 +11:00
|
|
|
import kotlinx.parcelize.Parcelize
|
2022-04-29 04:37:31 +10:00
|
|
|
import retrofit2.HttpException
|
2018-04-14 06:37:21 +10:00
|
|
|
import java.util.concurrent.ConcurrentHashMap
|
2018-05-27 18:22:12 +10:00
|
|
|
import java.util.concurrent.TimeUnit
|
2018-04-14 06:37:21 +10:00
|
|
|
import javax.inject.Inject
|
|
|
|
|
2022-03-21 06:21:42 +11:00
|
|
|
class SendStatusService : Service(), Injectable {
|
2018-04-14 06:37:21 +10:00
|
|
|
|
|
|
|
@Inject
|
|
|
|
lateinit var mastodonApi: MastodonApi
|
|
|
|
@Inject
|
|
|
|
lateinit var accountManager: AccountManager
|
2018-05-27 18:22:12 +10:00
|
|
|
@Inject
|
|
|
|
lateinit var eventHub: EventHub
|
|
|
|
@Inject
|
2021-01-22 04:57:09 +11:00
|
|
|
lateinit var draftHelper: DraftHelper
|
2018-04-14 06:37:21 +10:00
|
|
|
|
2021-06-25 05:23:29 +10:00
|
|
|
private val supervisorJob = SupervisorJob()
|
|
|
|
private val serviceScope = CoroutineScope(Dispatchers.Main + supervisorJob)
|
|
|
|
|
2022-03-21 06:21:42 +11:00
|
|
|
private val statusesToSend = ConcurrentHashMap<Int, StatusToSend>()
|
2022-04-29 04:37:31 +10:00
|
|
|
private val sendJobs = ConcurrentHashMap<Int, Job>()
|
2018-04-14 06:37:21 +10:00
|
|
|
|
2018-04-18 04:07:14 +10:00
|
|
|
private val notificationManager by lazy { getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager }
|
2018-04-14 06:37:21 +10:00
|
|
|
|
|
|
|
override fun onCreate() {
|
|
|
|
AndroidInjection.inject(this)
|
|
|
|
super.onCreate()
|
|
|
|
}
|
|
|
|
|
2022-04-29 04:37:31 +10:00
|
|
|
override fun onBind(intent: Intent): IBinder? = null
|
2018-04-14 06:37:21 +10:00
|
|
|
|
|
|
|
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
|
2022-03-21 06:21:42 +11:00
|
|
|
if (intent.hasExtra(KEY_STATUS)) {
|
|
|
|
val statusToSend = intent.getParcelableExtra<StatusToSend>(KEY_STATUS)
|
|
|
|
?: throw IllegalStateException("SendStatusService started without $KEY_STATUS extra")
|
2018-04-14 06:37:21 +10:00
|
|
|
|
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
2022-03-21 06:21:42 +11:00
|
|
|
val channel = NotificationChannel(CHANNEL_ID, getString(R.string.send_post_notification_channel_name), NotificationManager.IMPORTANCE_LOW)
|
2018-04-14 06:37:21 +10:00
|
|
|
notificationManager.createNotificationChannel(channel)
|
|
|
|
}
|
|
|
|
|
2022-03-21 06:21:42 +11:00
|
|
|
var notificationText = statusToSend.warningText
|
2018-04-14 06:37:21 +10:00
|
|
|
if (notificationText.isBlank()) {
|
2022-03-21 06:21:42 +11:00
|
|
|
notificationText = statusToSend.text
|
2018-04-14 06:37:21 +10:00
|
|
|
}
|
|
|
|
|
|
|
|
val builder = NotificationCompat.Builder(this, CHANNEL_ID)
|
2021-06-29 05:13:24 +10:00
|
|
|
.setSmallIcon(R.drawable.ic_notify)
|
2022-03-21 06:21:42 +11:00
|
|
|
.setContentTitle(getString(R.string.send_post_notification_title))
|
2021-06-29 05:13:24 +10:00
|
|
|
.setContentText(notificationText)
|
|
|
|
.setProgress(1, 0, true)
|
|
|
|
.setOngoing(true)
|
2022-08-05 00:48:26 +10:00
|
|
|
.setColor(getColor(R.color.notification_color))
|
2021-06-29 05:13:24 +10:00
|
|
|
.addAction(0, getString(android.R.string.cancel), cancelSendingIntent(sendingNotificationId))
|
2018-04-14 06:37:21 +10:00
|
|
|
|
2022-03-21 06:21:42 +11:00
|
|
|
if (statusesToSend.size == 0 || Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
2018-04-14 06:37:21 +10:00
|
|
|
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_DETACH)
|
2018-04-22 18:37:09 +10:00
|
|
|
startForeground(sendingNotificationId, builder.build())
|
2018-04-14 06:37:21 +10:00
|
|
|
} else {
|
2018-04-22 18:37:09 +10:00
|
|
|
notificationManager.notify(sendingNotificationId, builder.build())
|
2018-04-14 06:37:21 +10:00
|
|
|
}
|
|
|
|
|
2022-03-21 06:21:42 +11:00
|
|
|
statusesToSend[sendingNotificationId] = statusToSend
|
|
|
|
sendStatus(sendingNotificationId--)
|
2018-04-14 06:37:21 +10:00
|
|
|
} else {
|
|
|
|
|
2018-05-27 18:22:12 +10:00
|
|
|
if (intent.hasExtra(KEY_CANCEL)) {
|
2018-04-14 06:37:21 +10:00
|
|
|
cancelSending(intent.getIntExtra(KEY_CANCEL, 0))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return START_NOT_STICKY
|
|
|
|
}
|
|
|
|
|
2022-03-21 06:21:42 +11:00
|
|
|
private fun sendStatus(statusId: Int) {
|
2018-04-14 06:37:21 +10:00
|
|
|
|
2022-03-21 06:21:42 +11:00
|
|
|
// when statusToSend == null, sending has been canceled
|
|
|
|
val statusToSend = statusesToSend[statusId] ?: return
|
2018-04-14 06:37:21 +10:00
|
|
|
|
|
|
|
// when account == null, user has logged out, cancel sending
|
2022-03-21 06:21:42 +11:00
|
|
|
val account = accountManager.getAccountById(statusToSend.accountId)
|
2018-04-14 06:37:21 +10:00
|
|
|
|
2018-05-27 18:22:12 +10:00
|
|
|
if (account == null) {
|
2022-03-21 06:21:42 +11:00
|
|
|
statusesToSend.remove(statusId)
|
|
|
|
notificationManager.cancel(statusId)
|
2018-04-18 04:07:14 +10:00
|
|
|
stopSelfWhenDone()
|
2018-04-14 06:37:21 +10:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2022-03-21 06:21:42 +11:00
|
|
|
statusToSend.retries++
|
2018-04-14 06:37:21 +10:00
|
|
|
|
2022-04-29 04:37:31 +10:00
|
|
|
sendJobs[statusId] = serviceScope.launch {
|
|
|
|
try {
|
|
|
|
var mediaCheckRetries = 0
|
|
|
|
while (statusToSend.mediaProcessed.any { !it }) {
|
|
|
|
delay(1000L * mediaCheckRetries)
|
|
|
|
statusToSend.mediaProcessed.forEachIndexed { index, processed ->
|
|
|
|
if (!processed) {
|
|
|
|
// Mastodon returns 206 if the media was not yet processed
|
|
|
|
statusToSend.mediaProcessed[index] = mastodonApi.getMedia(statusToSend.mediaIds[index]).code() == 200
|
|
|
|
}
|
|
|
|
}
|
|
|
|
mediaCheckRetries ++
|
|
|
|
}
|
|
|
|
} catch (e: Exception) {
|
|
|
|
Log.w(TAG, "failed getting media status", e)
|
|
|
|
retrySending(statusId)
|
|
|
|
return@launch
|
|
|
|
}
|
2019-08-23 04:30:08 +10:00
|
|
|
|
2022-04-29 04:37:31 +10:00
|
|
|
val newStatus = NewStatus(
|
|
|
|
statusToSend.text,
|
|
|
|
statusToSend.warningText,
|
|
|
|
statusToSend.inReplyToId,
|
|
|
|
statusToSend.visibility,
|
|
|
|
statusToSend.sensitive,
|
|
|
|
statusToSend.mediaIds,
|
|
|
|
statusToSend.scheduledAt,
|
|
|
|
statusToSend.poll
|
|
|
|
)
|
|
|
|
|
|
|
|
mastodonApi.createStatus(
|
|
|
|
"Bearer " + account.accessToken,
|
|
|
|
account.domain,
|
|
|
|
statusToSend.idempotencyKey,
|
|
|
|
newStatus
|
|
|
|
).fold({ sentStatus ->
|
|
|
|
statusesToSend.remove(statusId)
|
|
|
|
// If the status was loaded from a draft, delete the draft and associated media files.
|
|
|
|
if (statusToSend.draftId != 0) {
|
|
|
|
draftHelper.deleteDraftAndAttachments(statusToSend.draftId)
|
|
|
|
}
|
2018-04-14 06:37:21 +10:00
|
|
|
|
2022-04-29 04:37:31 +10:00
|
|
|
val scheduled = !statusToSend.scheduledAt.isNullOrEmpty()
|
2018-04-14 06:37:21 +10:00
|
|
|
|
2022-04-29 04:37:31 +10:00
|
|
|
if (scheduled) {
|
|
|
|
eventHub.dispatch(StatusScheduledEvent(sentStatus))
|
|
|
|
} else {
|
|
|
|
eventHub.dispatch(StatusComposedEvent(sentStatus))
|
|
|
|
}
|
2018-04-14 06:37:21 +10:00
|
|
|
|
2022-04-29 04:37:31 +10:00
|
|
|
notificationManager.cancel(statusId)
|
|
|
|
}, { throwable ->
|
|
|
|
Log.w(TAG, "failed sending status", throwable)
|
|
|
|
if (throwable is HttpException) {
|
|
|
|
// the server refused to accept the status, save status & show error message
|
2022-04-03 01:15:18 +11:00
|
|
|
statusesToSend.remove(statusId)
|
2022-04-29 04:37:31 +10:00
|
|
|
saveStatusToDrafts(statusToSend)
|
|
|
|
|
2022-08-17 04:08:03 +10:00
|
|
|
val notification = buildDraftNotification(
|
|
|
|
R.string.send_post_notification_error_title,
|
|
|
|
R.string.send_post_notification_saved_content,
|
|
|
|
statusToSend.accountId,
|
|
|
|
statusId
|
|
|
|
)
|
2018-04-14 06:37:21 +10:00
|
|
|
|
2022-04-29 04:37:31 +10:00
|
|
|
notificationManager.cancel(statusId)
|
2022-08-17 04:08:03 +10:00
|
|
|
notificationManager.notify(errorNotificationId--, notification)
|
2022-04-29 04:37:31 +10:00
|
|
|
} else {
|
|
|
|
// a network problem occurred, let's retry sending the status
|
|
|
|
retrySending(statusId)
|
2018-04-14 06:37:21 +10:00
|
|
|
}
|
2022-04-29 04:37:31 +10:00
|
|
|
})
|
|
|
|
stopSelfWhenDone()
|
|
|
|
}
|
|
|
|
}
|
2018-04-14 06:37:21 +10:00
|
|
|
|
2022-04-29 04:37:31 +10:00
|
|
|
private suspend fun retrySending(statusId: Int) {
|
|
|
|
// when statusToSend == null, sending has been canceled
|
|
|
|
val statusToSend = statusesToSend[statusId] ?: return
|
2018-04-14 06:37:21 +10:00
|
|
|
|
2022-04-29 04:37:31 +10:00
|
|
|
val backoff = TimeUnit.SECONDS.toMillis(statusToSend.retries.toLong()).coerceAtMost(MAX_RETRY_INTERVAL)
|
2018-04-14 06:37:21 +10:00
|
|
|
|
2022-04-29 04:37:31 +10:00
|
|
|
delay(backoff)
|
|
|
|
sendStatus(statusId)
|
2018-04-14 06:37:21 +10:00
|
|
|
}
|
|
|
|
|
2018-04-18 04:07:14 +10:00
|
|
|
private fun stopSelfWhenDone() {
|
2018-04-22 18:37:09 +10:00
|
|
|
|
2022-03-21 06:21:42 +11:00
|
|
|
if (statusesToSend.isEmpty()) {
|
|
|
|
ServiceCompat.stopForeground(this@SendStatusService, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
2018-04-18 04:07:14 +10:00
|
|
|
stopSelf()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-03 01:15:18 +11:00
|
|
|
private fun cancelSending(statusId: Int) = serviceScope.launch {
|
2022-03-21 06:21:42 +11:00
|
|
|
val statusToCancel = statusesToSend.remove(statusId)
|
|
|
|
if (statusToCancel != null) {
|
2022-04-29 04:37:31 +10:00
|
|
|
val sendJob = sendJobs.remove(statusId)
|
|
|
|
sendJob?.cancel()
|
2018-04-14 06:37:21 +10:00
|
|
|
|
2022-03-21 06:21:42 +11:00
|
|
|
saveStatusToDrafts(statusToCancel)
|
2018-04-14 06:37:21 +10:00
|
|
|
|
2022-08-17 04:08:03 +10:00
|
|
|
val notification = buildDraftNotification(
|
|
|
|
R.string.send_post_notification_cancel_title,
|
|
|
|
R.string.send_post_notification_saved_content,
|
|
|
|
statusToCancel.accountId,
|
|
|
|
statusId
|
|
|
|
)
|
2018-04-14 06:37:21 +10:00
|
|
|
|
2022-08-17 04:08:03 +10:00
|
|
|
notificationManager.notify(statusId, notification)
|
2018-04-14 06:37:21 +10:00
|
|
|
|
2022-04-03 01:15:18 +11:00
|
|
|
delay(5000)
|
2022-08-17 04:08:03 +10:00
|
|
|
|
|
|
|
stopSelfWhenDone()
|
2018-04-14 06:37:21 +10:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-03 01:15:18 +11:00
|
|
|
private suspend fun saveStatusToDrafts(status: StatusToSend) {
|
|
|
|
draftHelper.saveDraft(
|
|
|
|
draftId = status.draftId,
|
|
|
|
accountId = status.accountId,
|
|
|
|
inReplyToId = status.inReplyToId,
|
|
|
|
content = status.text,
|
|
|
|
contentWarning = status.warningText,
|
|
|
|
sensitive = status.sensitive,
|
|
|
|
visibility = Status.Visibility.byString(status.visibility),
|
|
|
|
mediaUris = status.mediaUris,
|
|
|
|
mediaDescriptions = status.mediaDescriptions,
|
|
|
|
poll = status.poll,
|
2022-07-28 05:06:51 +10:00
|
|
|
failedToSend = true,
|
|
|
|
scheduledAt = status.scheduledAt
|
2022-04-03 01:15:18 +11:00
|
|
|
)
|
2018-04-14 06:37:21 +10:00
|
|
|
}
|
|
|
|
|
2022-03-21 06:21:42 +11:00
|
|
|
private fun cancelSendingIntent(statusId: Int): PendingIntent {
|
|
|
|
val intent = Intent(this, SendStatusService::class.java)
|
|
|
|
intent.putExtra(KEY_CANCEL, statusId)
|
2022-08-17 04:08:03 +10:00
|
|
|
return PendingIntent.getService(
|
|
|
|
this,
|
|
|
|
statusId,
|
|
|
|
intent,
|
|
|
|
NotificationHelper.pendingIntentFlags(false)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun buildDraftNotification(
|
|
|
|
@StringRes title: Int,
|
|
|
|
@StringRes content: Int,
|
|
|
|
accountId: Long,
|
|
|
|
statusId: Int
|
|
|
|
): Notification {
|
|
|
|
|
|
|
|
val intent = Intent(this, MainActivity::class.java)
|
|
|
|
intent.putExtra(NotificationHelper.ACCOUNT_ID, accountId)
|
|
|
|
intent.putExtra(MainActivity.OPEN_DRAFTS, true)
|
|
|
|
|
|
|
|
val pendingIntent = PendingIntent.getActivity(
|
|
|
|
this,
|
|
|
|
statusId,
|
|
|
|
intent,
|
|
|
|
NotificationHelper.pendingIntentFlags(false)
|
|
|
|
)
|
|
|
|
|
|
|
|
return NotificationCompat.Builder(this@SendStatusService, CHANNEL_ID)
|
|
|
|
.setSmallIcon(R.drawable.ic_notify)
|
|
|
|
.setContentTitle(getString(title))
|
|
|
|
.setContentText(getString(content))
|
|
|
|
.setColor(getColor(R.color.notification_color))
|
|
|
|
.setAutoCancel(true)
|
|
|
|
.setOngoing(false)
|
|
|
|
.setContentIntent(pendingIntent)
|
|
|
|
.build()
|
2018-04-14 06:37:21 +10:00
|
|
|
}
|
|
|
|
|
2021-06-25 05:23:29 +10:00
|
|
|
override fun onDestroy() {
|
|
|
|
super.onDestroy()
|
|
|
|
supervisorJob.cancel()
|
|
|
|
}
|
2018-04-14 06:37:21 +10:00
|
|
|
|
|
|
|
companion object {
|
2022-04-29 04:37:31 +10:00
|
|
|
private const val TAG = "SendStatusService"
|
2018-04-14 06:37:21 +10:00
|
|
|
|
2022-03-21 06:21:42 +11:00
|
|
|
private const val KEY_STATUS = "status"
|
2018-04-14 06:37:21 +10:00
|
|
|
private const val KEY_CANCEL = "cancel_id"
|
|
|
|
private const val CHANNEL_ID = "send_toots"
|
|
|
|
|
2018-05-27 18:22:12 +10:00
|
|
|
private val MAX_RETRY_INTERVAL = TimeUnit.MINUTES.toMillis(1)
|
2018-04-14 06:37:21 +10:00
|
|
|
|
2018-04-22 18:37:09 +10:00
|
|
|
private var sendingNotificationId = -1 // use negative ids to not clash with other notis
|
|
|
|
private var errorNotificationId = Int.MIN_VALUE // use even more negative ids to not clash with other notis
|
2018-04-14 06:37:21 +10:00
|
|
|
|
2022-03-21 06:21:42 +11:00
|
|
|
fun sendStatusIntent(
|
2021-06-29 05:13:24 +10:00
|
|
|
context: Context,
|
2022-03-21 06:21:42 +11:00
|
|
|
statusToSend: StatusToSend
|
2018-04-14 06:37:21 +10:00
|
|
|
): Intent {
|
2022-03-21 06:21:42 +11:00
|
|
|
val intent = Intent(context, SendStatusService::class.java)
|
|
|
|
intent.putExtra(KEY_STATUS, statusToSend)
|
2018-04-14 06:37:21 +10:00
|
|
|
|
2022-03-21 06:21:42 +11:00
|
|
|
if (statusToSend.mediaUris.isNotEmpty()) {
|
2018-06-29 06:22:29 +10:00
|
|
|
// forward uri permissions
|
|
|
|
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
|
|
|
val uriClip = ClipData(
|
2022-03-21 06:21:42 +11:00
|
|
|
ClipDescription("Status Media", arrayOf("image/*", "video/*")),
|
|
|
|
ClipData.Item(statusToSend.mediaUris[0])
|
2018-06-29 06:22:29 +10:00
|
|
|
)
|
2022-03-21 06:21:42 +11:00
|
|
|
statusToSend.mediaUris
|
2021-06-29 05:13:24 +10:00
|
|
|
.drop(1)
|
|
|
|
.forEach { mediaUri ->
|
|
|
|
uriClip.addItem(ClipData.Item(mediaUri))
|
|
|
|
}
|
2018-06-29 06:22:29 +10:00
|
|
|
|
|
|
|
intent.clipData = uriClip
|
|
|
|
}
|
|
|
|
|
2018-04-14 06:37:21 +10:00
|
|
|
return intent
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@Parcelize
|
2022-03-21 06:21:42 +11:00
|
|
|
data class StatusToSend(
|
2021-06-29 05:13:24 +10:00
|
|
|
val text: String,
|
|
|
|
val warningText: String,
|
|
|
|
val visibility: String,
|
|
|
|
val sensitive: Boolean,
|
|
|
|
val mediaIds: List<String>,
|
|
|
|
val mediaUris: List<String>,
|
|
|
|
val mediaDescriptions: List<String>,
|
|
|
|
val scheduledAt: String?,
|
|
|
|
val inReplyToId: String?,
|
|
|
|
val poll: NewPoll?,
|
|
|
|
val replyingStatusContent: String?,
|
|
|
|
val replyingStatusAuthorUsername: String?,
|
|
|
|
val accountId: Long,
|
|
|
|
val draftId: Int,
|
|
|
|
val idempotencyKey: String,
|
2022-04-29 04:37:31 +10:00
|
|
|
var retries: Int,
|
|
|
|
val mediaProcessed: MutableList<Boolean>
|
2019-12-20 05:09:40 +11:00
|
|
|
) : Parcelable
|