Twidere-App-Android-Twitter.../twidere/src/main/kotlin/org/mariotaku/twidere/service/StreamingService.kt

360 lines
14 KiB
Kotlin

package org.mariotaku.twidere.service
import android.accounts.AccountManager
import android.accounts.OnAccountsUpdateListener
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.ContentValues
import android.content.Context
import android.content.Intent
import android.os.IBinder
import android.support.v4.app.NotificationCompat
import android.support.v4.util.SimpleArrayMap
import android.text.TextUtils
import android.util.Log
import org.mariotaku.ktextension.addOnAccountsUpdatedListenerSafe
import org.mariotaku.ktextension.removeOnAccountsUpdatedListenerSafe
import org.mariotaku.microblog.library.MicroBlogException
import org.mariotaku.microblog.library.twitter.TwitterUserStream
import org.mariotaku.microblog.library.twitter.UserStreamCallback
import org.mariotaku.microblog.library.twitter.model.*
import org.mariotaku.sqliteqb.library.Expression
import org.mariotaku.twidere.BuildConfig
import org.mariotaku.twidere.R
import org.mariotaku.twidere.TwidereConstants.LOGTAG
import org.mariotaku.twidere.TwidereConstants.QUERY_PARAM_NOTIFY
import org.mariotaku.twidere.activity.SettingsActivity
import org.mariotaku.twidere.extension.model.newMicroBlogInstance
import org.mariotaku.twidere.model.AccountDetails
import org.mariotaku.twidere.model.AccountPreferences
import org.mariotaku.twidere.model.UserKey
import org.mariotaku.twidere.model.account.cred.OAuthCredentials
import org.mariotaku.twidere.model.util.AccountUtils
import org.mariotaku.twidere.provider.TwidereDataStore.*
import org.mariotaku.twidere.util.ContentValuesCreator
import org.mariotaku.twidere.util.DataStoreUtils
import org.mariotaku.twidere.util.DebugLog
import org.mariotaku.twidere.util.TwidereArrayUtils
import java.io.ByteArrayOutputStream
import java.io.IOException
import java.nio.charset.Charset
class StreamingService : Service() {
private val callbacks = SimpleArrayMap<UserKey, UserStreamCallback>()
private var notificationManager: NotificationManager? = null
private var accountKeys: Array<UserKey>? = null
private val accountChangeObserver = OnAccountsUpdateListener {
if (!TwidereArrayUtils.contentMatch(accountKeys, DataStoreUtils.getActivatedAccountKeys(this@StreamingService))) {
initStreaming()
}
}
override fun onCreate() {
super.onCreate()
notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
DebugLog.d(LOGTAG, "Stream service started.")
initStreaming()
AccountManager.get(this).addOnAccountsUpdatedListenerSafe(accountChangeObserver, updateImmediately = false)
}
override fun onDestroy() {
clearTwitterInstances()
AccountManager.get(this).removeOnAccountsUpdatedListenerSafe(accountChangeObserver)
DebugLog.d(LOGTAG, "Stream service stopped.")
super.onDestroy()
}
override fun onBind(intent: Intent): IBinder? {
return null
}
private fun clearTwitterInstances() {
var i = 0
val j = callbacks.size()
while (i < j) {
Thread(ShutdownStreamTwitterRunnable(callbacks.valueAt(i))).start()
i++
}
callbacks.clear()
notificationManager!!.cancel(NOTIFICATION_SERVICE_STARTED)
}
private fun initStreaming() {
if (!BuildConfig.DEBUG) return
setTwitterInstances()
updateStreamState()
}
private fun setTwitterInstances(): Boolean {
val accountsList = AccountUtils.getAllAccountDetails(AccountManager.get(this), true).filter { it.credentials is OAuthCredentials }
val accountKeys = accountsList.map { it.key }.toTypedArray()
val activatedPreferences = AccountPreferences.getAccountPreferences(this, accountKeys)
DebugLog.d(LOGTAG, "Setting up twitter stream instances")
this.accountKeys = accountKeys
clearTwitterInstances()
var result = false
accountsList.forEachIndexed { i, account ->
val preferences = activatedPreferences[i]
if (!preferences.isStreamingEnabled) {
return@forEachIndexed
}
val twitter = account.newMicroBlogInstance(context = this, cls = TwitterUserStream::class.java)
val callback = TwidereUserStreamCallback(this, account)
callbacks.put(account.key, callback)
object : Thread() {
override fun run() {
twitter.getUserStream(callback)
Log.d(LOGTAG, String.format("Stream %s disconnected", account.key))
callbacks.remove(account.key)
updateStreamState()
}
}.start()
result = result or true
}
return result
}
private fun updateStreamState() {
if (callbacks.size() > 0) {
val intent = Intent(this, SettingsActivity::class.java)
val contentIntent = PendingIntent.getActivity(this, 0, intent,
PendingIntent.FLAG_UPDATE_CURRENT)
val contentTitle = getString(R.string.app_name)
val contentText = getString(R.string.timeline_streaming_running)
val builder = NotificationCompat.Builder(this)
builder.setOngoing(true)
builder.setSmallIcon(R.drawable.ic_stat_refresh)
builder.setContentTitle(contentTitle)
builder.setContentText(contentText)
builder.setContentIntent(contentIntent)
notificationManager!!.notify(NOTIFICATION_SERVICE_STARTED, builder.build())
} else {
notificationManager!!.cancel(NOTIFICATION_SERVICE_STARTED)
}
}
internal class ShutdownStreamTwitterRunnable(private val callback: UserStreamCallback?) : Runnable {
override fun run() {
if (callback == null) return
Log.d(LOGTAG, "Disconnecting stream")
callback.disconnect()
}
}
internal class TwidereUserStreamCallback(
private val context: Context,
private val account: AccountDetails
) : UserStreamCallback() {
private var statusStreamStarted: Boolean = false
private val mentionsStreamStarted: Boolean = false
override fun onConnected() {
}
override fun onBlock(source: User, blockedUser: User) {
val message = String.format("%s blocked %s", source.screenName, blockedUser.screenName)
Log.d(LOGTAG, message)
}
override fun onDirectMessageDeleted(event: DeletionEvent) {
val where = Expression.equalsArgs(DirectMessages.MESSAGE_ID).sql
val whereArgs = arrayOf(event.id)
for (uri in MESSAGES_URIS) {
context.contentResolver.delete(uri, where, whereArgs)
}
}
override fun onStatusDeleted(event: DeletionEvent) {
val statusId = event.id
context.contentResolver.delete(Statuses.CONTENT_URI, Expression.equalsArgs(Statuses.STATUS_ID).sql,
arrayOf(statusId))
context.contentResolver.delete(Activities.AboutMe.CONTENT_URI, Expression.equalsArgs(Activities.STATUS_ID).sql,
arrayOf(statusId))
}
@Throws(IOException::class)
override fun onDirectMessage(directMessage: DirectMessage) {
if (directMessage.id == null) return
val resolver = context.contentResolver
val where = Expression.and(Expression.equalsArgs(DirectMessages.ACCOUNT_KEY),
Expression.equalsArgs(DirectMessages.MESSAGE_ID)).sql
val whereArgs = arrayOf(account.key.toString(), directMessage.id)
for (uri in MESSAGES_URIS) {
resolver.delete(uri, where, whereArgs)
}
val sender = directMessage.sender
val recipient = directMessage.recipient
if (TextUtils.equals(sender.id, account.key.id)) {
val values = ContentValuesCreator.createDirectMessage(directMessage,
account.key, true)
if (values != null) {
resolver.insert(DirectMessages.Outbox.CONTENT_URI, values)
}
}
if (TextUtils.equals(recipient.id, account.key.id)) {
val values = ContentValuesCreator.createDirectMessage(directMessage,
account.key, false)
val builder = DirectMessages.Inbox.CONTENT_URI.buildUpon()
builder.appendQueryParameter(QUERY_PARAM_NOTIFY, "true")
if (values != null) {
resolver.insert(builder.build(), values)
}
}
}
override fun onException(ex: Throwable) {
if (ex is MicroBlogException) {
Log.w(LOGTAG, String.format("Error %d", ex.statusCode), ex)
val response = ex.httpResponse
if (response != null) {
try {
val body = response.body
if (body != null) {
val os = ByteArrayOutputStream()
body.writeTo(os)
val charsetName: String
val contentType = body.contentType()
if (contentType != null) {
val charset = contentType.charset
if (charset != null) {
charsetName = charset.name()
} else {
charsetName = Charset.defaultCharset().name()
}
} else {
charsetName = Charset.defaultCharset().name()
}
Log.w(LOGTAG, os.toString(charsetName))
}
} catch (e: IOException) {
Log.w(LOGTAG, e)
}
}
} else {
Log.w(LOGTAG, ex)
}
}
override fun onFavorite(source: User, target: User, targetStatus: Status) {
val message = String.format("%s favorited %s's tweet: %s", source.screenName,
target.screenName, targetStatus.extendedText)
Log.d(LOGTAG, message)
}
override fun onFollow(source: User, followedUser: User) {
val message = String
.format("%s followed %s", source.screenName, followedUser.screenName)
Log.d(LOGTAG, message)
}
override fun onFriendList(friendIds: LongArray) {
}
override fun onScrubGeo(userId: Long, upToStatusId: Long) {
val resolver = context.contentResolver
val where = Expression.and(Expression.equalsArgs(Statuses.USER_KEY),
Expression.greaterEqualsArgs(Statuses.SORT_ID)).sql
val whereArgs = arrayOf(userId.toString(), upToStatusId.toString())
val values = ContentValues()
values.putNull(Statuses.LOCATION)
resolver.update(Statuses.CONTENT_URI, values, where, whereArgs)
}
override fun onStallWarning(warn: Warning) {
}
@Throws(IOException::class)
override fun onStatus(status: Status) {
val resolver = context.contentResolver
val values = ContentValuesCreator.createStatus(status, account.key)
if (!statusStreamStarted) {
statusStreamStarted = true
values.put(Statuses.IS_GAP, true)
}
val where = Expression.and(Expression.equalsArgs(AccountSupportColumns.ACCOUNT_KEY),
Expression.equalsArgs(Statuses.STATUS_ID)).sql
val whereArgs = arrayOf(account.key.toString(), status.id.toString())
resolver.delete(Statuses.CONTENT_URI, where, whereArgs)
resolver.delete(Mentions.CONTENT_URI, where, whereArgs)
resolver.insert(Statuses.CONTENT_URI, values)
val rt = status.retweetedStatus
if (rt != null && rt.extendedText.contains("@" + account.user.screen_name) ||
rt == null && status.extendedText.contains("@" + account.user.screen_name)) {
resolver.insert(Mentions.CONTENT_URI, values)
}
}
override fun onTrackLimitationNotice(numberOfLimitedStatuses: Int) {
}
override fun onUnblock(source: User, unblockedUser: User) {
val message = String.format("%s unblocked %s", source.screenName,
unblockedUser.screenName)
Log.d(LOGTAG, message)
}
override fun onUnfavorite(source: User, target: User, targetStatus: Status) {
val message = String.format("%s unfavorited %s's tweet: %s", source.screenName,
target.screenName, targetStatus.extendedText)
Log.d(LOGTAG, message)
}
override fun onUserListCreation(listOwner: User, list: UserList) {
}
override fun onUserListDeletion(listOwner: User, list: UserList) {
}
override fun onUserListMemberAddition(addedMember: User, listOwner: User, list: UserList) {
}
override fun onUserListMemberDeletion(deletedMember: User, listOwner: User, list: UserList) {
}
override fun onUserListSubscription(subscriber: User, listOwner: User, list: UserList) {
}
override fun onUserListUnsubscription(subscriber: User, listOwner: User, list: UserList) {
}
override fun onUserListUpdate(listOwner: User, list: UserList) {
}
override fun onUserProfileUpdate(updatedUser: User) {
}
}
companion object {
private val NOTIFICATION_SERVICE_STARTED = 1
private val MESSAGES_URIS = arrayOf(DirectMessages.Inbox.CONTENT_URI, DirectMessages.Outbox.CONTENT_URI)
}
}