implemented mastodon home timeline

This commit is contained in:
Mariotaku Lee 2017-04-22 01:06:07 +08:00
parent 8fc93f1d2e
commit dad16bdc3b
No known key found for this signature in database
GPG Key ID: 15C10F89D7C33535
20 changed files with 164 additions and 113 deletions

View File

@ -18,27 +18,27 @@
package org.mariotaku.microblog.library.mastodon;
import org.mariotaku.microblog.library.mastodon.api.AccountResources;
import org.mariotaku.microblog.library.mastodon.api.ApplicationResources;
import org.mariotaku.microblog.library.mastodon.api.BlockResources;
import org.mariotaku.microblog.library.mastodon.api.FavouriteResources;
import org.mariotaku.microblog.library.mastodon.api.FollowRequestResources;
import org.mariotaku.microblog.library.mastodon.api.FollowResources;
import org.mariotaku.microblog.library.mastodon.api.InstanceResources;
import org.mariotaku.microblog.library.mastodon.api.AccountsResources;
import org.mariotaku.microblog.library.mastodon.api.AppsResources;
import org.mariotaku.microblog.library.mastodon.api.BlocksResources;
import org.mariotaku.microblog.library.mastodon.api.FavouritesResources;
import org.mariotaku.microblog.library.mastodon.api.FollowRequestsResources;
import org.mariotaku.microblog.library.mastodon.api.FollowsResources;
import org.mariotaku.microblog.library.mastodon.api.InstancesResources;
import org.mariotaku.microblog.library.mastodon.api.MediaResources;
import org.mariotaku.microblog.library.mastodon.api.MuteResources;
import org.mariotaku.microblog.library.mastodon.api.NotificationResources;
import org.mariotaku.microblog.library.mastodon.api.ReportResources;
import org.mariotaku.microblog.library.mastodon.api.MutesResources;
import org.mariotaku.microblog.library.mastodon.api.NotificationsResources;
import org.mariotaku.microblog.library.mastodon.api.ReportsResources;
import org.mariotaku.microblog.library.mastodon.api.SearchResources;
import org.mariotaku.microblog.library.mastodon.api.StatusResources;
import org.mariotaku.microblog.library.mastodon.api.TimelineResources;
import org.mariotaku.microblog.library.mastodon.api.StatusesResources;
import org.mariotaku.microblog.library.mastodon.api.TimelinesResources;
/**
* Created by mariotaku on 2017/4/17.
*/
public interface Mastodon extends AccountResources, ApplicationResources, BlockResources,
FavouriteResources, FollowRequestResources, FollowResources, InstanceResources,
MediaResources, MuteResources, NotificationResources, ReportResources, SearchResources,
StatusResources, TimelineResources {
public interface Mastodon extends AccountsResources, AppsResources, BlocksResources,
FavouritesResources, FollowRequestsResources, FollowsResources, InstancesResources,
MediaResources, MutesResources, NotificationsResources, ReportsResources, SearchResources,
StatusesResources, TimelinesResources {
}

View File

@ -39,7 +39,7 @@ import org.mariotaku.restfu.annotation.param.Query;
* Created by mariotaku on 2017/4/17.
*/
public interface AccountResources {
public interface AccountsResources {
@GET("/v1/accounts/{id}")
Account getAccount(@Path("id") String id) throws MicroBlogException;

View File

@ -29,7 +29,7 @@ import org.mariotaku.restfu.annotation.param.Param;
* Created by mariotaku on 2017/4/17.
*/
public interface ApplicationResources {
public interface AppsResources {
@POST("/v1/apps")
RegisteredApplication registerApplication(@Param("client_name") String clientName,
@Param("redirect_uris") String redirectUris,

View File

@ -28,7 +28,7 @@ import org.mariotaku.restfu.annotation.param.Query;
* Created by mariotaku on 2017/4/17.
*/
public interface BlockResources {
public interface BlocksResources {
@GET("/v1/blocks")
LinkHeaderList<Account> getBlocks(@Query Paging paging);
}

View File

@ -29,7 +29,7 @@ import org.mariotaku.restfu.annotation.param.Query;
* Created by mariotaku on 2017/4/17.
*/
public interface FavouriteResources {
public interface FavouritesResources {
/**
* @return An array of {@link Status} favourited by the authenticated user.

View File

@ -27,7 +27,7 @@ import org.mariotaku.restfu.annotation.param.Query;
/**
* Created by mariotaku on 2017/4/17.
*/
public interface FollowRequestResources {
public interface FollowRequestsResources {
@GET("/v1/follow_requests")
LinkHeaderList<Account> getFollowRequests(@Query Paging paging);
}

View File

@ -22,5 +22,5 @@ package org.mariotaku.microblog.library.mastodon.api;
* Created by mariotaku on 2017/4/17.
*/
public interface ReportResources {
public interface FollowsResources {
}

View File

@ -1,26 +0,0 @@
/*
* Twidere - Twitter client for Android
*
* Copyright 2012-2017 Mariotaku Lee <mariotaku.lee@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.mariotaku.microblog.library.mastodon.api;
/**
* Created by mariotaku on 2017/4/17.
*/
public interface InstanceResources {
}

View File

@ -22,5 +22,5 @@ package org.mariotaku.microblog.library.mastodon.api;
* Created by mariotaku on 2017/4/17.
*/
public interface TimelineResources {
public interface InstancesResources {
}

View File

@ -28,7 +28,7 @@ import org.mariotaku.restfu.annotation.param.Query;
* Created by mariotaku on 2017/4/17.
*/
public interface MuteResources {
public interface MutesResources {
@GET("/v1/mutes")
LinkHeaderList<Account> getMutes(@Query Paging paging);
}

View File

@ -22,5 +22,5 @@ package org.mariotaku.microblog.library.mastodon.api;
* Created by mariotaku on 2017/4/17.
*/
public interface NotificationResources {
public interface NotificationsResources {
}

View File

@ -22,5 +22,5 @@ package org.mariotaku.microblog.library.mastodon.api;
* Created by mariotaku on 2017/4/17.
*/
public interface FollowResources {
public interface ReportsResources {
}

View File

@ -34,7 +34,7 @@ import org.mariotaku.restfu.annotation.param.Path;
* Created by mariotaku on 2017/4/17.
*/
public interface StatusResources {
public interface StatusesResources {
@GET("/v1/statuses/{id}")
Status fetchStatus(@Path("id") String id) throws MicroBlogException;

View File

@ -0,0 +1,44 @@
/*
* Twidere - Twitter client for Android
*
* Copyright 2012-2017 Mariotaku Lee <mariotaku.lee@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.mariotaku.microblog.library.mastodon.api;
import org.mariotaku.microblog.library.MicroBlogException;
import org.mariotaku.microblog.library.mastodon.model.LinkHeaderList;
import org.mariotaku.microblog.library.mastodon.model.Status;
import org.mariotaku.microblog.library.twitter.model.Paging;
import org.mariotaku.restfu.annotation.method.GET;
import org.mariotaku.restfu.annotation.param.Path;
import org.mariotaku.restfu.annotation.param.Query;
/**
* Created by mariotaku on 2017/4/17.
*/
public interface TimelinesResources {
@GET("/v1/timelines/home")
LinkHeaderList<Status> getHomeTimeline(@Query Paging paging) throws MicroBlogException;
@GET("/v1/timelines/public")
LinkHeaderList<Status> getPublicTimeline(@Query Paging paging, @Query("local") boolean local)
throws MicroBlogException;
@GET("/v1/timelines/tag/{tag}")
LinkHeaderList<Status> getHashtagTimeline(@Path("tag") String hashtag, @Query Paging paging,
@Query("local") boolean local) throws MicroBlogException;
}

View File

@ -23,9 +23,7 @@ import org.mariotaku.twidere.model.RefreshTaskParam
import org.mariotaku.twidere.model.pagination.SinceMaxPagination
val RefreshTaskParam.hasMaxIds: Boolean
get() {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
get() = pagination?.any { (it as? SinceMaxPagination)?.maxId != null } ?: false
fun RefreshTaskParam.getMaxId(index: Int): String? {
return (pagination?.get(index) as? SinceMaxPagination)?.maxId

View File

@ -385,6 +385,9 @@ class AccountsDashboardFragment : BaseFragment(), LoaderCallbacks<AccountsInfo>,
AccountType.FANFOU -> {
hasPublicTimeline = !hasPublicTimelineTab
}
AccountType.MASTODON -> {
hasPublicTimeline = !hasPublicTimelineTab
}
}
menu.setItemAvailability(R.id.groups, hasGroups)
menu.setItemAvailability(R.id.lists, hasLists)

View File

@ -24,8 +24,12 @@ import android.database.sqlite.SQLiteDatabase
import android.support.annotation.WorkerThread
import org.mariotaku.microblog.library.MicroBlog
import org.mariotaku.microblog.library.MicroBlogException
import org.mariotaku.microblog.library.mastodon.Mastodon
import org.mariotaku.microblog.library.twitter.model.Paging
import org.mariotaku.twidere.annotation.AccountType
import org.mariotaku.twidere.exception.APINotSupportedException
import org.mariotaku.twidere.extension.model.api.mastodon.mapToPaginated
import org.mariotaku.twidere.extension.model.api.mastodon.toParcelable
import org.mariotaku.twidere.extension.model.api.toParcelable
import org.mariotaku.twidere.extension.model.newMicroBlogInstance
import org.mariotaku.twidere.model.AccountDetails
@ -47,13 +51,19 @@ class NetworkPublicTimelineLoader(
@Throws(MicroBlogException::class)
override fun getStatuses(account: AccountDetails, paging: Paging): PaginatedList<ParcelableStatus> {
when (account.type) {
AccountType.MASTODON -> {
val mastodon = account.newMicroBlogInstance(context, Mastodon::class.java)
return mastodon.getPublicTimeline(paging, true).mapToPaginated {
it.toParcelable(account.key)
}
}
AccountType.STATUSNET -> {
val microBlog = account.newMicroBlogInstance(context, MicroBlog::class.java)
return microBlog.getNetworkPublicTimeline(paging).mapMicroBlogToPaginated {
it.toParcelable(account.key, account.type, profileImageSize = profileImageSize)
}
}
else -> throw MicroBlogException("STUB")
else -> throw APINotSupportedException(account.type)
}
}

View File

@ -24,8 +24,11 @@ import android.database.sqlite.SQLiteDatabase
import android.support.annotation.WorkerThread
import org.mariotaku.microblog.library.MicroBlog
import org.mariotaku.microblog.library.MicroBlogException
import org.mariotaku.microblog.library.mastodon.Mastodon
import org.mariotaku.microblog.library.twitter.model.Paging
import org.mariotaku.twidere.annotation.AccountType
import org.mariotaku.twidere.extension.model.api.mastodon.mapToPaginated
import org.mariotaku.twidere.extension.model.api.mastodon.toParcelable
import org.mariotaku.twidere.extension.model.api.toParcelable
import org.mariotaku.twidere.extension.model.newMicroBlogInstance
import org.mariotaku.twidere.model.AccountDetails
@ -47,7 +50,12 @@ class PublicTimelineLoader(
@Throws(MicroBlogException::class)
override fun getStatuses(account: AccountDetails, paging: Paging): PaginatedList<ParcelableStatus> {
when (account.type) {
AccountType.MASTODON -> throw MicroBlogException("STUB")
AccountType.MASTODON -> {
val mastodon = account.newMicroBlogInstance(context, Mastodon::class.java)
return mastodon.getPublicTimeline(paging, true).mapToPaginated {
it.toParcelable(account.key)
}
}
else -> {
val microBlog = account.newMicroBlogInstance(context, MicroBlog::class.java)
return microBlog.getPublicTimeline(paging).mapMicroBlogToPaginated {

View File

@ -23,11 +23,16 @@ import android.content.Context
import android.net.Uri
import org.mariotaku.microblog.library.MicroBlog
import org.mariotaku.microblog.library.MicroBlogException
import org.mariotaku.microblog.library.mastodon.Mastodon
import org.mariotaku.microblog.library.twitter.model.Paging
import org.mariotaku.microblog.library.twitter.model.ResponseList
import org.mariotaku.microblog.library.twitter.model.Status
import org.mariotaku.twidere.R
import org.mariotaku.twidere.annotation.AccountType
import org.mariotaku.twidere.annotation.ReadPositionTag
import org.mariotaku.twidere.extension.model.api.mastodon.toParcelable
import org.mariotaku.twidere.extension.model.api.toParcelable
import org.mariotaku.twidere.extension.model.newMicroBlogInstance
import org.mariotaku.twidere.model.AccountDetails
import org.mariotaku.twidere.model.ParcelableStatus
import org.mariotaku.twidere.model.UserKey
import org.mariotaku.twidere.provider.TwidereDataStore.Statuses
import org.mariotaku.twidere.util.ErrorInfoStore
@ -45,12 +50,27 @@ class GetHomeTimelineTask(context: Context) : GetStatusesTask(context) {
override val errorInfoKey: String
get() = ErrorInfoStore.KEY_HOME_TIMELINE
private val profileImageSize = context.getString(R.string.profile_image_size)
@Throws(MicroBlogException::class)
override fun getStatuses(twitter: MicroBlog, paging: Paging): ResponseList<Status> {
return twitter.getHomeTimeline(paging)
override fun getStatuses(account: AccountDetails, paging: Paging): List<ParcelableStatus> {
when (account.type) {
AccountType.MASTODON -> {
val mastodon = account.newMicroBlogInstance(context, Mastodon::class.java)
return mastodon.getHomeTimeline(paging).map {
it.toParcelable(account.key)
}
}
else -> {
val microBlog = account.newMicroBlogInstance(context, MicroBlog::class.java)
return microBlog.getHomeTimeline(paging).map {
it.toParcelable(account.key, account.type, profileImageSize)
}
}
}
}
override fun setLocalReadPosition(accountKey: UserKey, details: AccountDetails, twitter: MicroBlog) {
override fun setLocalReadPosition(accountKey: UserKey, details: AccountDetails) {
val syncManager = timelineSyncManagerFactory.get() ?: return
try {
val tag = Utils.getReadPositionTagWithAccount(ReadPositionTag.HOME_TIMELINE, accountKey)

View File

@ -4,65 +4,60 @@ import android.accounts.AccountManager
import android.content.ContentValues
import android.content.Context
import android.net.Uri
import org.mariotaku.abstask.library.TaskStarter
import org.mariotaku.kpreferences.get
import org.mariotaku.ktextension.toLongOr
import org.mariotaku.library.objectcursor.ObjectCursor
import org.mariotaku.microblog.library.MicroBlog
import org.mariotaku.microblog.library.MicroBlogException
import org.mariotaku.microblog.library.twitter.model.Paging
import org.mariotaku.microblog.library.twitter.model.ResponseList
import org.mariotaku.microblog.library.twitter.model.Status
import org.mariotaku.sqliteqb.library.Columns
import org.mariotaku.sqliteqb.library.Expression
import org.mariotaku.twidere.R
import org.mariotaku.twidere.TwidereConstants.LOGTAG
import org.mariotaku.twidere.TwidereConstants.QUERY_PARAM_NOTIFY_CHANGE
import org.mariotaku.twidere.constant.loadItemLimitKey
import org.mariotaku.twidere.extension.model.*
import org.mariotaku.twidere.extension.model.api.toParcelable
import org.mariotaku.twidere.model.*
import org.mariotaku.twidere.extension.model.api.applyLoadLimit
import org.mariotaku.twidere.extension.model.getMaxId
import org.mariotaku.twidere.extension.model.getMaxSortId
import org.mariotaku.twidere.extension.model.getSinceId
import org.mariotaku.twidere.extension.model.getSinceSortId
import org.mariotaku.twidere.model.AccountDetails
import org.mariotaku.twidere.model.ParcelableStatus
import org.mariotaku.twidere.model.RefreshTaskParam
import org.mariotaku.twidere.model.UserKey
import org.mariotaku.twidere.model.event.GetStatusesTaskEvent
import org.mariotaku.twidere.model.util.AccountUtils
import org.mariotaku.twidere.model.util.ParcelableStatusUtils
import org.mariotaku.twidere.provider.TwidereDataStore.AccountSupportColumns
import org.mariotaku.twidere.provider.TwidereDataStore.Statuses
import org.mariotaku.twidere.task.BaseAbstractTask
import org.mariotaku.twidere.task.cache.CacheUsersStatusesTask
import org.mariotaku.twidere.util.*
import org.mariotaku.twidere.util.DataStoreUtils
import org.mariotaku.twidere.util.DebugLog
import org.mariotaku.twidere.util.ErrorInfoStore
import org.mariotaku.twidere.util.UriUtils
import org.mariotaku.twidere.util.content.ContentResolverUtils
import java.util.*
/**
* Created by mariotaku on 16/1/2.
*/
abstract class GetStatusesTask(
context: Context
) : BaseAbstractTask<RefreshTaskParam, List<StatusListResponse>, (Boolean) -> Unit>(context) {
private val profileImageSize = context.getString(R.string.profile_image_size)
@Throws(MicroBlogException::class)
abstract fun getStatuses(twitter: MicroBlog, paging: Paging): ResponseList<Status>
) : BaseAbstractTask<RefreshTaskParam, List<GetStatusesTask.GetStatusesResult?>, (Boolean) -> Unit>(context) {
protected abstract val contentUri: Uri
protected abstract val errorInfoKey: String
override fun doLongOperation(param: RefreshTaskParam): List<StatusListResponse> {
override fun doLongOperation(param: RefreshTaskParam): List<GetStatusesResult?> {
if (param.shouldAbort) return emptyList()
val accountKeys = param.accountKeys
val result = ArrayList<StatusListResponse>()
val loadItemLimit = preferences[loadItemLimitKey]
var saveReadPosition = false
for (i in 0 until accountKeys.size) {
val accountKey = accountKeys[i]
val details = AccountUtils.getAccountDetails(AccountManager.get(context),
accountKey, true) ?: continue
val microBlog = details.newMicroBlogInstance(context = context, cls = MicroBlog::class.java)
return accountKeys.mapIndexed { i, accountKey ->
val account = AccountUtils.getAccountDetails(AccountManager.get(context),
accountKey, true) ?: return@mapIndexed null
try {
val paging = Paging()
paging.count(loadItemLimit)
paging.applyLoadLimit(account, loadItemLimit)
val maxId = param.getMaxId(i)
val sinceId = param.getSinceId(i)
val maxSortId = param.getMaxSortId(i)
@ -84,20 +79,18 @@ abstract class GetStatusesTask(
}
saveReadPosition = true
}
val statuses = getStatuses(microBlog, paging)
val storeResult = storeStatus(accountKey, details, statuses, sinceId, maxId,
sinceSortId, maxSortId, loadItemLimit, false)
val statuses = getStatuses(account, paging)
val storeResult = storeStatus(account, statuses, sinceId, maxId, sinceSortId,
maxSortId, loadItemLimit, false)
if (saveReadPosition) {
setLocalReadPosition(accountKey, details, microBlog)
setLocalReadPosition(accountKey, account)
}
// TODO cache related data and preload
val cacheTask = CacheUsersStatusesTask(context, accountKey, details.type, statuses)
TaskStarter.execute(cacheTask)
errorInfoStore.remove(errorInfoKey, accountKey.id)
result.add(StatusListResponse(accountKey, statuses))
if (storeResult != 0) {
throw GetTimelineException(storeResult)
}
return@mapIndexed GetStatusesResult(null)
} catch (e: MicroBlogException) {
DebugLog.w(LOGTAG, tr = e)
if (e.isCausedByNetworkIssue) {
@ -105,17 +98,16 @@ abstract class GetStatusesTask(
} else if (e.statusCode == 401) {
// Unauthorized
}
result.add(StatusListResponse(accountKey, e))
return@mapIndexed GetStatusesResult(e)
} catch (e: GetTimelineException) {
result.add(StatusListResponse(accountKey, e))
return@mapIndexed GetStatusesResult(e)
}
}
return result
}
override fun afterExecute(handler: ((Boolean) -> Unit)?, result: List<StatusListResponse>) {
override fun afterExecute(handler: ((Boolean) -> Unit)?, result: List<GetStatusesResult?>) {
context.contentResolver.notifyChange(contentUri, null)
val exception = AsyncTwitterWrapper.getException(result)
val exception = result.firstOrNull { it?.exception != null }?.exception
bus.post(GetStatusesTaskEvent(contentUri, false, exception))
handler?.invoke(true)
}
@ -124,14 +116,15 @@ abstract class GetStatusesTask(
bus.post(GetStatusesTaskEvent(contentUri, true, null))
}
protected abstract fun setLocalReadPosition(accountKey: UserKey, details: AccountDetails,
twitter: MicroBlog)
@Throws(MicroBlogException::class)
protected abstract fun getStatuses(account: AccountDetails, paging: Paging): List<ParcelableStatus>
private fun storeStatus(accountKey: UserKey, details: AccountDetails,
statuses: List<Status>,
sinceId: String?, maxId: String?,
sinceSortId: Long, maxSortId: Long,
protected abstract fun setLocalReadPosition(accountKey: UserKey, details: AccountDetails)
private fun storeStatus(account: AccountDetails, statuses: List<ParcelableStatus>,
sinceId: String?, maxId: String?, sinceSortId: Long, maxSortId: Long,
loadItemLimit: Int, notify: Boolean): Int {
val accountKey = account.key
val uri = contentUri
val writeUri = UriUtils.appendQueryParameters(uri, QUERY_PARAM_NOTIFY_CHANGE, notify)
val resolver = context.contentResolver
@ -142,29 +135,28 @@ abstract class GetStatusesTask(
var minPositionKey: Long = -1
var hasIntersection = false
if (!statuses.isEmpty()) {
val firstSortId = statuses.first().sortId
val lastSortId = statuses.last().sortId
val firstSortId = statuses.first().sort_id
val lastSortId = statuses.last().sort_id
// Get id diff of first and last item
val sortDiff = firstSortId - lastSortId
val creator = ObjectCursor.valuesCreatorFrom(ParcelableStatus::class.java)
for (i in 0 until statuses.size) {
val item = statuses[i]
val status = item.toParcelable(accountKey, details.type, profileImageSize)
ParcelableStatusUtils.updateExtraInformation(status, details)
val status = statuses[i]
ParcelableStatusUtils.updateExtraInformation(status, account)
status.position_key = getPositionKey(status.timestamp, status.sort_id, lastSortId,
sortDiff, i, statuses.size)
status.inserted_date = System.currentTimeMillis()
mediaPreloader.preloadStatus(status)
values[i] = creator.create(status)
if (minIdx == -1 || item < statuses[minIdx]) {
if (minIdx == -1 || status < statuses[minIdx]) {
minIdx = i
minPositionKey = status.position_key
}
if (sinceId != null && item.sortId <= sinceSortId) {
if (sinceId != null && status.sort_id <= sinceSortId) {
hasIntersection = true
}
statusIds[i] = item.id
statusIds[i] = status.id
}
}
// Delete all rows conflicting before new data inserted.
@ -218,6 +210,8 @@ abstract class GetStatusesTask(
}
}
data class GetStatusesResult(val exception: Exception?)
companion object {
const val ERROR_LOAD_GAP = 1