Twidere-App-Android-Twitter.../twidere/src/main/kotlin/org/mariotaku/twidere/fragment/timeline/AbsTimelineFragment.kt

850 lines
37 KiB
Kotlin
Raw Normal View History

2017-10-10 13:33:04 +02:00
/*
* 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.fragment.timeline
2017-10-19 15:43:04 +02:00
import android.app.Activity
2017-10-10 13:33:04 +02:00
import android.arch.lifecycle.LiveData
2017-12-13 13:24:59 +01:00
import android.arch.lifecycle.MediatorLiveData
import android.arch.lifecycle.MutableLiveData
2017-12-19 05:01:36 +01:00
import android.arch.paging.LivePagedListBuilder
2017-10-13 13:33:14 +02:00
import android.arch.paging.PagedList
2017-10-19 15:43:04 +02:00
import android.content.ContentValues
2017-10-10 13:33:04 +02:00
import android.content.Context
2017-10-19 15:43:04 +02:00
import android.content.Intent
2017-10-10 13:33:04 +02:00
import android.net.Uri
import android.os.Bundle
2017-10-22 12:46:18 +02:00
import android.support.annotation.CallSuper
2017-10-19 15:43:04 +02:00
import android.support.v4.app.Fragment
2017-10-10 13:33:04 +02:00
import android.support.v7.widget.FixedLinearLayoutManager
import android.support.v7.widget.LinearLayoutManager
2017-10-13 13:33:14 +02:00
import android.support.v7.widget.RecyclerView
2017-10-10 13:33:04 +02:00
import android.support.v7.widget.RecyclerView.LayoutManager
import android.support.v7.widget.StaggeredGridLayoutManager
2017-10-19 15:43:04 +02:00
import android.view.ContextMenu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
2017-10-13 13:33:14 +02:00
import android.widget.Toast
2017-10-10 13:33:04 +02:00
import com.bumptech.glide.RequestManager
2017-10-13 13:33:14 +02:00
import com.squareup.otto.Subscribe
2017-10-19 15:43:04 +02:00
import kotlinx.android.synthetic.main.fragment_content_recyclerview.*
2017-10-15 16:49:34 +02:00
import org.mariotaku.kpreferences.get
2017-10-19 15:43:04 +02:00
import org.mariotaku.ktextension.*
2017-10-15 16:49:34 +02:00
import org.mariotaku.sqliteqb.library.Expression
2017-12-25 07:44:09 +01:00
import org.mariotaku.twidere.BuildConfig
2017-10-10 13:33:04 +02:00
import org.mariotaku.twidere.R
2017-10-19 15:43:04 +02:00
import org.mariotaku.twidere.activity.AccountSelectorActivity
import org.mariotaku.twidere.activity.ComposeActivity
2017-10-10 13:33:04 +02:00
import org.mariotaku.twidere.adapter.ParcelableStatusesAdapter
2017-10-13 13:33:14 +02:00
import org.mariotaku.twidere.adapter.decorator.ExtendedDividerItemDecoration
import org.mariotaku.twidere.adapter.iface.IContentAdapter
2017-10-10 13:33:04 +02:00
import org.mariotaku.twidere.annotation.FilterScope
2017-11-02 07:13:45 +01:00
import org.mariotaku.twidere.annotation.LoadMorePosition
2017-10-22 12:46:18 +02:00
import org.mariotaku.twidere.annotation.ReadPositionTag
2017-10-10 13:33:04 +02:00
import org.mariotaku.twidere.annotation.TimelineStyle
2017-10-20 19:39:48 +02:00
import org.mariotaku.twidere.constant.*
2017-10-19 15:43:04 +02:00
import org.mariotaku.twidere.constant.IntentConstants.*
import org.mariotaku.twidere.constant.KeyboardShortcutConstants.*
2017-12-19 05:01:36 +01:00
import org.mariotaku.twidere.data.CursorObjectDataSourceFactory
2017-12-13 13:24:59 +01:00
import org.mariotaku.twidere.data.ExceptionLiveData
2017-12-19 05:01:36 +01:00
import org.mariotaku.twidere.data.StatusesDataSourceFactory
2017-11-02 07:13:45 +01:00
import org.mariotaku.twidere.data.fetcher.StatusesFetcher
import org.mariotaku.twidere.data.processor.ParcelableStatusDisplayProcessor
2017-12-09 19:24:10 +01:00
import org.mariotaku.twidere.extension.*
2017-10-19 15:43:04 +02:00
import org.mariotaku.twidere.extension.adapter.removeStatuses
2017-12-13 13:24:59 +01:00
import org.mariotaku.twidere.extension.data.observe
import org.mariotaku.twidere.extension.model.quoted
2017-12-30 10:26:14 +01:00
import org.mariotaku.twidere.extension.view.PositionWithOffset
import org.mariotaku.twidere.extension.view.firstVisibleItemPosition
2017-12-28 18:22:16 +01:00
import org.mariotaku.twidere.extension.view.firstVisibleItemPositionWithOffset
import org.mariotaku.twidere.extension.view.lastVisibleItemPosition
2017-10-10 13:33:04 +02:00
import org.mariotaku.twidere.fragment.AbsContentRecyclerViewFragment
2017-10-19 15:43:04 +02:00
import org.mariotaku.twidere.fragment.BaseFragment
2017-12-25 07:44:09 +01:00
import org.mariotaku.twidere.fragment.iface.IFloatingActionButtonFragment
2017-10-19 15:43:04 +02:00
import org.mariotaku.twidere.fragment.status.FavoriteConfirmDialogFragment
import org.mariotaku.twidere.fragment.status.RetweetQuoteDialogFragment
import org.mariotaku.twidere.graphic.like.LikeAnimationDrawable
2017-12-13 13:24:59 +01:00
import org.mariotaku.twidere.model.*
2017-10-17 07:47:32 +02:00
import org.mariotaku.twidere.model.event.FavoriteTaskEvent
2017-10-13 13:33:14 +02:00
import org.mariotaku.twidere.model.event.GetStatusesTaskEvent
2017-10-19 15:43:04 +02:00
import org.mariotaku.twidere.model.event.StatusDestroyedEvent
import org.mariotaku.twidere.model.event.StatusRetweetedEvent
2017-10-10 13:33:04 +02:00
import org.mariotaku.twidere.model.pagination.SinceMaxPagination
2017-10-19 15:43:04 +02:00
import org.mariotaku.twidere.model.refresh.BaseContentRefreshParam
2017-10-15 16:49:34 +02:00
import org.mariotaku.twidere.model.refresh.ContentRefreshParam
2017-11-29 17:23:08 +01:00
import org.mariotaku.twidere.model.tab.extra.TimelineTabExtras
2017-10-17 14:05:55 +02:00
import org.mariotaku.twidere.model.timeline.TimelineFilter
2017-11-25 13:49:08 +01:00
import org.mariotaku.twidere.promise.StatusPromises
2017-10-13 13:33:14 +02:00
import org.mariotaku.twidere.provider.TwidereDataStore.Statuses
import org.mariotaku.twidere.singleton.BusSingleton
import org.mariotaku.twidere.singleton.PreferencesSingleton
2017-11-25 12:01:37 +01:00
import org.mariotaku.twidere.task.CreateFavoriteTask
2017-10-13 13:33:14 +02:00
import org.mariotaku.twidere.task.statuses.GetStatusesTask
2017-10-19 15:43:04 +02:00
import org.mariotaku.twidere.util.*
import org.mariotaku.twidere.view.ExtendedRecyclerView
import org.mariotaku.twidere.view.holder.GapViewHolder
import org.mariotaku.twidere.view.holder.TimelineFilterHeaderViewHolder
2017-10-17 07:47:32 +02:00
import org.mariotaku.twidere.view.holder.iface.IStatusViewHolder
import org.mariotaku.twidere.view.holder.status.StatusViewHolder
2017-12-30 10:26:14 +01:00
import java.util.concurrent.atomic.AtomicReference
2017-10-10 13:33:04 +02:00
2017-12-25 07:44:09 +01:00
abstract class AbsTimelineFragment : AbsContentRecyclerViewFragment<ParcelableStatusesAdapter, LayoutManager>(),
IFloatingActionButtonFragment {
2017-10-10 13:33:04 +02:00
override val reachingStart: Boolean
get() = recyclerView.layoutManager.firstVisibleItemPosition <= 0
2017-10-10 13:33:04 +02:00
override val reachingEnd: Boolean
get() = recyclerView.layoutManager.lastVisibleItemPosition >= recyclerView.layoutManager.itemCount - 1
2017-10-10 13:33:04 +02:00
@TimelineStyle
protected open val timelineStyle: Int
2017-11-29 17:23:08 +01:00
get() {
val extras = arguments?.getParcelable<TimelineTabExtras>(EXTRA_EXTRAS)
if (extras != null) return extras.timelineStyle
return arguments!!.getInt(EXTRA_TIMELINE_STYLE, TimelineStyle.PLAIN)
}
2017-10-10 13:33:04 +02:00
2017-10-13 13:33:14 +02:00
protected open val isStandalone: Boolean
2017-10-10 13:33:04 +02:00
get() = tabId <= 0
2017-10-15 16:49:34 +02:00
protected open val filtersEnabled: Boolean
get() = true
2017-10-17 14:05:55 +02:00
protected open val timelineFilter: TimelineFilter? = null
2017-10-22 12:46:18 +02:00
protected open val readPositionTag: String? = null
protected open val readPositionTagWithArguments: String?
get() = readPositionTag
2017-10-10 13:33:04 +02:00
@FilterScope
protected abstract val filterScope: Int
/**
* Content Uri for in-database data source
*/
protected abstract val contentUri: Uri
2017-12-13 13:24:59 +01:00
protected var statuses: LiveData<SingleResponse<PagedList<ParcelableStatus>?>>? = null
private set(value) {
field?.removeObservers(this)
field = value
}
2017-10-10 13:33:04 +02:00
protected val accountKeys: Array<UserKey>
2017-11-16 10:54:08 +01:00
get() = Utils.getAccountKeys(context!!, arguments) ?: if (isStandalone) {
2017-10-10 13:33:04 +02:00
emptyArray()
} else {
2017-11-16 10:54:08 +01:00
DataStoreUtils.getActivatedAccountKeys(context!!)
2017-10-10 13:33:04 +02:00
}
2017-10-13 13:33:14 +02:00
private val busEventHandler = BusEventHandler()
2017-10-22 12:46:18 +02:00
private val scrollHandler = ScrollHandler()
private val timelineBoundaryCallback = StatusesBoundaryCallback()
2017-12-30 10:26:14 +01:00
private val positionBackup: AtomicReference<PositionWithOffset> = AtomicReference()
2017-10-13 13:33:14 +02:00
2017-10-10 13:33:04 +02:00
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
2017-10-20 19:39:48 +02:00
registerForContextMenu(recyclerView)
2017-10-17 07:47:32 +02:00
adapter.statusClickListener = StatusClickHandler()
2017-12-30 10:26:14 +01:00
adapter.pagedListListener = this::onPagedListChanged
2017-11-02 08:01:56 +01:00
adapter.loadMoreSupportedPosition = if (isStandalone) {
LoadMorePosition.NONE
} else {
LoadMorePosition.END
}
setupLiveData()
2017-10-10 13:33:04 +02:00
showProgress()
}
2017-10-13 13:33:14 +02:00
override fun onStart() {
super.onStart()
2017-10-22 12:46:18 +02:00
recyclerView.addOnScrollListener(scrollHandler)
BusSingleton.register(busEventHandler)
2017-10-13 13:33:14 +02:00
}
override fun onStop() {
BusSingleton.unregister(busEventHandler)
2017-10-22 12:46:18 +02:00
recyclerView.removeOnScrollListener(scrollHandler)
if (userVisibleHint) {
saveReadPosition(layoutManager.firstVisibleItemPosition)
}
2017-10-13 13:33:14 +02:00
super.onStop()
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
handleActionActivityResult(this, requestCode, resultCode, data)
}
2017-10-10 13:33:04 +02:00
override fun onCreateLayoutManager(context: Context): LayoutManager = when (timelineStyle) {
TimelineStyle.STAGGERED -> StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL)
else -> FixedLinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
}
override fun onCreateAdapter(context: Context, requestManager: RequestManager): ParcelableStatusesAdapter {
return ParcelableStatusesAdapter(context, requestManager, timelineStyle)
}
2017-10-13 13:33:14 +02:00
override fun onCreateItemDecoration(context: Context, recyclerView: RecyclerView,
layoutManager: LayoutManager): RecyclerView.ItemDecoration? {
return when (timelineStyle) {
2017-10-20 19:39:48 +02:00
TimelineStyle.PLAIN -> createStatusesListItemDecoration(context, recyclerView, adapter)
TimelineStyle.GALLERY -> createStatusesListGalleryDecoration(context, recyclerView)
else -> super.onCreateItemDecoration(context, recyclerView, layoutManager)
2017-10-13 13:33:14 +02:00
}
}
2017-10-19 15:43:04 +02:00
override fun onCreateContextMenu(menu: ContextMenu, v: View, menuInfo: ContextMenu.ContextMenuInfo?) {
if (!userVisibleHint || menuInfo == null) return
2017-11-16 10:54:08 +01:00
val context = this.context!!
2017-10-19 15:43:04 +02:00
val inflater = MenuInflater(context)
val contextMenuInfo = menuInfo as ExtendedRecyclerView.ContextMenuInfo
val status = adapter.getStatus(contextMenuInfo.position)
2017-10-19 15:43:04 +02:00
inflater.inflate(R.menu.action_status, menu)
MenuUtils.setupForStatus(context, menu, PreferencesSingleton.get(this.context!!), UserColorNameManager.get(this.context!!), status)
2017-10-19 15:43:04 +02:00
}
override fun onContextItemSelected(item: MenuItem): Boolean {
if (!userVisibleHint) return false
2017-11-16 10:54:08 +01:00
val context = this.context!!
2017-10-19 15:43:04 +02:00
val contextMenuInfo = item.menuInfo as ExtendedRecyclerView.ContextMenuInfo
val status = adapter.getStatus(contextMenuInfo.position)
when (item.itemId) {
R.id.share -> {
2017-11-16 10:54:08 +01:00
val shareIntent = Utils.createStatusShareIntent(context, status)
2017-10-19 15:43:04 +02:00
val chooser = Intent.createChooser(shareIntent, getString(R.string.share_status))
startActivity(chooser)
return true
}
R.id.make_gap -> {
if (isStandalone) return true
val resolver = context.contentResolver
val values = ContentValues()
values.put(Statuses.IS_GAP, 1)
val where = Expression.equals(Statuses._ID, status._id).sql
resolver.update(contentUri, values, where, null)
return true
}
2017-11-16 10:54:08 +01:00
else -> return MenuUtils.handleStatusClick(context, this, fragmentManager!!,
PreferencesSingleton.get(this.context!!), UserColorNameManager.get(this.context!!), status, item)
2017-10-19 15:43:04 +02:00
}
}
2017-10-10 13:33:04 +02:00
override fun triggerRefresh(): Boolean {
2017-10-15 16:49:34 +02:00
if (isStandalone) {
return false
}
return getStatuses(object : ContentRefreshParam {
override val accountKeys: Array<UserKey> by lazy {
this@AbsTimelineFragment.accountKeys
}
override val pagination by lazy {
2017-11-16 10:54:08 +01:00
val context = context!!
2017-10-15 16:49:34 +02:00
val keys = accountKeys.toNulls()
val sinceIds = DataStoreUtils.getNewestStatusIds(context, contentUri, keys)
val sinceSortIds = DataStoreUtils.getNewestStatusSortIds(context, contentUri, keys)
return@lazy Array(keys.size) { idx ->
SinceMaxPagination.sinceId(sinceIds[idx], sinceSortIds[idx])
}
}
override val tabId: Long
get() = this@AbsTimelineFragment.tabId
2017-10-15 16:49:34 +02:00
override val shouldAbort: Boolean
get() = context == null
override val hasMaxIds: Boolean
get() = false
})
2017-10-10 13:33:04 +02:00
}
2017-11-02 07:13:45 +01:00
override fun onLoadMoreContents(position: Int) {
// No-op
2017-10-10 13:33:04 +02:00
}
override fun scrollToPositionWithOffset(position: Int, offset: Int) {
val layoutManager = this.layoutManager
when (layoutManager) {
is StaggeredGridLayoutManager -> {
layoutManager.scrollToPositionWithOffset(position, offset)
}
is LinearLayoutManager -> {
layoutManager.scrollToPositionWithOffset(position, offset)
}
}
}
2017-12-30 10:26:14 +01:00
/**
* Scroll to start of the timeline. This also updates read position
*/
2017-11-11 16:41:47 +01:00
override fun scrollToStart(): Boolean {
val result = super.scrollToStart()
if (result) saveReadPosition(0)
return result
}
2017-10-19 15:43:04 +02:00
2017-12-25 07:44:09 +01:00
override fun onActionClick(tag: String): Boolean {
when (tag) {
"home" -> {
val intent = Intent(INTENT_ACTION_COMPOSE).setPackage(BuildConfig.APPLICATION_ID)
val accountKeys = Utils.getAccountKeys(context!!, arguments)
if (accountKeys != null) {
intent.putExtra(EXTRA_ACCOUNT_KEYS, accountKeys)
}
startActivity(intent)
return true
}
}
return false
}
2017-10-19 15:43:04 +02:00
fun reloadAll() {
2018-01-01 17:39:23 +01:00
adapter.statuses = null
setupLiveData()
2017-10-19 15:43:04 +02:00
showProgress()
}
2017-10-13 13:33:14 +02:00
protected open fun onDataLoaded(data: PagedList<ParcelableStatus>?) {
2017-12-28 18:22:16 +01:00
val firstVisiblePosition = layoutManager.firstVisibleItemPositionWithOffset
adapter.showAccountsColor = accountKeys.size > 1
2017-10-13 13:33:14 +02:00
adapter.statuses = data
2017-10-17 14:05:55 +02:00
adapter.timelineFilter = timelineFilter
2017-10-10 13:33:04 +02:00
when {
data == null || data.isEmpty() -> {
showEmpty(R.drawable.ic_info_refresh, getString(R.string.swipe_down_to_refresh))
}
else -> {
showContent()
}
}
2017-12-30 10:26:14 +01:00
positionBackup.set(firstVisiblePosition)
}
protected open fun onPagedListChanged(data: PagedList<ParcelableStatus>?) {
2018-05-16 19:19:46 +02:00
val context = context ?: return
2017-12-30 10:26:14 +01:00
val firstVisiblePosition = positionBackup.getAndSet(null) ?: return
2018-05-16 19:19:46 +02:00
if (firstVisiblePosition.position == 0 && !PreferencesSingleton.get(context)[readFromBottomKey]) {
2017-12-30 10:26:14 +01:00
scrollToPositionWithOffset(0, 0)
2017-12-28 18:22:16 +01:00
} else {
2017-12-30 10:26:14 +01:00
scrollToPositionWithOffset(firstVisiblePosition.position, firstVisiblePosition.offset)
2017-10-20 19:39:48 +02:00
}
2017-10-10 13:33:04 +02:00
}
2017-10-15 16:49:34 +02:00
protected abstract fun getStatuses(param: ContentRefreshParam): Boolean
2017-10-10 13:33:04 +02:00
2017-10-15 16:49:34 +02:00
protected abstract fun onCreateStatusesFetcher(): StatusesFetcher
2017-10-10 13:33:04 +02:00
2017-10-17 07:47:32 +02:00
protected open fun getExtraSelection(): Pair<Expression, Array<String>?>? {
return null
}
2017-10-15 16:49:34 +02:00
protected open fun getMaxLoadItemLimit(forAccount: UserKey): Int {
return 200
2017-10-10 13:33:04 +02:00
}
2017-10-17 07:47:32 +02:00
protected open fun onFavoriteTaskEvent(event: FavoriteTaskEvent) {
val status = event.status
if (event.isSucceeded && status != null) {
replaceStatusStates(status)
}
}
2017-10-19 15:43:04 +02:00
protected open fun onStatusRetweetedEvent(event: StatusRetweetedEvent) {
replaceStatusStates(event.status)
}
protected open fun onStatusDestroyedEvent(event: StatusDestroyedEvent) {
if (!isStandalone) return
adapter.removeStatuses { it != null && it.id == event.status.id }
}
protected open fun onTimelineFilterClick() {
}
2017-10-22 12:46:18 +02:00
@CallSuper
protected open fun saveReadPosition(position: Int) {
if (host == null) return
2018-01-01 17:39:23 +01:00
if (position == RecyclerView.NO_POSITION || adapter.getStatusCount() <= 0) return
2017-10-22 12:46:18 +02:00
val status = adapter.getStatus(position.coerceIn(rangeOfSize(adapter.statusStartIndex,
2018-01-01 17:39:23 +01:00
adapter.getStatusCount())))
2017-10-22 12:46:18 +02:00
val readPosition = if (isStandalone) {
status.sort_id
} else {
status.position_key
}
val positionTag = readPositionTag ?: ReadPositionTag.CUSTOM_TIMELINE
readPositionTagWithArguments?.let {
accountKeys.forEach { accountKey ->
val tag = Utils.getReadPositionTagWithAccount(it, accountKey)
readStateManager.setPosition(tag, readPosition)
}
}
}
2017-10-17 07:47:32 +02:00
private fun setupLiveData() {
2017-11-02 08:01:56 +01:00
statuses = if (isStandalone) onCreateStandaloneLiveData() else onCreateDatabaseLiveData()
2017-12-30 10:26:14 +01:00
statuses?.observe(this, success = this::onDataLoaded, fail = {
2017-12-13 13:24:59 +01:00
showError(R.drawable.ic_info_error_generic, it.getErrorMessage(context!!))
})
2017-10-15 16:49:34 +02:00
}
2017-12-13 13:24:59 +01:00
private fun onCreateStandaloneLiveData(): LiveData<SingleResponse<PagedList<ParcelableStatus>?>> {
val merger = MediatorLiveData<SingleResponse<PagedList<ParcelableStatus>?>>()
2017-11-16 10:54:08 +01:00
val context = context!!
2017-10-15 16:49:34 +02:00
val accountKey = accountKeys.singleOrNull()!!
2017-12-13 13:24:59 +01:00
val errorLiveData = MutableLiveData<SingleResponse<PagedList<ParcelableStatus>?>>()
2017-12-19 05:01:36 +01:00
val factory = StatusesDataSourceFactory(context.applicationContext,
2017-12-13 13:24:59 +01:00
onCreateStatusesFetcher(), accountKey, timelineFilter) {
errorLiveData.postValue(SingleResponse(it))
}
2017-10-15 16:49:34 +02:00
val maxLoadLimit = getMaxLoadItemLimit(accountKey)
val loadLimit = PreferencesSingleton.get(this.context!!)[loadItemLimitKey]
2017-12-19 05:01:36 +01:00
val apiLiveData = ExceptionLiveData.wrap(LivePagedListBuilder(factory, PagedList.Config.Builder()
2017-10-15 16:49:34 +02:00
.setPageSize(loadLimit.coerceAtMost(maxLoadLimit))
.setInitialLoadSizeHint(loadLimit.coerceAtMost(maxLoadLimit))
2017-12-19 05:01:36 +01:00
.build()).build())
2017-12-13 13:24:59 +01:00
merger.addSource(errorLiveData) {
merger.removeSource(apiLiveData)
merger.removeSource(errorLiveData)
merger.value = it
}
merger.addSource(apiLiveData) {
merger.value = it
}
return merger
2017-10-15 16:49:34 +02:00
}
2017-12-13 13:24:59 +01:00
private fun onCreateDatabaseLiveData(): LiveData<SingleResponse<PagedList<ParcelableStatus>?>> {
2017-11-16 10:54:08 +01:00
val context = context!!
2017-10-15 16:49:34 +02:00
val table = DataStoreUtils.getTableNameByUri(contentUri)!!
val accountKeys = accountKeys
val expressions = mutableListOf(Expression.inArgs(Statuses.ACCOUNT_KEY, accountKeys.size))
val expressionArgs = mutableListOf(*accountKeys.mapToArray(UserKey::toString))
if (filtersEnabled) {
expressions.add(DataStoreUtils.buildStatusFilterWhereClause(PreferencesSingleton.get(this.context!!), table,
2017-10-15 16:49:34 +02:00
null, filterScope))
}
2017-10-17 07:47:32 +02:00
val extraSelection = getExtraSelection()
if (extraSelection != null) {
extraSelection.first.addTo(expressions)
extraSelection.second?.addAllTo(expressionArgs)
}
val processor = ParcelableStatusDisplayProcessor(context)
2017-12-19 05:01:36 +01:00
val factory = CursorObjectDataSourceFactory(context.contentResolver, contentUri,
2017-12-14 17:06:49 +01:00
statusColumnsLite, Expression.and(*expressions.toTypedArray()).sql,
expressionArgs.toTypedArray(), Statuses.DEFAULT_SORT_ORDER,
ParcelableStatus::class.java, processor)
2017-12-19 05:01:36 +01:00
// dataController = factory.obtainDataController()
return ExceptionLiveData.wrap(LivePagedListBuilder(factory, databasePagedListConfig)
.setBoundaryCallback(timelineBoundaryCallback).build())
2017-10-13 13:33:14 +02:00
}
2017-12-14 17:06:49 +01:00
private fun getFullStatus(position: Int): ParcelableStatus {
2017-10-17 07:47:32 +02:00
if (isStandalone) {
2018-01-01 17:39:23 +01:00
return adapter.getStatus(position)
2017-10-17 07:47:32 +02:00
}
2017-11-16 10:54:08 +01:00
val context = context!!
val rowId = adapter.getRowId(position)
2017-12-14 17:06:49 +01:00
return context.contentResolver.queryOne(contentUri, Statuses.COLUMNS, rowId, ParcelableStatus::class.java)!!
2017-10-17 07:47:32 +02:00
}
2017-10-19 15:43:04 +02:00
private fun replaceStatusStates(status: ParcelableStatus) {
2017-10-17 07:47:32 +02:00
val statuses = adapter.statuses?.snapshot() ?: return
val lm = layoutManager
val range = lm.firstVisibleItemPosition..lm.lastVisibleItemPosition
statuses.forEachIndexed { index, item ->
if (item?.id != status.id) return@forEachIndexed
item.favorite_count = status.favorite_count
item.retweet_count = status.retweet_count
item.reply_count = status.reply_count
2017-10-19 15:43:04 +02:00
item.my_retweet_id = status.my_retweet_id
2017-10-17 07:47:32 +02:00
item.is_favorite = status.is_favorite
if (index in range) {
adapter.notifyItemRangeChanged(index, 1)
}
}
}
2017-12-30 10:26:14 +01:00
class DefaultOnLikedListener(
private val context: Context,
private val status: ParcelableStatus,
private val accountKey: UserKey? = null
) : LikeAnimationDrawable.OnLikedListener {
override fun onLiked(): Boolean {
if (status.is_favorite) return false
CreateFavoriteTask(context, accountKey ?: status.account_key, status).promise()
return true
}
}
2017-11-24 17:40:35 +01:00
internal inner class BusEventHandler {
2017-10-13 13:33:14 +02:00
@Subscribe
fun notifyGetStatusesTaskChanged(event: GetStatusesTaskEvent) {
2017-11-16 10:54:08 +01:00
val context = context ?: return
2017-10-13 13:33:14 +02:00
if (event.uri != contentUri) return
refreshing = event.running
if (!event.running) {
2017-11-02 07:13:45 +01:00
setLoadMoreIndicatorPosition(LoadMorePosition.NONE)
2017-10-13 13:33:14 +02:00
refreshEnabled = true
// TODO: showContentOrError()
val exception = event.exception
if (exception is GetStatusesTask.GetTimelineException && userVisibleHint) {
Toast.makeText(context, exception.getToastMessage(context), Toast.LENGTH_SHORT).show()
}
}
}
2017-10-19 15:43:04 +02:00
@Subscribe
fun notifyStatusDestroyed(event: StatusDestroyedEvent) {
onStatusDestroyedEvent(event)
}
2017-10-17 07:47:32 +02:00
@Subscribe
fun notifyFavoriteTask(event: FavoriteTaskEvent) {
onFavoriteTaskEvent(event)
}
2017-10-19 15:43:04 +02:00
@Subscribe
fun notifyRetweetTask(event: StatusRetweetedEvent) {
onStatusRetweetedEvent(event)
}
2017-10-17 07:47:32 +02:00
}
private inner class StatusClickHandler : IStatusViewHolder.StatusClickListener {
override fun onStatusClick(holder: IStatusViewHolder, position: Int) {
2017-11-16 10:54:08 +01:00
val context = context ?: return
2018-04-11 13:52:33 +02:00
val status = getFullStatus(position)
2017-11-16 10:54:08 +01:00
IntentUtils.openStatus(context, status, null)
2017-10-17 07:47:32 +02:00
}
2017-10-20 19:39:48 +02:00
override fun onStatusLongClick(holder: IStatusViewHolder, position: Int): Boolean {
return false
}
2017-10-17 07:47:32 +02:00
override fun onItemActionClick(holder: RecyclerView.ViewHolder, id: Int, position: Int) {
2018-04-11 13:52:33 +02:00
val status = getFullStatus(position)
2017-10-19 15:43:04 +02:00
handleActionClick(this@AbsTimelineFragment, id, status,
holder as IStatusViewHolder)
2017-10-17 07:47:32 +02:00
}
override fun onItemActionLongClick(holder: RecyclerView.ViewHolder, id: Int, position: Int): Boolean {
2018-04-11 13:52:33 +02:00
val status = getFullStatus(position)
2017-10-19 15:43:04 +02:00
return handleActionLongClick(this@AbsTimelineFragment, status,
2017-10-17 07:47:32 +02:00
adapter.getItemId(position), id)
}
2017-10-19 15:43:04 +02:00
override fun onFilterClick(holder: TimelineFilterHeaderViewHolder) {
onTimelineFilterClick()
}
override fun onMediaClick(holder: IStatusViewHolder, view: View, current: ParcelableMedia, statusPosition: Int) {
2017-11-16 10:54:08 +01:00
val context = context ?: return
2017-12-14 17:06:49 +01:00
val status = getFullStatus(statusPosition)
IntentUtils.openMedia(context, status, current, PreferencesSingleton.get(this@AbsTimelineFragment.context!!)[newDocumentApiKey],
PreferencesSingleton.get(this@AbsTimelineFragment.context!!)[displaySensitiveContentsKey])
2017-10-19 15:43:04 +02:00
}
override fun onQuotedMediaClick(holder: IStatusViewHolder, view: View, current: ParcelableMedia, statusPosition: Int) {
2017-11-16 10:54:08 +01:00
val context = context ?: return
2017-12-14 17:06:49 +01:00
val status = getFullStatus(statusPosition)
val quotedMedia = status.quoted?.media ?: return
2017-11-16 10:54:08 +01:00
IntentUtils.openMedia(context, status.account_key, status.is_possibly_sensitive, status,
current, quotedMedia, PreferencesSingleton.get(this@AbsTimelineFragment.context!!)[newDocumentApiKey], PreferencesSingleton.get(this@AbsTimelineFragment.context!!)[displaySensitiveContentsKey])
2017-10-19 15:43:04 +02:00
}
override fun onQuotedStatusClick(holder: IStatusViewHolder, position: Int) {
2017-11-16 10:54:08 +01:00
val context = context ?: return
2017-12-14 17:06:49 +01:00
val status = getFullStatus(position)
val quotedId = status.quoted?.id ?: return
2017-11-16 10:54:08 +01:00
IntentUtils.openStatus(context, status.account_key, quotedId)
2017-10-19 15:43:04 +02:00
}
override fun onUserProfileClick(holder: IStatusViewHolder, position: Int) {
2017-12-14 17:06:49 +01:00
val status = getFullStatus(position)
2017-10-19 15:43:04 +02:00
val intent = IntentUtils.userProfile(status.account_key, status.user_key,
status.user_screen_name, status.extras?.user_statusnet_profile_url)
IntentUtils.applyNewDocument(intent, PreferencesSingleton.get(context!!)[newDocumentApiKey])
2017-10-19 15:43:04 +02:00
startActivity(intent)
}
override fun onItemMenuClick(holder: RecyclerView.ViewHolder, menuView: View, position: Int) {
if (activity == null) return
val view = layoutManager.findViewByPosition(position) ?: return
2017-12-09 19:24:10 +01:00
recyclerView.showContextMenuForChild(view, menuView)
2017-10-19 15:43:04 +02:00
}
override fun onGapClick(holder: GapViewHolder, position: Int) {
2017-12-14 17:06:49 +01:00
val status = getFullStatus(position)
2017-10-19 15:43:04 +02:00
DebugLog.v(msg = "Load activity gap $status")
adapter.addGapLoadingId(ObjectId(status.account_key, status.id))
val accountKeys = arrayOf(status.account_key)
val pagination = arrayOf(SinceMaxPagination.maxId(status.id, status.sort_id))
getStatuses(BaseContentRefreshParam(accountKeys, pagination).also {
it.tabId = tabId
})
2017-10-19 15:43:04 +02:00
}
}
2017-10-22 12:46:18 +02:00
private inner class ScrollHandler : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
val layoutManager = layoutManager
saveReadPosition(layoutManager.firstVisibleItemPosition)
}
}
}
2017-10-19 15:43:04 +02:00
private inner class StatusesBoundaryCallback : PagedList.BoundaryCallback<ParcelableStatus>() {
override fun onItemAtEndLoaded(itemAtEnd: ParcelableStatus) {
2017-12-28 18:22:16 +01:00
val started = getStatuses(object : ContentRefreshParam {
override val accountKeys by lazy {
this@AbsTimelineFragment.accountKeys
}
override val pagination by lazy {
val context = context!!
val keys = accountKeys.toNulls()
val maxIds = DataStoreUtils.getOldestStatusIds(context, contentUri, keys)
val maxSortIds = DataStoreUtils.getOldestStatusSortIds(context, contentUri, keys)
return@lazy Array(keys.size) { idx ->
SinceMaxPagination.maxId(maxIds[idx], maxSortIds[idx])
}
}
override val tabId: Long
get() = this@AbsTimelineFragment.tabId
})
if (started) {
adapter.loadMoreIndicatorPosition = LoadMorePosition.END
}
}
}
2017-10-10 13:33:04 +02:00
companion object {
2017-10-13 13:33:14 +02:00
const val REQUEST_OPEN_SELECT_ACCOUNT = 101
2017-10-19 15:43:04 +02:00
const val REQUEST_RETWEET_SELECT_ACCOUNT = 102
const val REQUEST_FAVORITE_SELECT_ACCOUNT = 103
val RANGE_REQUEST_CODES = 100 until 110
2017-10-19 15:43:04 +02:00
2017-12-14 17:06:49 +01:00
val statusColumnsLite = Statuses.COLUMNS - arrayOf(Statuses.MENTIONS_JSON,
Statuses.CARD, Statuses.FILTER_FLAGS, Statuses.FILTER_USERS, Statuses.FILTER_LINKS,
Statuses.FILTER_SOURCES, Statuses.FILTER_NAMES, Statuses.FILTER_TEXTS,
Statuses.FILTER_DESCRIPTIONS)
2017-10-19 15:43:04 +02:00
fun handleActionClick(fragment: BaseFragment, id: Int, status: ParcelableStatus,
holder: IStatusViewHolder) {
2017-10-19 15:43:04 +02:00
when (id) {
R.id.reply -> {
val intent = Intent(INTENT_ACTION_REPLY)
2017-11-16 10:54:08 +01:00
intent.`package` = fragment.context!!.packageName
2017-10-19 15:43:04 +02:00
intent.putExtra(EXTRA_STATUS, status)
fragment.startActivity(intent)
}
R.id.retweet -> {
fragment.executeAfterFragmentResumed { f ->
RetweetQuoteDialogFragment.show(f.childFragmentManager,
status.account_key, status.id, status)
}
}
R.id.favorite -> {
when {
PreferencesSingleton.get(fragment.context!!)[favoriteConfirmationKey] -> fragment.executeAfterFragmentResumed {
2017-10-19 15:43:04 +02:00
FavoriteConfirmDialogFragment.show(it.childFragmentManager,
status.account_key, status.id, status)
}
2017-11-25 13:49:08 +01:00
status.is_favorite -> StatusPromises.get(fragment.context!!).unfavorite(status.account_key, status.id)
2017-11-25 12:01:37 +01:00
else -> holder.playLikeAnimation(DefaultOnLikedListener(fragment.context!!, status))
2017-10-19 15:43:04 +02:00
}
}
}
}
fun handleActionLongClick(fragment: Fragment, status: ParcelableStatus, itemId: Long, id: Int): Boolean {
2017-11-16 10:54:08 +01:00
val context = fragment.context ?: return false
2017-10-19 15:43:04 +02:00
when (id) {
R.id.favorite -> {
2017-11-16 10:54:08 +01:00
val intent = selectAccountIntent(context, status, itemId)
2017-10-19 15:43:04 +02:00
fragment.startActivityForResult(intent, REQUEST_FAVORITE_SELECT_ACCOUNT)
return true
}
R.id.retweet -> {
2017-11-16 10:54:08 +01:00
val intent = selectAccountIntent(context, status, itemId, false)
2017-10-19 15:43:04 +02:00
fragment.startActivityForResult(intent, REQUEST_RETWEET_SELECT_ACCOUNT)
return true
}
}
return false
}
fun handleActionActivityResult(fragment: BaseFragment, requestCode: Int, resultCode: Int, data: Intent?) {
when (requestCode) {
REQUEST_FAVORITE_SELECT_ACCOUNT -> {
if (resultCode != Activity.RESULT_OK || data == null) return
val accountKey = data.getParcelableExtra<UserKey>(EXTRA_ACCOUNT_KEY)
val extras = data.getBundleExtra(EXTRA_EXTRAS)
val status = extras.status!!
if (PreferencesSingleton.get(fragment.context!!)[favoriteConfirmationKey]) {
2017-10-19 15:43:04 +02:00
fragment.executeAfterFragmentResumed {
FavoriteConfirmDialogFragment.show(it.childFragmentManager,
accountKey, status.id, status)
}
} else {
2017-11-25 13:49:08 +01:00
StatusPromises.get(fragment.context!!).favorite(accountKey, status)
2017-10-19 15:43:04 +02:00
}
}
REQUEST_RETWEET_SELECT_ACCOUNT -> {
if (resultCode != Activity.RESULT_OK || data == null) return
2017-12-17 14:09:44 +01:00
val accountKey = data.extras!!.accountKey!!
2017-10-19 15:43:04 +02:00
val extras = data.getBundleExtra(EXTRA_EXTRAS)
2017-12-17 14:09:44 +01:00
val status = extras.status!!
2017-10-19 15:43:04 +02:00
if (status.account_key.host != accountKey.host) {
val composeIntent = Intent(fragment.context, ComposeActivity::class.java)
2017-12-17 14:09:44 +01:00
val link = LinkCreator.getStatusWebLink(status)
if (link == null) {
Toast.makeText(fragment.context, R.string.message_toast_retweet_not_supported, Toast.LENGTH_SHORT).show()
return
}
composeIntent.putExtra(Intent.EXTRA_TEXT, "${status.text_plain} $link")
2017-10-19 15:43:04 +02:00
composeIntent.putExtra(EXTRA_ACCOUNT_KEY, accountKey)
composeIntent.putExtra(EXTRA_SELECTION, 0)
fragment.startActivity(composeIntent)
} else fragment.executeAfterFragmentResumed {
RetweetQuoteDialogFragment.show(it.childFragmentManager, accountKey,
status.id, status)
}
}
REQUEST_OPEN_SELECT_ACCOUNT -> {
if (resultCode != Activity.RESULT_OK || data == null) return
val accountKey = data.extras!!.accountKey
val extras = data.getBundleExtra(EXTRA_EXTRAS)
val status = extras.status!!
IntentUtils.openStatus(fragment.context!!, accountKey, status.id)
}
2017-10-19 15:43:04 +02:00
}
}
fun handleKeyboardShortcutAction(fragment: BaseFragment, action: String,
status: ParcelableStatus, position: Int): Boolean {
when (action) {
ACTION_STATUS_REPLY -> {
val intent = Intent(INTENT_ACTION_REPLY)
intent.putExtra(EXTRA_STATUS, status)
fragment.startActivity(intent)
return true
}
ACTION_STATUS_RETWEET -> {
fragment.executeAfterFragmentResumed {
RetweetQuoteDialogFragment.show(it.childFragmentManager,
status.account_key, status.id, status)
}
return true
}
ACTION_STATUS_FAVORITE -> {
if (PreferencesSingleton.get(fragment.context!!)[favoriteConfirmationKey]) {
2017-10-19 15:43:04 +02:00
fragment.executeAfterFragmentResumed {
FavoriteConfirmDialogFragment.show(it.childFragmentManager,
status.account_key, status.id, status)
}
} else if (status.is_favorite) {
2017-11-25 13:49:08 +01:00
StatusPromises.get(fragment.context!!).unfavorite(status.account_key, status.id)
2017-10-19 15:43:04 +02:00
} else {
val holder = fragment.recyclerView.findViewHolderForLayoutPosition(position) as StatusViewHolder
2017-11-25 12:01:37 +01:00
holder.playLikeAnimation(DefaultOnLikedListener(fragment.context!!, status))
2017-10-19 15:43:04 +02:00
}
return true
}
}
return false
}
2017-10-13 13:33:14 +02:00
fun createStatusesListItemDecoration(context: Context, recyclerView: RecyclerView,
adapter: IContentAdapter): RecyclerView.ItemDecoration {
adapter as RecyclerView.Adapter<*>
val itemDecoration = ExtendedDividerItemDecoration(context, (recyclerView.layoutManager as LinearLayoutManager).orientation)
val res = context.resources
if (adapter.profileImageEnabled) {
val decorPaddingLeft = res.getDimensionPixelSize(R.dimen.element_spacing_normal) * 2 + res.getDimensionPixelSize(R.dimen.icon_size_status_profile_image)
itemDecoration.setPadding { position, rect ->
val itemViewType = adapter.getItemViewType(position)
var nextItemIsStatus = false
if (position < adapter.itemCount - 1) {
nextItemIsStatus = adapter.getItemViewType(position + 1) in RecyclerViewTypes.STATUS_TYPES
2017-10-13 13:33:14 +02:00
}
if (nextItemIsStatus && itemViewType in RecyclerViewTypes.STATUS_TYPES) {
2017-10-13 13:33:14 +02:00
rect.left = decorPaddingLeft
} else {
rect.left = 0
}
true
}
}
itemDecoration.setDecorationEndOffset(1)
return itemDecoration
}
2017-10-19 15:43:04 +02:00
2017-10-20 19:39:48 +02:00
fun createStatusesListGalleryDecoration(context: Context, recyclerView: RecyclerView): RecyclerView.ItemDecoration {
val itemDecoration = ExtendedDividerItemDecoration(context, (recyclerView.layoutManager as LinearLayoutManager).orientation)
itemDecoration.setDecorationEndOffset(1)
return itemDecoration
}
2017-10-19 15:43:04 +02:00
fun selectAccountIntent(context: Context, status: ParcelableStatus, itemId: Long,
sameHostOnly: Boolean = true): Intent {
val intent = Intent(context, AccountSelectorActivity::class.java)
intent.putExtra(EXTRA_SELECT_ONLY_ITEM_AUTOMATICALLY, true)
if (sameHostOnly) {
intent.putExtra(EXTRA_ACCOUNT_HOST, status.account_key.host)
}
intent.putExtra(EXTRA_SINGLE_SELECTION, true)
intent.putExtra(EXTRA_EXTRAS, Bundle {
this[EXTRA_STATUS] = status
this[EXTRA_ID] = itemId
})
return intent
}
val databasePagedListConfig: PagedList.Config = PagedList.Config.Builder()
.setPageSize(50)
.setInitialLoadSizeHint(50)
2017-12-28 18:22:16 +01:00
.setPrefetchDistance(15)
.setEnablePlaceholders(true)
.build()
2017-10-19 15:43:04 +02:00
2017-10-10 13:33:04 +02:00
}
}