2018-04-14 06:37:21 +10:00
|
|
|
package com.keylesspalace.tusky.service
|
|
|
|
|
|
|
|
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
|
2018-06-29 06:22:29 +10:00
|
|
|
import android.net.Uri
|
2018-04-14 06:37:21 +10:00
|
|
|
import android.os.Build
|
|
|
|
import android.os.IBinder
|
|
|
|
import android.os.Parcelable
|
2018-12-18 01:25:35 +11:00
|
|
|
import androidx.core.app.NotificationCompat
|
|
|
|
import androidx.core.app.ServiceCompat
|
|
|
|
import androidx.core.content.ContextCompat
|
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
|
2018-04-14 06:37:21 +10:00
|
|
|
import com.keylesspalace.tusky.db.AccountEntity
|
|
|
|
import com.keylesspalace.tusky.db.AccountManager
|
2018-05-27 18:22:12 +10:00
|
|
|
import com.keylesspalace.tusky.db.AppDatabase
|
2018-04-14 06:37:21 +10:00
|
|
|
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 com.keylesspalace.tusky.util.SaveTootHelper
|
2019-02-01 05:03:34 +11:00
|
|
|
import com.keylesspalace.tusky.util.randomAlphanumericString
|
2018-04-14 06:37:21 +10:00
|
|
|
import dagger.android.AndroidInjection
|
|
|
|
import kotlinx.android.parcel.Parcelize
|
|
|
|
import retrofit2.Call
|
|
|
|
import retrofit2.Callback
|
|
|
|
import retrofit2.Response
|
|
|
|
import java.util.*
|
|
|
|
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
|
|
|
|
|
2018-05-27 18:22:12 +10:00
|
|
|
class SendTootService : 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
|
|
|
|
lateinit var database: AppDatabase
|
2018-04-14 06:37:21 +10:00
|
|
|
|
|
|
|
private lateinit var saveTootHelper: SaveTootHelper
|
|
|
|
|
|
|
|
private val tootsToSend = ConcurrentHashMap<Int, TootToSend>()
|
|
|
|
private val sendCalls = ConcurrentHashMap<Int, Call<Status>>()
|
|
|
|
|
|
|
|
private val timer = Timer()
|
|
|
|
|
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)
|
2018-05-27 18:22:12 +10:00
|
|
|
saveTootHelper = SaveTootHelper(database.tootDao(), this)
|
2018-04-14 06:37:21 +10:00
|
|
|
super.onCreate()
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onBind(intent: Intent): IBinder? {
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
|
|
|
|
|
2018-05-27 18:22:12 +10:00
|
|
|
if (intent.hasExtra(KEY_TOOT)) {
|
2018-04-14 06:37:21 +10:00
|
|
|
val tootToSend = intent.getParcelableExtra<TootToSend>(KEY_TOOT)
|
2018-05-27 18:22:12 +10:00
|
|
|
?: throw IllegalStateException("SendTootService started without $KEY_TOOT extra")
|
2018-04-14 06:37:21 +10:00
|
|
|
|
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
|
|
val channel = NotificationChannel(CHANNEL_ID, getString(R.string.send_toot_notification_channel_name), NotificationManager.IMPORTANCE_LOW)
|
|
|
|
notificationManager.createNotificationChannel(channel)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
var notificationText = tootToSend.warningText
|
|
|
|
if (notificationText.isBlank()) {
|
|
|
|
notificationText = tootToSend.text
|
|
|
|
}
|
|
|
|
|
|
|
|
val builder = NotificationCompat.Builder(this, CHANNEL_ID)
|
|
|
|
.setSmallIcon(R.drawable.ic_notify)
|
|
|
|
.setContentTitle(getString(R.string.send_toot_notification_title))
|
|
|
|
.setContentText(notificationText)
|
|
|
|
.setProgress(1, 0, true)
|
|
|
|
.setOngoing(true)
|
2018-12-18 01:25:35 +11:00
|
|
|
.setColor(ContextCompat.getColor(this, R.color.tusky_blue))
|
2018-04-22 18:37:09 +10:00
|
|
|
.addAction(0, getString(android.R.string.cancel), cancelSendingIntent(sendingNotificationId))
|
2018-04-14 06:37:21 +10:00
|
|
|
|
2018-05-27 18:22:12 +10:00
|
|
|
if (tootsToSend.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
|
|
|
}
|
|
|
|
|
2018-04-22 18:37:09 +10:00
|
|
|
tootsToSend[sendingNotificationId] = tootToSend
|
|
|
|
sendToot(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
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun sendToot(tootId: Int) {
|
|
|
|
|
|
|
|
// when tootToSend == null, sending has been canceled
|
|
|
|
val tootToSend = tootsToSend[tootId] ?: return
|
|
|
|
|
|
|
|
// when account == null, user has logged out, cancel sending
|
|
|
|
val account = accountManager.getAccountById(tootToSend.accountId)
|
|
|
|
|
2018-05-27 18:22:12 +10:00
|
|
|
if (account == null) {
|
2018-04-14 06:37:21 +10:00
|
|
|
tootsToSend.remove(tootId)
|
2018-04-18 04:07:14 +10:00
|
|
|
notificationManager.cancel(tootId)
|
|
|
|
stopSelfWhenDone()
|
2018-04-14 06:37:21 +10:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
tootToSend.retries++
|
|
|
|
|
2019-08-23 04:30:08 +10:00
|
|
|
val newStatus = NewStatus(
|
2018-04-14 06:37:21 +10:00
|
|
|
tootToSend.text,
|
|
|
|
tootToSend.warningText,
|
2019-08-23 04:30:08 +10:00
|
|
|
tootToSend.inReplyToId,
|
2018-04-14 06:37:21 +10:00
|
|
|
tootToSend.visibility,
|
|
|
|
tootToSend.sensitive,
|
|
|
|
tootToSend.mediaIds,
|
2019-10-03 05:28:12 +10:00
|
|
|
tootToSend.scheduledAt,
|
2019-08-23 04:30:08 +10:00
|
|
|
tootToSend.poll
|
|
|
|
)
|
|
|
|
|
|
|
|
val sendCall = mastodonApi.createStatus(
|
|
|
|
"Bearer " + account.accessToken,
|
|
|
|
account.domain,
|
|
|
|
tootToSend.idempotencyKey,
|
|
|
|
newStatus
|
2018-04-14 06:37:21 +10:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
sendCalls[tootId] = sendCall
|
|
|
|
|
2018-05-27 18:22:12 +10:00
|
|
|
val callback = object : Callback<Status> {
|
2018-04-14 06:37:21 +10:00
|
|
|
override fun onResponse(call: Call<Status>, response: Response<Status>) {
|
|
|
|
|
2019-10-03 05:28:12 +10:00
|
|
|
val scheduled = !tootToSend.scheduledAt.isNullOrEmpty()
|
2018-04-14 06:37:21 +10:00
|
|
|
tootsToSend.remove(tootId)
|
|
|
|
|
|
|
|
if (response.isSuccessful) {
|
|
|
|
// If the status was loaded from a draft, delete the draft and associated media files.
|
2018-05-27 18:22:12 +10:00
|
|
|
if (tootToSend.savedTootUid != 0) {
|
2018-04-14 06:37:21 +10:00
|
|
|
saveTootHelper.deleteDraft(tootToSend.savedTootUid)
|
|
|
|
}
|
|
|
|
|
2019-10-03 05:28:12 +10:00
|
|
|
if (scheduled) {
|
|
|
|
response.body()?.let(::StatusScheduledEvent)?.let(eventHub::dispatch)
|
|
|
|
} else {
|
|
|
|
response.body()?.let(::StatusComposedEvent)?.let(eventHub::dispatch)
|
|
|
|
}
|
2018-05-27 18:22:12 +10:00
|
|
|
|
2018-04-14 06:37:21 +10:00
|
|
|
notificationManager.cancel(tootId)
|
|
|
|
|
|
|
|
} else {
|
|
|
|
// the server refused to accept the toot, save toot & show error message
|
|
|
|
saveTootToDrafts(tootToSend)
|
|
|
|
|
|
|
|
val builder = NotificationCompat.Builder(this@SendTootService, CHANNEL_ID)
|
|
|
|
.setSmallIcon(R.drawable.ic_notify)
|
|
|
|
.setContentTitle(getString(R.string.send_toot_notification_error_title))
|
|
|
|
.setContentText(getString(R.string.send_toot_notification_saved_content))
|
2018-12-18 01:25:35 +11:00
|
|
|
.setColor(ContextCompat.getColor(this@SendTootService, R.color.tusky_blue))
|
2018-04-14 06:37:21 +10:00
|
|
|
|
2018-04-22 18:37:09 +10:00
|
|
|
notificationManager.cancel(tootId)
|
|
|
|
notificationManager.notify(errorNotificationId--, builder.build())
|
2018-04-14 06:37:21 +10:00
|
|
|
|
|
|
|
}
|
2018-04-22 18:37:09 +10:00
|
|
|
|
|
|
|
stopSelfWhenDone()
|
|
|
|
|
2018-04-14 06:37:21 +10:00
|
|
|
}
|
|
|
|
|
|
|
|
override fun onFailure(call: Call<Status>, t: Throwable) {
|
2018-05-27 18:22:12 +10:00
|
|
|
var backoff = TimeUnit.SECONDS.toMillis(tootToSend.retries.toLong())
|
2018-04-14 06:37:21 +10:00
|
|
|
if (backoff > MAX_RETRY_INTERVAL) {
|
|
|
|
backoff = MAX_RETRY_INTERVAL
|
|
|
|
}
|
|
|
|
|
|
|
|
timer.schedule(object : TimerTask() {
|
|
|
|
override fun run() {
|
|
|
|
sendToot(tootId)
|
|
|
|
}
|
|
|
|
}, backoff)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
sendCall.enqueue(callback)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2018-04-18 04:07:14 +10:00
|
|
|
private fun stopSelfWhenDone() {
|
2018-04-22 18:37:09 +10:00
|
|
|
|
2018-04-18 04:07:14 +10:00
|
|
|
if (tootsToSend.isEmpty()) {
|
2018-04-22 18:37:09 +10:00
|
|
|
ServiceCompat.stopForeground(this@SendTootService, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
2018-04-18 04:07:14 +10:00
|
|
|
stopSelf()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-04-14 06:37:21 +10:00
|
|
|
private fun cancelSending(tootId: Int) {
|
|
|
|
val tootToCancel = tootsToSend.remove(tootId)
|
2018-05-27 18:22:12 +10:00
|
|
|
if (tootToCancel != null) {
|
2018-04-14 06:37:21 +10:00
|
|
|
val sendCall = sendCalls.remove(tootId)
|
|
|
|
sendCall?.cancel()
|
|
|
|
|
|
|
|
saveTootToDrafts(tootToCancel)
|
|
|
|
|
|
|
|
val builder = NotificationCompat.Builder(this@SendTootService, CHANNEL_ID)
|
|
|
|
.setSmallIcon(R.drawable.ic_notify)
|
|
|
|
.setContentTitle(getString(R.string.send_toot_notification_cancel_title))
|
|
|
|
.setContentText(getString(R.string.send_toot_notification_saved_content))
|
2018-12-18 01:25:35 +11:00
|
|
|
.setColor(ContextCompat.getColor(this@SendTootService, R.color.tusky_blue))
|
2018-04-14 06:37:21 +10:00
|
|
|
|
|
|
|
notificationManager.notify(tootId, builder.build())
|
|
|
|
|
|
|
|
timer.schedule(object : TimerTask() {
|
|
|
|
override fun run() {
|
|
|
|
notificationManager.cancel(tootId)
|
2018-04-22 18:37:09 +10:00
|
|
|
stopSelfWhenDone()
|
2018-04-14 06:37:21 +10:00
|
|
|
}
|
|
|
|
}, 5000)
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun saveTootToDrafts(toot: TootToSend) {
|
|
|
|
|
|
|
|
saveTootHelper.saveToot(toot.text,
|
|
|
|
toot.warningText,
|
|
|
|
toot.savedJsonUrls,
|
|
|
|
toot.mediaUris,
|
2018-11-12 05:25:45 +11:00
|
|
|
toot.mediaDescriptions,
|
2018-04-14 06:37:21 +10:00
|
|
|
toot.savedTootUid,
|
|
|
|
toot.inReplyToId,
|
|
|
|
toot.replyingStatusContent,
|
|
|
|
toot.replyingStatusAuthorUsername,
|
2019-08-23 04:30:08 +10:00
|
|
|
Status.Visibility.byString(toot.visibility),
|
|
|
|
toot.poll)
|
2018-04-14 06:37:21 +10:00
|
|
|
}
|
|
|
|
|
|
|
|
private fun cancelSendingIntent(tootId: Int): PendingIntent {
|
|
|
|
|
|
|
|
val intent = Intent(this, SendTootService::class.java)
|
|
|
|
|
|
|
|
intent.putExtra(KEY_CANCEL, tootId)
|
|
|
|
|
|
|
|
return PendingIntent.getService(this, tootId, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
companion object {
|
|
|
|
|
|
|
|
private const val KEY_TOOT = "toot"
|
|
|
|
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
|
|
|
|
|
|
|
@JvmStatic
|
|
|
|
fun sendTootIntent(context: Context,
|
|
|
|
text: String,
|
|
|
|
warningText: String,
|
|
|
|
visibility: Status.Visibility,
|
|
|
|
sensitive: Boolean,
|
|
|
|
mediaIds: List<String>,
|
2018-06-29 06:22:29 +10:00
|
|
|
mediaUris: List<Uri>,
|
2018-11-12 05:25:45 +11:00
|
|
|
mediaDescriptions: List<String>,
|
2019-10-03 05:28:12 +10:00
|
|
|
scheduledAt: String?,
|
2018-04-14 06:37:21 +10:00
|
|
|
inReplyToId: String?,
|
2019-08-23 04:30:08 +10:00
|
|
|
poll: NewPoll?,
|
2018-04-14 06:37:21 +10:00
|
|
|
replyingStatusContent: String?,
|
|
|
|
replyingStatusAuthorUsername: String?,
|
|
|
|
savedJsonUrls: String?,
|
|
|
|
account: AccountEntity,
|
|
|
|
savedTootUid: Int
|
|
|
|
): Intent {
|
|
|
|
val intent = Intent(context, SendTootService::class.java)
|
|
|
|
|
2019-02-01 05:03:34 +11:00
|
|
|
val idempotencyKey = randomAlphanumericString(16)
|
2018-04-14 06:37:21 +10:00
|
|
|
|
|
|
|
val tootToSend = TootToSend(text,
|
|
|
|
warningText,
|
|
|
|
visibility.serverString(),
|
|
|
|
sensitive,
|
|
|
|
mediaIds,
|
2018-06-29 06:22:29 +10:00
|
|
|
mediaUris.map { it.toString() },
|
2018-11-12 05:25:45 +11:00
|
|
|
mediaDescriptions,
|
2019-10-03 05:28:12 +10:00
|
|
|
scheduledAt,
|
2018-04-14 06:37:21 +10:00
|
|
|
inReplyToId,
|
2019-08-23 04:30:08 +10:00
|
|
|
poll,
|
2018-04-14 06:37:21 +10:00
|
|
|
replyingStatusContent,
|
|
|
|
replyingStatusAuthorUsername,
|
|
|
|
savedJsonUrls,
|
|
|
|
account.id,
|
|
|
|
savedTootUid,
|
|
|
|
idempotencyKey,
|
|
|
|
0)
|
|
|
|
|
|
|
|
intent.putExtra(KEY_TOOT, tootToSend)
|
|
|
|
|
2018-06-29 06:22:29 +10:00
|
|
|
if(mediaUris.isNotEmpty()) {
|
|
|
|
// forward uri permissions
|
|
|
|
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
|
|
|
val uriClip = ClipData(
|
|
|
|
ClipDescription("Toot Media", arrayOf("image/*", "video/*")),
|
|
|
|
ClipData.Item(mediaUris[0])
|
|
|
|
)
|
|
|
|
mediaUris
|
|
|
|
.drop(1)
|
|
|
|
.forEach { mediaUri ->
|
|
|
|
uriClip.addItem(ClipData.Item(mediaUri))
|
|
|
|
}
|
|
|
|
|
|
|
|
intent.clipData = uriClip
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2018-04-14 06:37:21 +10:00
|
|
|
return intent
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@Parcelize
|
|
|
|
data class TootToSend(val text: String,
|
|
|
|
val warningText: String,
|
|
|
|
val visibility: String,
|
|
|
|
val sensitive: Boolean,
|
|
|
|
val mediaIds: List<String>,
|
|
|
|
val mediaUris: List<String>,
|
2018-11-12 05:25:45 +11:00
|
|
|
val mediaDescriptions: List<String>,
|
2019-10-03 05:28:12 +10:00
|
|
|
val scheduledAt: String?,
|
2018-04-14 06:37:21 +10:00
|
|
|
val inReplyToId: String?,
|
2019-08-23 04:30:08 +10:00
|
|
|
val poll: NewPoll?,
|
2018-04-14 06:37:21 +10:00
|
|
|
val replyingStatusContent: String?,
|
|
|
|
val replyingStatusAuthorUsername: String?,
|
|
|
|
val savedJsonUrls: String?,
|
|
|
|
val accountId: Long,
|
|
|
|
val savedTootUid: Int,
|
|
|
|
val idempotencyKey: String,
|
2018-05-27 18:22:12 +10:00
|
|
|
var retries: Int) : Parcelable
|