Twidere-App-Android-Twitter.../twidere/src/main/kotlin/org/mariotaku/twidere/util/AsyncTwitterWrapper.kt

558 lines
22 KiB
Kotlin

/*
* Twidere - Twitter client for Android
*
* Copyright (C) 2012-2017 Mariotaku Lee <mariotaku.lee@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.mariotaku.twidere.util
import android.content.Context
import android.content.SharedPreferences
import android.net.Uri
import android.widget.Toast
import com.squareup.otto.Bus
import com.squareup.otto.Subscribe
import org.apache.commons.collections.primitives.ArrayIntList
import org.apache.commons.collections.primitives.ArrayLongList
import org.apache.commons.collections.primitives.IntList
import org.mariotaku.abstask.library.TaskStarter
import org.mariotaku.kpreferences.get
import org.mariotaku.ktextension.mapToArray
import org.mariotaku.ktextension.toNulls
import org.mariotaku.microblog.library.MicroBlog
import org.mariotaku.microblog.library.MicroBlogException
import org.mariotaku.microblog.library.twitter.model.*
import org.mariotaku.sqliteqb.library.Expression
import org.mariotaku.twidere.R
import org.mariotaku.twidere.TwidereConstants
import org.mariotaku.twidere.constant.homeRefreshDirectMessagesKey
import org.mariotaku.twidere.constant.homeRefreshMentionsKey
import org.mariotaku.twidere.constant.homeRefreshSavedSearchesKey
import org.mariotaku.twidere.constant.nameFirstKey
import org.mariotaku.twidere.extension.model.api.microblog.toParcelable
import org.mariotaku.twidere.extension.model.newMicroBlogInstance
import org.mariotaku.twidere.model.*
import org.mariotaku.twidere.model.event.*
import org.mariotaku.twidere.model.pagination.SinceMaxPagination
import org.mariotaku.twidere.model.util.AccountUtils
import org.mariotaku.twidere.model.util.ParcelableRelationshipUtils
import org.mariotaku.twidere.provider.TwidereDataStore.*
import org.mariotaku.twidere.task.*
import org.mariotaku.twidere.task.twitter.GetActivitiesAboutMeTask
import org.mariotaku.twidere.task.twitter.GetHomeTimelineTask
import org.mariotaku.twidere.task.twitter.GetSavedSearchesTask
import org.mariotaku.twidere.task.twitter.GetTrendsTask
import org.mariotaku.twidere.task.twitter.message.GetMessagesTask
import org.mariotaku.twidere.util.collection.CompactHashSet
class AsyncTwitterWrapper(
val context: Context,
private val bus: Bus,
private val preferences: SharedPreferences,
private val notificationManager: NotificationManagerWrapper
) {
private val resolver = context.contentResolver
var destroyingStatusIds: IntList = ArrayIntList()
private val updatingRelationshipIds = ArrayIntList()
private val sendingDraftIds = ArrayLongList()
private val getMessageTasks = CompactHashSet<Uri>()
private val getStatusTasks = CompactHashSet<Uri>()
init {
bus.register(object : Any() {
@Subscribe
fun onGetDirectMessagesTaskEvent(event: GetMessagesTaskEvent) {
if (event.running) {
getMessageTasks.add(event.uri)
} else {
getMessageTasks.remove(event.uri)
}
}
@Subscribe
fun onGetStatusesTaskEvent(event: GetStatusesTaskEvent) {
if (event.running) {
getStatusTasks.add(event.uri)
} else {
getStatusTasks.remove(event.uri)
}
}
})
}
fun acceptFriendshipAsync(accountKey: UserKey, userKey: UserKey) {
val task = AcceptFriendshipTask(context)
task.setup(accountKey, userKey)
TaskStarter.execute(task)
}
fun addSendingDraftId(id: Long) {
synchronized(sendingDraftIds) {
sendingDraftIds.add(id)
resolver.notifyChange(Drafts.CONTENT_URI_UNSENT, null)
}
}
fun addUserListMembersAsync(accountKey: UserKey, listId: String, vararg users: ParcelableUser) {
val task = AddUserListMembersTask(context, accountKey, listId, users)
TaskStarter.execute(task)
}
fun cancelRetweetAsync(accountKey: UserKey, statusId: String?, myRetweetId: String?) {
if (myRetweetId != null) {
destroyStatusAsync(accountKey, myRetweetId)
} else if (statusId != null) {
destroyStatusAsync(accountKey, statusId)
}
}
fun clearNotificationAsync(notificationType: Int) {
clearNotificationAsync(notificationType, null)
}
fun clearNotificationAsync(notificationId: Int, accountKey: UserKey?) {
notificationManager.cancelById(Utils.getNotificationId(notificationId, accountKey))
}
fun createBlockAsync(accountKey: UserKey, userKey: UserKey, filterEverywhere: Boolean) {
val task = CreateUserBlockTask(context, filterEverywhere)
task.setup(accountKey, userKey)
TaskStarter.execute(task)
}
fun createFavoriteAsync(accountKey: UserKey, status: ParcelableStatus) {
val task = CreateFavoriteTask(context, accountKey, status)
TaskStarter.execute(task)
}
fun createFriendshipAsync(accountKey: UserKey, userKey: UserKey, screenName: String) {
val task = CreateFriendshipTask(context)
task.setup(accountKey, userKey, screenName)
TaskStarter.execute(task)
}
fun createMultiBlockAsync(accountKey: UserKey, userIds: Array<String>) {
}
fun createMuteAsync(accountKey: UserKey, userKey: UserKey, filterEverywhere: Boolean) {
val task = CreateUserMuteTask(context, filterEverywhere)
task.setup(accountKey, userKey)
TaskStarter.execute(task)
}
fun createSavedSearchAsync(accountKey: UserKey, query: String) {
val task = CreateSavedSearchTask(context, accountKey, query)
TaskStarter.execute(task)
}
fun createUserListAsync(accountKey: UserKey, listName: String, isPublic: Boolean,
description: String) {
val task = CreateUserListTask(context, accountKey, listName, isPublic,
description)
TaskStarter.execute(task)
}
fun createUserListSubscriptionAsync(accountKey: UserKey, listId: String) {
val task = CreateUserListSubscriptionTask(context, accountKey, listId)
TaskStarter.execute(task)
}
fun deleteUserListMembersAsync(accountKey: UserKey, listId: String, users: Array<ParcelableUser>) {
val task = DeleteUserListMembersTask(context, accountKey, listId, users)
TaskStarter.execute(task)
}
fun denyFriendshipAsync(accountKey: UserKey, userKey: UserKey) {
val task = DenyFriendshipTask(context)
task.setup(accountKey, userKey)
TaskStarter.execute(task)
}
fun destroyBlockAsync(accountKey: UserKey, userKey: UserKey) {
val task = DestroyUserBlockTask(context)
task.setup(accountKey, userKey)
TaskStarter.execute(task)
}
fun destroyFavoriteAsync(accountKey: UserKey, statusId: String) {
val task = DestroyFavoriteTask(context, accountKey, statusId)
TaskStarter.execute(task)
}
fun destroyFriendshipAsync(accountKey: UserKey, userKey: UserKey) {
val task = DestroyFriendshipTask(context)
task.setup(accountKey, userKey)
TaskStarter.execute(task)
}
fun destroyMuteAsync(accountKey: UserKey, userKey: UserKey) {
val task = DestroyUserMuteTask(context)
task.setup(accountKey, userKey)
TaskStarter.execute(task)
}
fun destroySavedSearchAsync(accountKey: UserKey, searchId: Long) {
val task = DestroySavedSearchTask(context, accountKey, searchId)
TaskStarter.execute(task)
}
fun destroyStatusAsync(accountKey: UserKey, statusId: String) {
val task = DestroyStatusTask(context, accountKey, statusId)
TaskStarter.execute(task)
}
fun destroyUserListAsync(accountKey: UserKey, listId: String) {
val task = DestroyUserListTask(context, accountKey, listId)
TaskStarter.execute(task)
}
fun destroyUserListSubscriptionAsync(accountKey: UserKey, listId: String) {
val task = DestroyUserListSubscriptionTask(context, accountKey, listId)
TaskStarter.execute(task)
}
fun getHomeTimelineAsync(param: RefreshTaskParam): Boolean {
val task = GetHomeTimelineTask(context)
task.params = param
TaskStarter.execute(task)
return true
}
fun getLocalTrendsAsync(accountKey: UserKey, woeId: Int) {
val task = GetTrendsTask(context, accountKey, woeId)
TaskStarter.execute(task)
}
fun getMessagesAsync(param: GetMessagesTask.RefreshMessagesTaskParam) {
val task = GetMessagesTask(context)
task.params = param
TaskStarter.execute(task)
}
fun getSavedSearchesAsync(accountKeys: Array<UserKey>) {
val task = GetSavedSearchesTask(context)
task.params = accountKeys
TaskStarter.execute(task)
}
fun getSendingDraftIds(): LongArray {
return sendingDraftIds.toArray()
}
fun isDestroyingStatus(accountKey: UserKey?, statusId: String?): Boolean {
return destroyingStatusIds.contains(calculateHashCode(accountKey, statusId))
}
fun isStatusTimelineRefreshing(uri: Uri): Boolean {
return getStatusTasks.contains(uri)
}
fun refreshAll() {
refreshAll { DataStoreUtils.getActivatedAccountKeys(context) }
}
fun refreshAll(accountKeys: Array<UserKey>): Boolean {
return refreshAll { accountKeys }
}
fun refreshAll(action: () -> Array<UserKey>): Boolean {
getHomeTimelineAsync(object : RefreshTaskParam {
override val accountKeys by lazy { action() }
override val pagination by lazy {
return@lazy DataStoreUtils.getNewestStatusIds(context, Statuses.CONTENT_URI,
accountKeys.toNulls()).mapToArray {
return@mapToArray SinceMaxPagination.sinceId(it, -1)
}
}
})
if (preferences[homeRefreshMentionsKey]) {
getActivitiesAboutMeAsync(object : RefreshTaskParam {
override val accountKeys by lazy { action() }
override val pagination by lazy {
return@lazy DataStoreUtils.getRefreshNewestActivityMaxPositions(context,
Activities.AboutMe.CONTENT_URI, accountKeys.toNulls()).mapToArray {
return@mapToArray SinceMaxPagination.sinceId(it, -1)
}
}
})
}
if (preferences[homeRefreshDirectMessagesKey]) {
getMessagesAsync(object : GetMessagesTask.RefreshMessagesTaskParam(context) {
override val accountKeys by lazy { action() }
})
}
if (preferences[homeRefreshSavedSearchesKey]) {
getSavedSearchesAsync(action())
}
return true
}
fun removeSendingDraftId(id: Long) {
synchronized(sendingDraftIds) {
sendingDraftIds.removeElement(id)
resolver.notifyChange(Drafts.CONTENT_URI_UNSENT, null)
}
}
fun reportMultiSpam(accountKey: UserKey, userIds: Array<String>) {
// TODO implementation
}
fun reportSpamAsync(accountKey: UserKey, userKey: UserKey) {
val task = ReportSpamAndBlockTask(context)
task.setup(accountKey, userKey)
TaskStarter.execute(task)
}
fun retweetStatusAsync(accountKey: UserKey, status: ParcelableStatus) {
val task = RetweetStatusTask(context, accountKey, status)
TaskStarter.execute<Any, SingleResponse<ParcelableStatus>, Any>(task)
}
fun updateUserListDetails(accountKey: UserKey, listId: String, update: UserListUpdate) {
val task = UpdateUserListDetailsTask(context, accountKey, listId, update)
TaskStarter.execute(task)
}
fun updateFriendship(accountKey: UserKey, userKey: UserKey, update: FriendshipUpdate) {
TaskStarter.execute(object : ExceptionHandlingAbstractTask<Any?, Relationship, Exception, Any>(context) {
override val exceptionClass = Exception::class.java
override fun onExecute(params: Any?): Relationship {
val microBlog = MicroBlogAPIFactory.getInstance(context, accountKey)
?: throw MicroBlogException("No account")
val relationship = microBlog.updateFriendship(userKey.id, update)
val cr = context.contentResolver
if (!relationship.isSourceWantRetweetsFromTarget) {
// TODO remove cached retweets
val where = Expression.and(
Expression.equalsArgs(Statuses.ACCOUNT_KEY),
Expression.equalsArgs(Statuses.RETWEETED_BY_USER_KEY)
)
val selectionArgs = arrayOf(accountKey.toString(), userKey.toString())
cr.delete(Statuses.CONTENT_URI, where.sql, selectionArgs)
}
ParcelableRelationshipUtils.insert(cr, listOf(ParcelableRelationshipUtils
.create(accountKey, userKey, relationship)))
return relationship
}
override fun onSucceed(callback: Any?, result: Relationship) {
bus.post(FriendshipUpdatedEvent(accountKey, userKey, result))
}
override fun onException(callback: Any?, exception: Exception) {
if (exception !is MicroBlogException) {
Analyzer.logException(exception)
return
}
DebugLog.w(TwidereConstants.LOGTAG, "Unable to update friendship", exception)
}
})
}
fun getActivitiesAboutMeAsync(param: RefreshTaskParam) {
val task = GetActivitiesAboutMeTask(context)
task.params = param
TaskStarter.execute(task)
}
fun setActivitiesAboutMeUnreadAsync(accountKeys: Array<UserKey>, cursor: Long) {
val task = object : ExceptionHandlingAbstractTask<Any?, Unit, MicroBlogException, Any?>(context) {
override val exceptionClass = MicroBlogException::class.java
override fun onExecute(params: Any?) {
for (accountKey in accountKeys) {
val microBlog = MicroBlogAPIFactory.getInstance(context, accountKey) ?: continue
if (!AccountUtils.isOfficial(context, accountKey)) continue
microBlog.setActivitiesAboutMeUnread(cursor)
}
}
}
TaskStarter.execute(task)
}
fun addUpdatingRelationshipId(accountKey: UserKey, userKey: UserKey) {
updatingRelationshipIds.add(ParcelableUser.calculateHashCode(accountKey, userKey))
}
fun removeUpdatingRelationshipId(accountKey: UserKey, userKey: UserKey) {
updatingRelationshipIds.removeElement(ParcelableUser.calculateHashCode(accountKey, userKey))
}
fun isUpdatingRelationship(accountKey: UserKey, userKey: UserKey): Boolean {
return updatingRelationshipIds.contains(ParcelableUser.calculateHashCode(accountKey, userKey))
}
internal class CreateSavedSearchTask(context: Context, accountKey: UserKey,
private val query: String) : AbsAccountRequestTask<Any?, SavedSearch, Any?>(context,
accountKey) {
override fun onExecute(account: AccountDetails, params: Any?): SavedSearch {
val microBlog = account.newMicroBlogInstance(context, MicroBlog::class.java)
return microBlog.createSavedSearch(query)
}
override fun onSucceed(callback: Any?, result: SavedSearch) {
val message = context.getString(R.string.message_toast_search_name_saved, result.query)
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
}
override fun onException(callback: Any?, exception: MicroBlogException) {
if (exception.statusCode == 403) {
Toast.makeText(context, R.string.saved_searches_already_saved_hint,
Toast.LENGTH_SHORT).show()
return
}
super.onException(callback, exception)
}
}
internal class CreateUserListSubscriptionTask(context: Context, accountKey: UserKey,
private val listId: String) : AbsAccountRequestTask<Any?, ParcelableUserList, Any?>(context, accountKey) {
override fun onExecute(account: AccountDetails, params: Any?): ParcelableUserList {
val microBlog = account.newMicroBlogInstance(context, MicroBlog::class.java)
val userList = microBlog.createUserListSubscription(listId)
return userList.toParcelable(account.key)
}
override fun onSucceed(callback: Any?, result: ParcelableUserList) {
val message = context.getString(R.string.subscribed_to_list, result.name)
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
bus.post(UserListSubscriptionEvent(UserListSubscriptionEvent.Action.SUBSCRIBE, result))
}
}
internal class CreateUserListTask(context: Context, accountKey: UserKey,
private val listName: String, private val isPublic: Boolean,
private val description: String) : AbsAccountRequestTask<Any?, ParcelableUserList, Any?>(context, accountKey) {
override fun onExecute(account: AccountDetails, params: Any?): ParcelableUserList {
val microBlog = account.newMicroBlogInstance(context, MicroBlog::class.java)
val userListUpdate = UserListUpdate()
userListUpdate.setName(listName)
userListUpdate.setMode(if (isPublic) UserList.Mode.PUBLIC else UserList.Mode.PRIVATE)
userListUpdate.setDescription(description)
val list = microBlog.createUserList(userListUpdate)
return list.toParcelable(account.key)
}
override fun onSucceed(callback: Any?, result: ParcelableUserList) {
val message = context.getString(R.string.created_list, result.name)
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
bus.post(UserListCreatedEvent(result))
}
}
internal class DeleteUserListMembersTask(
context: Context,
accountKey: UserKey,
private val userListId: String,
private val users: Array<ParcelableUser>
) : AbsAccountRequestTask<Any?, ParcelableUserList, Any?>(context, accountKey) {
override fun onExecute(account: AccountDetails, params: Any?): ParcelableUserList {
val microBlog = account.newMicroBlogInstance(context, MicroBlog::class.java)
val userKeys = users.mapToArray(ParcelableUser::key)
val userList = microBlog.deleteUserListMembers(userListId, UserKey.getIds(userKeys))
return userList.toParcelable(account.key)
}
override fun onSucceed(callback: Any?, result: ParcelableUserList) {
val message = if (users.size == 1) {
val user = users[0]
val nameFirst = preferences[nameFirstKey]
val displayName = userColorNameManager.getDisplayName(user.key,
user.name, user.screen_name, nameFirst)
context.getString(R.string.deleted_user_from_list, displayName,
result.name)
} else {
context.resources.getQuantityString(R.plurals.deleted_N_users_from_list, users.size,
users.size, result.name)
}
bus.post(UserListMembersChangedEvent(UserListMembersChangedEvent.Action.REMOVED,
result, users))
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
}
}
internal class DestroySavedSearchTask(
context: Context,
accountKey: UserKey,
private val searchId: Long
) : AbsAccountRequestTask<Any?, SavedSearch, Any?>(context, accountKey) {
override fun onExecute(account: AccountDetails, params: Any?): SavedSearch {
val microBlog = account.newMicroBlogInstance(context, MicroBlog::class.java)
return microBlog.destroySavedSearch(searchId)
}
override fun onSucceed(callback: Any?, result: SavedSearch) {
val message = context.getString(R.string.message_toast_search_name_deleted, result.query)
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
bus.post(SavedSearchDestroyedEvent(accountKey, searchId))
}
}
internal class DestroyUserListSubscriptionTask(
context: Context,
accountKey: UserKey,
private val listId: String
) : AbsAccountRequestTask<Any?, ParcelableUserList, Any?>(context, accountKey) {
override fun onExecute(account: AccountDetails, params: Any?): ParcelableUserList {
val microBlog = account.newMicroBlogInstance(context, MicroBlog::class.java)
val userList = microBlog.destroyUserListSubscription(listId)
return userList.toParcelable(account.key)
}
override fun onSucceed(callback: Any?, result: ParcelableUserList) {
val message = context.getString(R.string.unsubscribed_from_list, result.name)
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
bus.post(UserListSubscriptionEvent(UserListSubscriptionEvent.Action.UNSUBSCRIBE, result))
}
}
companion object {
fun calculateHashCode(accountKey: UserKey?, statusId: String?): Int {
return (accountKey?.hashCode() ?: 0) xor (statusId?.hashCode() ?: 0)
}
fun <T : Response<*>> getException(responses: List<T>): Exception? {
return responses.firstOrNull { it.hasException() }?.exception
}
}
}