streaming: implemented streaming for notifications
This commit is contained in:
parent
84ffe73d2b
commit
9bc6ece29a
|
@ -2,7 +2,7 @@
|
||||||
"formatVersion": 1,
|
"formatVersion": 1,
|
||||||
"database": {
|
"database": {
|
||||||
"version": 25,
|
"version": 25,
|
||||||
"identityHash": "7ab8482b8d5dcb97c4c8932f578879f2",
|
"identityHash": "ce7a96213f9e12e00a6ec21f3efaf547",
|
||||||
"entities": [
|
"entities": [
|
||||||
{
|
{
|
||||||
"tableName": "TootEntity",
|
"tableName": "TootEntity",
|
||||||
|
@ -92,7 +92,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"tableName": "AccountEntity",
|
"tableName": "AccountEntity",
|
||||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsFollowRequested` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationsEmojiReactions` INTEGER NOT NULL, `notificationsChatMessages` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL, `defaultFormattingSyntax` TEXT NOT NULL)",
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsStreamingEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsFollowRequested` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationsEmojiReactions` INTEGER NOT NULL, `notificationsChatMessages` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL, `defaultFormattingSyntax` TEXT NOT NULL)",
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"fieldPath": "id",
|
"fieldPath": "id",
|
||||||
|
@ -148,6 +148,12 @@
|
||||||
"affinity": "INTEGER",
|
"affinity": "INTEGER",
|
||||||
"notNull": true
|
"notNull": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsStreamingEnabled",
|
||||||
|
"columnName": "notificationsStreamingEnabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"fieldPath": "notificationsMentioned",
|
"fieldPath": "notificationsMentioned",
|
||||||
"columnName": "notificationsMentioned",
|
"columnName": "notificationsMentioned",
|
||||||
|
@ -879,7 +885,7 @@
|
||||||
"views": [],
|
"views": [],
|
||||||
"setupQueries": [
|
"setupQueries": [
|
||||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '7ab8482b8d5dcb97c4c8932f578879f2')"
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ce7a96213f9e12e00a6ec21f3efaf547')"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -44,6 +44,9 @@
|
||||||
|
|
||||||
<string name="link">Link</string> <!-- Web Link -->
|
<string name="link">Link</string> <!-- Web Link -->
|
||||||
|
|
||||||
|
<string name="streaming_notification_name">Live notifications</string>
|
||||||
|
<string name="streaming_notification_description">Running live notifications for: </string>
|
||||||
|
|
||||||
<!-- REPLACEMENT FOR TUSKY STRINGS -->
|
<!-- REPLACEMENT FOR TUSKY STRINGS -->
|
||||||
<string name="action_toggle_visibility">Post visibility</string>
|
<string name="action_toggle_visibility">Post visibility</string>
|
||||||
<string name="action_schedule_toot">Schedule post</string>
|
<string name="action_schedule_toot">Schedule post</string>
|
||||||
|
|
|
@ -168,6 +168,8 @@
|
||||||
|
|
||||||
<service android:name=".service.SendTootService" />
|
<service android:name=".service.SendTootService" />
|
||||||
|
|
||||||
|
<service android:name=".service.StreamingService" />
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:name="androidx.core.content.FileProvider"
|
android:name="androidx.core.content.FileProvider"
|
||||||
android:authorities="${applicationId}.fileprovider"
|
android:authorities="${applicationId}.fileprovider"
|
||||||
|
|
|
@ -60,6 +60,7 @@ import com.keylesspalace.tusky.interfaces.AccountSelectionListener
|
||||||
import com.keylesspalace.tusky.interfaces.ActionButtonActivity
|
import com.keylesspalace.tusky.interfaces.ActionButtonActivity
|
||||||
import com.keylesspalace.tusky.interfaces.ReselectableFragment
|
import com.keylesspalace.tusky.interfaces.ReselectableFragment
|
||||||
import com.keylesspalace.tusky.pager.MainPagerAdapter
|
import com.keylesspalace.tusky.pager.MainPagerAdapter
|
||||||
|
import com.keylesspalace.tusky.service.StreamingService
|
||||||
import com.keylesspalace.tusky.util.*
|
import com.keylesspalace.tusky.util.*
|
||||||
import com.mikepenz.iconics.IconicsDrawable
|
import com.mikepenz.iconics.IconicsDrawable
|
||||||
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
|
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
|
||||||
|
@ -191,53 +192,15 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
|
|
||||||
// Setup push notifications
|
// Setup push notifications
|
||||||
if (NotificationHelper.areNotificationsEnabled(this, accountManager)) {
|
if (NotificationHelper.areNotificationsEnabled(this, accountManager)) {
|
||||||
NotificationHelper.enablePullNotifications(this)
|
if(accountManager.areNotificationsStreamingEnabled()) {
|
||||||
|
NotificationHelper.disablePullNotifications(this)
|
||||||
// Use when WorkManager doesn't want to work
|
StreamingService.startStreaming(this)
|
||||||
/*
|
} else {
|
||||||
val accountList = accountManager.getAllAccountsOrderedByActive()
|
StreamingService.stopStreaming(this)
|
||||||
for (account in accountList) {
|
NotificationHelper.enablePullNotifications(this)
|
||||||
if (account.notificationsEnabled) {
|
|
||||||
try {
|
|
||||||
Log.d(TAG, "getting Notifications for " + account.fullName)
|
|
||||||
// don't care about withMuted because they are always silently ignored
|
|
||||||
val notificationsResponse = mastodonApi.notificationsWithAuth(
|
|
||||||
String.format("Bearer %s", account.accessToken),
|
|
||||||
account.domain, true,
|
|
||||||
setOf(Notification.Type.CHAT_MESSAGE.presentation)
|
|
||||||
).enqueue(object: Callback<List<Notification>> {
|
|
||||||
override fun onFailure(call: Call<List<Notification>>, t: Throwable) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResponse(call: Call<List<Notification>>, response: Response<List<Notification>>) {
|
|
||||||
val notifications = response.body()
|
|
||||||
val newId = account.lastNotificationId
|
|
||||||
var newestId = ""
|
|
||||||
var isFirstOfBatch = true
|
|
||||||
notifications?.reversed()?.forEach { notification ->
|
|
||||||
val currentId = notification.id
|
|
||||||
if (newestId.isLessThan(currentId)) {
|
|
||||||
newestId = currentId
|
|
||||||
}
|
|
||||||
if (newId.isLessThan(currentId)) {
|
|
||||||
NotificationHelper.make(this@MainActivity, notification, account, isFirstOfBatch)
|
|
||||||
isFirstOfBatch = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
account.lastNotificationId = newestId
|
|
||||||
accountManager.saveAccount(account)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} catch (e: IOException) {
|
|
||||||
Log.w(TAG, "error receiving notifications", e)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
StreamingService.stopStreaming(this)
|
||||||
NotificationHelper.disablePullNotifications(this)
|
NotificationHelper.disablePullNotifications(this)
|
||||||
}
|
}
|
||||||
eventHub.events
|
eventHub.events
|
||||||
|
|
|
@ -138,7 +138,7 @@ public class NotificationHelper {
|
||||||
/**
|
/**
|
||||||
* by setting this as false, it's possible to test legacy notification channels on newer devices
|
* by setting this as false, it's possible to test legacy notification channels on newer devices
|
||||||
*/
|
*/
|
||||||
//public static final boolean NOTIFICATION_USE_CHANNELS = false;
|
// public static final boolean NOTIFICATION_USE_CHANNELS = false;
|
||||||
public static final boolean NOTIFICATION_USE_CHANNELS = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
|
public static final boolean NOTIFICATION_USE_CHANNELS = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -518,7 +518,7 @@ public class NotificationHelper {
|
||||||
PeriodicWorkRequest.MIN_PERIODIC_FLEX_MILLIS, TimeUnit.MILLISECONDS
|
PeriodicWorkRequest.MIN_PERIODIC_FLEX_MILLIS, TimeUnit.MILLISECONDS
|
||||||
)
|
)
|
||||||
.addTag(NOTIFICATION_PULL_TAG)
|
.addTag(NOTIFICATION_PULL_TAG)
|
||||||
//.setConstraints(new Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build())
|
.setConstraints(new Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
workManager.enqueue(workRequest);
|
workManager.enqueue(workRequest);
|
||||||
|
|
|
@ -46,8 +46,7 @@ class NotificationWorker(
|
||||||
val notificationsResponse = mastodonApi.notificationsWithAuth(
|
val notificationsResponse = mastodonApi.notificationsWithAuth(
|
||||||
String.format("Bearer %s", account.accessToken),
|
String.format("Bearer %s", account.accessToken),
|
||||||
account.domain, true,
|
account.domain, true,
|
||||||
setOf(Notification.Type.CHAT_MESSAGE.presentation)
|
Notification.Type.asStringList).execute()
|
||||||
).execute()
|
|
||||||
val notifications = notificationsResponse.body()
|
val notifications = notificationsResponse.body()
|
||||||
if (notificationsResponse.isSuccessful && notifications != null) {
|
if (notificationsResponse.isSuccessful && notifications != null) {
|
||||||
onNotificationsReceived(account, notifications)
|
onNotificationsReceived(account, notifications)
|
||||||
|
|
|
@ -37,6 +37,7 @@ data class AccountEntity(@field:PrimaryKey(autoGenerate = true) var id: Long,
|
||||||
var displayName: String = "",
|
var displayName: String = "",
|
||||||
var profilePictureUrl: String = "",
|
var profilePictureUrl: String = "",
|
||||||
var notificationsEnabled: Boolean = true,
|
var notificationsEnabled: Boolean = true,
|
||||||
|
var notificationsStreamingEnabled: Boolean = true,
|
||||||
var notificationsMentioned: Boolean = true,
|
var notificationsMentioned: Boolean = true,
|
||||||
var notificationsFollowed: Boolean = true,
|
var notificationsFollowed: Boolean = true,
|
||||||
var notificationsFollowRequested: Boolean = false,
|
var notificationsFollowRequested: Boolean = false,
|
||||||
|
|
|
@ -184,6 +184,10 @@ class AccountManager @Inject constructor(db: AppDatabase) {
|
||||||
return accounts.any { it.notificationsEnabled }
|
return accounts.any { it.notificationsEnabled }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun areNotificationsStreamingEnabled() : Boolean {
|
||||||
|
return accounts.any { it.notificationsStreamingEnabled }
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finds an account by its database id
|
* Finds an account by its database id
|
||||||
* @param accountId the id of the account
|
* @param accountId the id of the account
|
||||||
|
|
|
@ -372,6 +372,7 @@ public abstract class AppDatabase extends RoomDatabase {
|
||||||
"PRIMARY KEY (`localId`, `messageId`))");
|
"PRIMARY KEY (`localId`, `messageId`))");
|
||||||
database.execSQL("ALTER TABLE `InstanceEntity` ADD COLUMN `chatLimit` INTEGER");
|
database.execSQL("ALTER TABLE `InstanceEntity` ADD COLUMN `chatLimit` INTEGER");
|
||||||
database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `notificationsChatMessages` INTEGER NOT NULL DEFAULT 1");
|
database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `notificationsChatMessages` INTEGER NOT NULL DEFAULT 1");
|
||||||
|
database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `notificationsStreamingEnabled` INTEGER NOT NULL DEFAULT 1");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,9 +60,9 @@ class NetworkModule {
|
||||||
addInterceptor(InstanceSwitchAuthInterceptor(accountManager))
|
addInterceptor(InstanceSwitchAuthInterceptor(accountManager))
|
||||||
if (BuildConfig.DEBUG) {
|
if (BuildConfig.DEBUG) {
|
||||||
addInterceptor(HttpLoggingInterceptor().apply {
|
addInterceptor(HttpLoggingInterceptor().apply {
|
||||||
//level = HttpLoggingInterceptor.Level.BASIC
|
level = HttpLoggingInterceptor.Level.BASIC
|
||||||
//level = HttpLoggingInterceptor.Level.HEADERS
|
//level = HttpLoggingInterceptor.Level.HEADERS
|
||||||
level = HttpLoggingInterceptor.Level.BODY
|
//level = HttpLoggingInterceptor.Level.BODY
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ import android.content.Context
|
||||||
import com.keylesspalace.tusky.service.SendTootService
|
import com.keylesspalace.tusky.service.SendTootService
|
||||||
import com.keylesspalace.tusky.service.ServiceClient
|
import com.keylesspalace.tusky.service.ServiceClient
|
||||||
import com.keylesspalace.tusky.service.ServiceClientImpl
|
import com.keylesspalace.tusky.service.ServiceClientImpl
|
||||||
|
import com.keylesspalace.tusky.service.StreamingService
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.android.ContributesAndroidInjector
|
import dagger.android.ContributesAndroidInjector
|
||||||
|
@ -28,6 +29,9 @@ abstract class ServicesModule {
|
||||||
@ContributesAndroidInjector
|
@ContributesAndroidInjector
|
||||||
abstract fun contributesSendTootService(): SendTootService
|
abstract fun contributesSendTootService(): SendTootService
|
||||||
|
|
||||||
|
@ContributesAndroidInjector
|
||||||
|
abstract fun contributesStreamingService(): StreamingService
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
companion object {
|
companion object {
|
||||||
@Provides
|
@Provides
|
||||||
|
|
|
@ -57,6 +57,8 @@ data class Notification(
|
||||||
return UNKNOWN
|
return UNKNOWN
|
||||||
}
|
}
|
||||||
val asList = listOf(MENTION, REBLOG, FAVOURITE, FOLLOW, POLL, EMOJI_REACTION, FOLLOW_REQUEST, CHAT_MESSAGE)
|
val asList = listOf(MENTION, REBLOG, FAVOURITE, FOLLOW, POLL, EMOJI_REACTION, FOLLOW_REQUEST, CHAT_MESSAGE)
|
||||||
|
|
||||||
|
val asStringList = asList.map { it.presentation }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
package com.keylesspalace.tusky.entity
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
|
data class StreamEvent (
|
||||||
|
val event: EventType,
|
||||||
|
val payload: String
|
||||||
|
) {
|
||||||
|
enum class EventType {
|
||||||
|
UNKNOWN,
|
||||||
|
@SerializedName("update")
|
||||||
|
UPDATE,
|
||||||
|
@SerializedName("notification")
|
||||||
|
NOTIFICATION,
|
||||||
|
@SerializedName("delete")
|
||||||
|
DELETE,
|
||||||
|
@SerializedName("filters_changed")
|
||||||
|
FILTERS_CHANGED;
|
||||||
|
}
|
||||||
|
}
|
|
@ -110,7 +110,7 @@ interface MastodonApi {
|
||||||
@Header("Authorization") auth: String,
|
@Header("Authorization") auth: String,
|
||||||
@Header(DOMAIN_HEADER) domain: String,
|
@Header(DOMAIN_HEADER) domain: String,
|
||||||
@Query("with_muted") withMuted: Boolean?,
|
@Query("with_muted") withMuted: Boolean?,
|
||||||
@Query("include_types[]") includeTypes: Set<String>?
|
@Query("include_types[]") includeTypes: List<String>?
|
||||||
): Call<List<Notification>>
|
): Call<List<Notification>>
|
||||||
|
|
||||||
@POST("api/v1/notifications/clear")
|
@POST("api/v1/notifications/clear")
|
||||||
|
|
|
@ -52,7 +52,7 @@ class SendStatusBroadcastReceiver : BroadcastReceiver() {
|
||||||
val senderIdentifier = intent.getStringExtra(NotificationHelper.KEY_SENDER_ACCOUNT_IDENTIFIER)
|
val senderIdentifier = intent.getStringExtra(NotificationHelper.KEY_SENDER_ACCOUNT_IDENTIFIER)
|
||||||
val senderFullName = intent.getStringExtra(NotificationHelper.KEY_SENDER_ACCOUNT_FULL_NAME)
|
val senderFullName = intent.getStringExtra(NotificationHelper.KEY_SENDER_ACCOUNT_FULL_NAME)
|
||||||
val citedStatusId = intent.getStringExtra(NotificationHelper.KEY_CITED_STATUS_ID)
|
val citedStatusId = intent.getStringExtra(NotificationHelper.KEY_CITED_STATUS_ID)
|
||||||
val visibility = intent.getSerializableExtra(NotificationHelper.KEY_VISIBILITY) as Status.Visibility
|
val visibility = intent.getSerializableExtra(NotificationHelper.KEY_VISIBILITY)
|
||||||
val spoiler = intent.getStringExtra(NotificationHelper.KEY_SPOILER)
|
val spoiler = intent.getStringExtra(NotificationHelper.KEY_SPOILER)
|
||||||
val mentions = intent.getStringArrayExtra(NotificationHelper.KEY_MENTIONS)
|
val mentions = intent.getStringArrayExtra(NotificationHelper.KEY_MENTIONS)
|
||||||
val citedText = intent.getStringExtra(NotificationHelper.KEY_CITED_TEXT)
|
val citedText = intent.getStringExtra(NotificationHelper.KEY_CITED_TEXT)
|
||||||
|
@ -93,7 +93,7 @@ class SendStatusBroadcastReceiver : BroadcastReceiver() {
|
||||||
|
|
||||||
val composeIntent = ComposeActivity.startIntent(context, ComposeOptions(
|
val composeIntent = ComposeActivity.startIntent(context, ComposeOptions(
|
||||||
inReplyToId = citedStatusId,
|
inReplyToId = citedStatusId,
|
||||||
replyVisibility = visibility,
|
replyVisibility = visibility as Status.Visibility,
|
||||||
contentWarning = spoiler,
|
contentWarning = spoiler,
|
||||||
mentionedUsernames = mentions.toSet(),
|
mentionedUsernames = mentions.toSet(),
|
||||||
replyingStatusAuthor = localAuthorId,
|
replyingStatusAuthor = localAuthorId,
|
||||||
|
@ -114,7 +114,7 @@ class SendStatusBroadcastReceiver : BroadcastReceiver() {
|
||||||
TootToSend(
|
TootToSend(
|
||||||
text,
|
text,
|
||||||
spoiler,
|
spoiler,
|
||||||
visibility.serverString(),
|
(visibility as Status.Visibility).serverString(),
|
||||||
false,
|
false,
|
||||||
emptyList(),
|
emptyList(),
|
||||||
emptyList(),
|
emptyList(),
|
||||||
|
|
|
@ -8,9 +8,7 @@ import android.content.ClipData
|
||||||
import android.content.ClipDescription
|
import android.content.ClipDescription
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Build
|
import android.os.*
|
||||||
import android.os.IBinder
|
|
||||||
import android.os.Parcelable
|
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.app.ServiceCompat
|
import androidx.core.app.ServiceCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
|
|
@ -0,0 +1,194 @@
|
||||||
|
package com.keylesspalace.tusky.service
|
||||||
|
|
||||||
|
import android.app.NotificationChannel
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.app.Service
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.IBinder
|
||||||
|
import android.os.Message
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.core.app.ServiceCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import com.keylesspalace.tusky.R
|
||||||
|
import com.keylesspalace.tusky.appstore.EventHub
|
||||||
|
import com.keylesspalace.tusky.components.notifications.NotificationHelper
|
||||||
|
import com.keylesspalace.tusky.db.AccountEntity
|
||||||
|
import com.keylesspalace.tusky.db.AccountManager
|
||||||
|
import com.keylesspalace.tusky.di.Injectable
|
||||||
|
import com.keylesspalace.tusky.entity.Account
|
||||||
|
import com.keylesspalace.tusky.entity.Notification
|
||||||
|
import com.keylesspalace.tusky.entity.StreamEvent
|
||||||
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
|
import dagger.android.AndroidInjection
|
||||||
|
import okhttp3.*
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class StreamingService: Service(), Injectable {
|
||||||
|
@Inject
|
||||||
|
lateinit var api: MastodonApi
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var eventHub: EventHub
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var accountManager: AccountManager
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var gson: Gson
|
||||||
|
|
||||||
|
private val sockets: MutableMap<Long, WebSocket> = mutableMapOf()
|
||||||
|
|
||||||
|
private val notificationManager by lazy { getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager }
|
||||||
|
|
||||||
|
override fun onBind(intent: Intent?): IBinder? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
AndroidInjection.inject(this)
|
||||||
|
super.onCreate()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stopStreamingForId(id: Long) {
|
||||||
|
if(id in sockets) {
|
||||||
|
sockets[id]!!.close(1000, null)
|
||||||
|
sockets.remove(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stopStreaming() : Int {
|
||||||
|
for(sock in sockets) {
|
||||||
|
sock.value.close(1000, null)
|
||||||
|
}
|
||||||
|
sockets.clear()
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_DETACH)
|
||||||
|
}
|
||||||
|
|
||||||
|
notificationManager.cancel(1337)
|
||||||
|
return START_NOT_STICKY
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
|
||||||
|
if(intent.hasExtra(KEY_STOP_STREAMING)) {
|
||||||
|
Log.d(TAG, "Stopping stream")
|
||||||
|
return stopStreaming()
|
||||||
|
}
|
||||||
|
|
||||||
|
var description = getString(R.string.streaming_notification_description)
|
||||||
|
val accounts = accountManager.getAllAccountsOrderedByActive()
|
||||||
|
var count = 0
|
||||||
|
for(account in accounts) {
|
||||||
|
stopStreamingForId(account.id)
|
||||||
|
|
||||||
|
if(!account.notificationsStreamingEnabled)
|
||||||
|
continue
|
||||||
|
|
||||||
|
val endpoint = "wss://${account.domain}/api/v1/streaming/?access_token=${account.accessToken}&stream=user:notification"
|
||||||
|
|
||||||
|
val request = Request.Builder().url(endpoint).build()
|
||||||
|
val client = OkHttpClient.Builder().build()
|
||||||
|
|
||||||
|
Log.d(TAG, "Running stream for ${account.fullName}")
|
||||||
|
|
||||||
|
sockets[account.id] = client.newWebSocket(request, StreamingListener(this, gson, account))
|
||||||
|
description += "\n" + account.fullName
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
|
||||||
|
if(count <= 0) {
|
||||||
|
Log.d(TAG, "No accounts. Stopping stream")
|
||||||
|
return stopStreaming()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (NotificationHelper.NOTIFICATION_USE_CHANNELS) {
|
||||||
|
val channel = NotificationChannel(CHANNEL_ID, getString(R.string.streaming_notification_name), NotificationManager.IMPORTANCE_LOW)
|
||||||
|
notificationManager.createNotificationChannel(channel)
|
||||||
|
}
|
||||||
|
|
||||||
|
val builder = NotificationCompat.Builder(this, CHANNEL_ID)
|
||||||
|
.setSmallIcon(R.drawable.ic_notify)
|
||||||
|
.setContentTitle(getString(R.string.streaming_notification_name))
|
||||||
|
.setContentText(description)
|
||||||
|
.setOngoing(true)
|
||||||
|
.setColor(ContextCompat.getColor(this, R.color.tusky_blue))
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_DETACH)
|
||||||
|
startForeground(1337, builder.build())
|
||||||
|
} else {
|
||||||
|
notificationManager.notify(1337, builder.build())
|
||||||
|
}
|
||||||
|
|
||||||
|
return START_NOT_STICKY
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val CHANNEL_ID = "streaming"
|
||||||
|
val KEY_STOP_STREAMING = "stop_streaming"
|
||||||
|
val TAG = "StreamingService"
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
private fun startForegroundService(ctx: Context, intent: Intent) {
|
||||||
|
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
ctx.startForegroundService(intent)
|
||||||
|
} else {
|
||||||
|
ctx.startService(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun startStreaming(context: Context) {
|
||||||
|
val intent = Intent(context, StreamingService::class.java)
|
||||||
|
|
||||||
|
Log.d(TAG, "Starting notifications streaming service...")
|
||||||
|
|
||||||
|
startForegroundService(context, intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun stopStreaming(context: Context) {
|
||||||
|
val intent = Intent(context, StreamingService::class.java)
|
||||||
|
intent.putExtra(KEY_STOP_STREAMING, 123)
|
||||||
|
|
||||||
|
Log.d(TAG, "Stopping notifications streaming service...")
|
||||||
|
|
||||||
|
startForegroundService(context, intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class StreamingListener(val context: Context, val gson: Gson, val account: AccountEntity) : WebSocketListener() {
|
||||||
|
|
||||||
|
override fun onOpen(webSocket: WebSocket, response: Response) {
|
||||||
|
Log.d(TAG, "Stream connected to: ${account.fullName}/user:notification")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
|
||||||
|
Log.d(TAG, "Stream closed for: ${account.fullName}/user:notification")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMessage(webSocket: WebSocket, text: String) {
|
||||||
|
val event = gson.fromJson(text, StreamEvent::class.java)
|
||||||
|
when(event.event) {
|
||||||
|
StreamEvent.EventType.NOTIFICATION -> {
|
||||||
|
val notification = gson.fromJson(event.payload, Notification::class.java)
|
||||||
|
NotificationHelper.make(context, notification, account, true)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
Log.d(TAG, "Unknown event type: ${event.event.toString()}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
super.onMessage(webSocket, text)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue