ComposeActivity improvements (#548)
* do not add media urls to status text * add scrolling to content * add arrow icon and animation to replying-to toggle * remove unnecessary compose_button_colors.xml * improve toot button * improve bottom bar, add bottom sheet for compose options, dedicated cw button * fix crash on Android < API 21 * move media picking from dialog to bottom sheet * add small style tootbutton * fix colors/button background for light theme * add icons to media chose bottom sheet * improve hide media button, delete unused styles * fix crash on dev build when taking photo * consolidate drawables * consolidate strings and ids, add tooltips to buttons * allow media only toots * change error message to show max size of upload correctly * fix button color * add emoji * code cleanup * Merge branch 'master' into compose_activity_refactoring # Conflicts: # app/src/main/java/com/keylesspalace/tusky/ComposeActivity.java * fix hidden snackbar * improve hint text color * add SendTootService * fix timeline refreshing * toot saving and error handling for sendtootservice * restructure some code * convert EditTextTyped to Kotlin * fixed pick media button disabled color * force sensitive media when content warning is shown * add db cache for emojis & fix tests * reorder buttons to match mastodon web * add possibility to cancel sending of toot * correctly delete sent toots * refresh SavedTootActivity after toot was sent * remove unused resources * correct params for toot saving in SendTootService * consolidate strings * bugfix * remove unused resources * fix notifications on old android for SendTootService * fix crash
This commit is contained in:
parent
8a23f034f0
commit
27eefbf65a
79 changed files with 1815 additions and 1234 deletions
|
@ -0,0 +1,329 @@
|
|||
package com.keylesspalace.tusky.service
|
||||
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.os.Parcelable
|
||||
import android.support.v4.app.NotificationCompat
|
||||
import android.support.v4.app.ServiceCompat
|
||||
import android.support.v4.content.ContextCompat
|
||||
import android.support.v4.content.LocalBroadcastManager
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.TuskyApplication
|
||||
import com.keylesspalace.tusky.db.AccountEntity
|
||||
import com.keylesspalace.tusky.db.AccountManager
|
||||
import com.keylesspalace.tusky.di.Injectable
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.receiver.TimelineReceiver
|
||||
import com.keylesspalace.tusky.util.SaveTootHelper
|
||||
import com.keylesspalace.tusky.util.StringUtils
|
||||
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
|
||||
import javax.inject.Inject
|
||||
|
||||
class SendTootService: Service(), Injectable {
|
||||
|
||||
@Inject
|
||||
lateinit var mastodonApi: MastodonApi
|
||||
@Inject
|
||||
lateinit var accountManager: AccountManager
|
||||
|
||||
private lateinit var saveTootHelper: SaveTootHelper
|
||||
|
||||
private val tootsToSend = ConcurrentHashMap<Int, TootToSend>()
|
||||
private val sendCalls = ConcurrentHashMap<Int, Call<Status>>()
|
||||
|
||||
private val timer = Timer()
|
||||
|
||||
|
||||
override fun onCreate() {
|
||||
AndroidInjection.inject(this)
|
||||
saveTootHelper = SaveTootHelper(TuskyApplication.getDB().tootDao(), this)
|
||||
super.onCreate()
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent): IBinder? {
|
||||
return null
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
|
||||
|
||||
if(intent.hasExtra(KEY_TOOT)) {
|
||||
|
||||
val tootToSend = intent.getParcelableExtra<TootToSend>(KEY_TOOT)
|
||||
|
||||
if (tootToSend == null) {
|
||||
throw IllegalStateException("SendTootService started without $KEY_TOOT extra")
|
||||
}
|
||||
|
||||
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
|
||||
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)
|
||||
.setColor(ContextCompat.getColor(this, R.color.primary))
|
||||
.addAction(0, getString(android.R.string.cancel), cancelSendingIntent(notificationId))
|
||||
|
||||
if(tootsToSend.size == 0 || Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_DETACH)
|
||||
startForeground(notificationId, builder.build())
|
||||
} else {
|
||||
notificationManager.notify(notificationId, builder.build())
|
||||
}
|
||||
|
||||
tootsToSend[notificationId] = tootToSend
|
||||
sendToot(notificationId)
|
||||
|
||||
notificationId--
|
||||
|
||||
} else {
|
||||
|
||||
if(intent.hasExtra(KEY_CANCEL)) {
|
||||
cancelSending(intent.getIntExtra(KEY_CANCEL, 0))
|
||||
stopSelf(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)
|
||||
|
||||
if(account == null) {
|
||||
tootsToSend.remove(tootId)
|
||||
return
|
||||
}
|
||||
|
||||
tootToSend.retries++
|
||||
|
||||
val sendCall = mastodonApi.createStatus(
|
||||
"Bearer " + account.accessToken,
|
||||
account.domain,
|
||||
tootToSend.text,
|
||||
tootToSend.inReplyToId,
|
||||
tootToSend.warningText,
|
||||
tootToSend.visibility,
|
||||
tootToSend.sensitive,
|
||||
tootToSend.mediaIds,
|
||||
tootToSend.idempotencyKey
|
||||
)
|
||||
|
||||
|
||||
sendCalls[tootId] = sendCall
|
||||
|
||||
val callback = object: Callback<Status> {
|
||||
override fun onResponse(call: Call<Status>, response: Response<Status>) {
|
||||
|
||||
tootsToSend.remove(tootId)
|
||||
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
|
||||
if (response.isSuccessful) {
|
||||
|
||||
val intent = Intent(TimelineReceiver.Types.STATUS_COMPOSED)
|
||||
LocalBroadcastManager.getInstance(applicationContext).sendBroadcast(intent)
|
||||
|
||||
// If the status was loaded from a draft, delete the draft and associated media files.
|
||||
if(tootToSend.savedTootUid != 0) {
|
||||
saveTootHelper.deleteDraft(tootToSend.savedTootUid)
|
||||
}
|
||||
|
||||
if (tootsToSend.isEmpty()) {
|
||||
ServiceCompat.stopForeground(this@SendTootService, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
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))
|
||||
.setColor(ContextCompat.getColor(this@SendTootService, R.color.primary))
|
||||
|
||||
notificationManager.notify(tootId, builder.build())
|
||||
|
||||
if (tootsToSend.isEmpty()) {
|
||||
ServiceCompat.stopForeground(this@SendTootService, ServiceCompat.STOP_FOREGROUND_DETACH)
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<Status>, t: Throwable) {
|
||||
var backoff = 1000L*tootToSend.retries
|
||||
if (backoff > MAX_RETRY_INTERVAL) {
|
||||
backoff = MAX_RETRY_INTERVAL
|
||||
}
|
||||
|
||||
timer.schedule(object : TimerTask() {
|
||||
override fun run() {
|
||||
sendToot(tootId)
|
||||
}
|
||||
}, backoff)
|
||||
}
|
||||
}
|
||||
|
||||
sendCall.enqueue(callback)
|
||||
|
||||
}
|
||||
|
||||
private fun cancelSending(tootId: Int) {
|
||||
val tootToCancel = tootsToSend.remove(tootId)
|
||||
if(tootToCancel != null) {
|
||||
val sendCall = sendCalls.remove(tootId)
|
||||
sendCall?.cancel()
|
||||
|
||||
saveTootToDrafts(tootToCancel)
|
||||
|
||||
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
|
||||
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))
|
||||
.setColor(ContextCompat.getColor(this@SendTootService, R.color.primary))
|
||||
|
||||
notificationManager.notify(tootId, builder.build())
|
||||
|
||||
timer.schedule(object : TimerTask() {
|
||||
override fun run() {
|
||||
notificationManager.cancel(tootId)
|
||||
}
|
||||
}, 5000)
|
||||
|
||||
if (tootsToSend.isEmpty()) {
|
||||
ServiceCompat.stopForeground(this@SendTootService, ServiceCompat.STOP_FOREGROUND_DETACH)
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveTootToDrafts(toot: TootToSend) {
|
||||
|
||||
saveTootHelper.saveToot(toot.text,
|
||||
toot.warningText,
|
||||
toot.savedJsonUrls,
|
||||
toot.mediaUris,
|
||||
toot.savedTootUid,
|
||||
toot.inReplyToId,
|
||||
toot.replyingStatusContent,
|
||||
toot.replyingStatusAuthorUsername,
|
||||
Status.Visibility.byString(toot.visibility))
|
||||
}
|
||||
|
||||
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"
|
||||
|
||||
private const val MAX_RETRY_INTERVAL = 60*1000L // 1 minute
|
||||
|
||||
private var notificationId = -1 // use negative ids to not clash with other notis
|
||||
|
||||
@JvmStatic
|
||||
fun sendTootIntent(context: Context,
|
||||
text: String,
|
||||
warningText: String,
|
||||
visibility: Status.Visibility,
|
||||
sensitive: Boolean,
|
||||
mediaIds: List<String>,
|
||||
mediaUris: List<String>,
|
||||
inReplyToId: String?,
|
||||
replyingStatusContent: String?,
|
||||
replyingStatusAuthorUsername: String?,
|
||||
savedJsonUrls: String?,
|
||||
account: AccountEntity,
|
||||
savedTootUid: Int
|
||||
): Intent {
|
||||
val intent = Intent(context, SendTootService::class.java)
|
||||
|
||||
val idempotencyKey = StringUtils.randomAlphanumericString(16)
|
||||
|
||||
val tootToSend = TootToSend(text,
|
||||
warningText,
|
||||
visibility.serverString(),
|
||||
sensitive,
|
||||
mediaIds,
|
||||
mediaUris,
|
||||
inReplyToId,
|
||||
replyingStatusContent,
|
||||
replyingStatusAuthorUsername,
|
||||
savedJsonUrls,
|
||||
account.id,
|
||||
savedTootUid,
|
||||
idempotencyKey,
|
||||
0)
|
||||
|
||||
intent.putExtra(KEY_TOOT, tootToSend)
|
||||
|
||||
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>,
|
||||
val inReplyToId: String?,
|
||||
val replyingStatusContent: String?,
|
||||
val replyingStatusAuthorUsername: String?,
|
||||
val savedJsonUrls: String?,
|
||||
val accountId: Long,
|
||||
val savedTootUid: Int,
|
||||
val idempotencyKey: String,
|
||||
var retries: Int): Parcelable
|
Loading…
Add table
Add a link
Reference in a new issue