trying to implement timeline sync

This commit is contained in:
Mariotaku Lee 2017-04-24 00:40:32 +08:00
parent fc4626f948
commit 8626109153
No known key found for this signature in database
GPG Key ID: 15C10F89D7C33535
25 changed files with 203 additions and 186 deletions

View File

@ -58,6 +58,7 @@ public interface TwidereConstants extends SharedPreferenceConstants, IntentConst
String PERMISSION_PREFERENCES_NAME = "app_permissions";
String SYNC_PREFERENCES_NAME = "sync_preferences";
String TIMELINE_POSITIONS_PREFERENCES_NAME = "timeline_positions";
String TIMELINE_SYNC_CACHE_PREFERENCES_NAME = "timeline_sync_cache";
String KEYBOARD_SHORTCUTS_PREFERENCES_NAME = "keyboard_shortcuts_preferences";
String ETAG_CACHE_PREFERENCES_NAME = "etag_cache";
String ETAG_MASTODON_APPS_PREFERENCES_NAME = "mastodon_apps";

View File

@ -30,7 +30,6 @@ import org.mariotaku.twidere.extension.model.hasInvalidAccount
import org.mariotaku.twidere.model.util.AccountUtils
import org.mariotaku.twidere.util.DeviceUtils
import org.mariotaku.twidere.util.StrictModeUtils
import org.mariotaku.twidere.util.Utils
open class MainActivity : BaseActivity() {

View File

@ -32,7 +32,10 @@ import org.mariotaku.twidere.constant.profileImageStyleKey
import org.mariotaku.twidere.constant.showAbsoluteTimeKey
import org.mariotaku.twidere.constant.textSizeKey
import org.mariotaku.twidere.model.DefaultFeatures
import org.mariotaku.twidere.util.*
import org.mariotaku.twidere.util.AsyncTwitterWrapper
import org.mariotaku.twidere.util.MultiSelectManager
import org.mariotaku.twidere.util.ReadStateManager
import org.mariotaku.twidere.util.UserColorNameManager
import org.mariotaku.twidere.util.dagger.GeneralComponent
import javax.inject.Inject

View File

@ -15,8 +15,6 @@ import org.mariotaku.twidere.annotation.AccountType
import org.mariotaku.twidere.model.ParcelableUser
import org.mariotaku.twidere.model.UserKey
import org.mariotaku.twidere.model.account.AccountExtras
import org.mariotaku.twidere.model.account.MastodonAccountExtras
import org.mariotaku.twidere.model.account.StatusNetAccountExtras
import org.mariotaku.twidere.model.account.TwitterAccountExtras
import org.mariotaku.twidere.model.account.cred.*
import org.mariotaku.twidere.model.util.AccountUtils

View File

@ -240,8 +240,17 @@ abstract class AbsActivitiesFragment protected constructor() :
lastReadViewTop = layoutManager.findViewByPosition(lastReadPosition)?.top ?: 0
loadMore = activityRange.endInclusive in 0..lastVisibleItemPosition
} else if (rememberPosition && readPositionTag != null) {
lastReadId = readStateManager.getPosition(readPositionTag)
lastReadViewTop = 0
val syncManager = timelineSyncManager
val positionTag = this.readPositionTag
val syncTag = this.timelineSyncTag
val currentTag = this.currentReadPositionTag
if (syncManager != null && positionTag != null && syncTag != null) {
lastReadId = syncManager.peekPosition(positionTag, syncTag)
}
if (lastReadId <= 0 && currentTag != null) {
lastReadId = readStateManager.getPosition(currentTag)
}
}
adapter.setData(data)

View File

@ -263,7 +263,7 @@ abstract class AbsStatusesFragment : AbsContentListRecyclerViewFragment<Parcelab
*/
override fun onLoadFinished(loader: Loader<List<ParcelableStatus>?>, data: List<ParcelableStatus>?) {
val rememberPosition = preferences[rememberPositionKey]
val readPositionTag = currentReadPositionTag
val currentReadPositionTag = currentReadPositionTag
val readFromBottom = preferences[readFromBottomKey]
val firstLoad = adapterData.isNullOrEmpty()
@ -292,8 +292,19 @@ abstract class AbsStatusesFragment : AbsContentListRecyclerViewFragment<Parcelab
}
lastReadViewTop = layoutManager.findViewByPosition(lastReadPosition)?.top ?: 0
loadMore = statusRange.endInclusive in 0..lastVisibleItemPosition
} else if (rememberPosition && readPositionTag != null) {
lastReadId = readStateManager.getPosition(readPositionTag)
} else if (rememberPosition) {
val syncManager = timelineSyncManager
val positionTag = this.readPositionTag
val syncTag = this.timelineSyncTag
val currentTag = this.currentReadPositionTag
if (syncManager != null && positionTag != null && syncTag != null) {
lastReadId = syncManager.peekPosition(positionTag, syncTag)
}
if (lastReadId <= 0 && currentTag != null) {
lastReadId = readStateManager.getPosition(currentTag)
}
lastReadViewTop = 0
}
// 2. Change adapter data

View File

@ -26,7 +26,10 @@ import com.squareup.otto.Bus
import com.twitter.Validator
import org.mariotaku.kpreferences.KPreferences
import org.mariotaku.restfu.http.RestHttpClient
import org.mariotaku.twidere.util.*
import org.mariotaku.twidere.util.AsyncTwitterWrapper
import org.mariotaku.twidere.util.DebugModeUtils
import org.mariotaku.twidere.util.KeyboardShortcutsHandler
import org.mariotaku.twidere.util.UserColorNameManager
import org.mariotaku.twidere.util.dagger.GeneralComponent
import org.mariotaku.twidere.util.premium.ExtraFeaturesService
import javax.inject.Inject

View File

@ -35,13 +35,11 @@ import android.widget.*
import android.widget.AbsListView.MultiChoiceModeListener
import android.widget.CompoundButton.OnCheckedChangeListener
import kotlinx.android.synthetic.main.fragment_content_listview.*
import org.mariotaku.twidere.Constants
import org.mariotaku.twidere.R
import org.mariotaku.twidere.TwidereConstants.HOST_MAPPING_PREFERENCES_NAME
import org.mariotaku.twidere.adapter.ArrayAdapter
import org.mariotaku.twidere.extension.applyTheme
import org.mariotaku.twidere.util.ParseUtils
import org.mariotaku.twidere.util.SharedPreferencesWrapper
class HostMappingsListFragment : AbsContentListViewFragment<HostMappingsListFragment.HostMappingAdapter>(),
AdapterView.OnItemClickListener, MultiChoiceModeListener, OnSharedPreferenceChangeListener {

View File

@ -20,8 +20,6 @@
package org.mariotaku.twidere.loader.statuses
import android.content.Context
import org.mariotaku.twidere.loader.statuses.ParcelableStatusesLoader
import org.mariotaku.twidere.model.ListResponse
import org.mariotaku.twidere.model.ParcelableStatus

View File

@ -22,7 +22,6 @@ package org.mariotaku.twidere.loader.statuses
import android.content.Context
import android.os.Bundle
import org.mariotaku.twidere.constant.IntentConstants.EXTRA_STATUSES
import org.mariotaku.twidere.loader.statuses.ParcelableStatusesLoader
import org.mariotaku.twidere.model.ListResponse
import org.mariotaku.twidere.model.ParcelableStatus
import java.util.*

View File

@ -20,13 +20,11 @@
package org.mariotaku.twidere.loader.userlists
import android.content.Context
import org.mariotaku.microblog.library.MicroBlog
import org.mariotaku.microblog.library.MicroBlogException
import org.mariotaku.microblog.library.twitter.model.PageableResponseList
import org.mariotaku.microblog.library.twitter.model.Paging
import org.mariotaku.microblog.library.twitter.model.UserList
import org.mariotaku.twidere.loader.userlists.BaseUserListsLoader
import org.mariotaku.twidere.model.ParcelableUserList
import org.mariotaku.twidere.model.UserKey

View File

@ -24,7 +24,6 @@ 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.UserList
import org.mariotaku.twidere.loader.userlists.BaseUserListsLoader
import org.mariotaku.twidere.model.ParcelableUserList
import org.mariotaku.twidere.model.UserKey

View File

@ -20,13 +20,11 @@
package org.mariotaku.twidere.loader.userlists
import android.content.Context
import org.mariotaku.microblog.library.MicroBlog
import org.mariotaku.microblog.library.MicroBlogException
import org.mariotaku.microblog.library.twitter.model.PageableResponseList
import org.mariotaku.microblog.library.twitter.model.Paging
import org.mariotaku.microblog.library.twitter.model.UserList
import org.mariotaku.twidere.loader.userlists.BaseUserListsLoader
import org.mariotaku.twidere.model.ParcelableUserList
import org.mariotaku.twidere.model.UserKey

View File

@ -16,4 +16,6 @@ interface RefreshTaskParam {
val shouldAbort: Boolean get() = false
val isBackground: Boolean get() = false
}

View File

@ -10,12 +10,10 @@ import org.mariotaku.library.objectcursor.ObjectCursor
import org.mariotaku.microblog.library.twitter.model.User
import org.mariotaku.sqliteqb.library.Expression
import org.mariotaku.twidere.extension.model.api.toParcelable
import org.mariotaku.twidere.extension.model.api.toParcelable
import org.mariotaku.twidere.model.ParcelableRelationship
import org.mariotaku.twidere.model.ParcelableUser
import org.mariotaku.twidere.model.UserKey
import org.mariotaku.twidere.model.util.ParcelableRelationshipUtils
import org.mariotaku.twidere.model.util.ParcelableUserUtils
import org.mariotaku.twidere.model.util.UserKeyUtils
import org.mariotaku.twidere.provider.TwidereDataStore.CachedRelationships
import org.mariotaku.twidere.provider.TwidereDataStore.CachedUsers

View File

@ -94,7 +94,7 @@ class GetActivitiesAboutMeTask(context: Context) : GetActivitiesTask(context) {
}
override fun setLocalReadPosition(accountKeys: Array<UserKey>, saveReadPosition: BooleanArray) {
override fun syncFetchReadPosition(accountKeys: Array<UserKey>) {
val manager = timelineSyncManagerFactory.get() ?: return
val tag = InteractionsTimelineFragment.getTimelineSyncTag(accountKeys)
try {

View File

@ -9,7 +9,6 @@ import org.mariotaku.kpreferences.get
import org.mariotaku.microblog.library.MicroBlogException
import org.mariotaku.microblog.library.twitter.model.Paging
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
@ -42,9 +41,8 @@ abstract class GetActivitiesTask(
override fun doLongOperation(param: RefreshTaskParam): List<GetTimelineResult?> {
if (param.shouldAbort) return emptyList()
val accountKeys = param.accountKeys
val accountKeys = param.accountKeys.takeIf { it.isNotEmpty() } ?: return emptyList()
val loadItemLimit = preferences[loadItemLimitKey]
val saveReadPosition = BooleanArray(accountKeys.size)
val result = accountKeys.mapIndexed { i, accountKey ->
val noItemsBefore = DataStoreUtils.getActivitiesCount(context, contentUri, accountKey) <= 0
val credentials = AccountUtils.getAccountDetails(AccountManager.get(context), accountKey,
@ -61,7 +59,6 @@ abstract class GetActivitiesTask(
paging.sinceId(sinceId)
if (maxId == null) {
paging.setLatestResults(true)
saveReadPosition[i] = true
}
}
// We should delete old activities has intersection with new items
@ -69,9 +66,6 @@ abstract class GetActivitiesTask(
val activities = getActivities(credentials, paging)
val storeResult = storeActivities(credentials, activities, sinceId, maxId,
loadItemLimit, noItemsBefore, false)
if (saveReadPosition[i]) {
}
errorInfoStore.remove(errorInfoKey, accountKey)
if (storeResult != 0) {
throw GetStatusesTask.GetTimelineException(storeResult)
@ -89,7 +83,9 @@ abstract class GetActivitiesTask(
}
return@mapIndexed GetTimelineResult(null)
}
setLocalReadPosition(accountKeys, saveReadPosition)
if (param.isBackground) {
syncFetchReadPosition(accountKeys)
}
return result
}
@ -108,7 +104,7 @@ abstract class GetActivitiesTask(
@Throws(MicroBlogException::class)
protected abstract fun getActivities(account: AccountDetails, paging: Paging): List<ParcelableActivity>
protected abstract fun setLocalReadPosition(accountKeys: Array<UserKey>, saveReadPosition: BooleanArray)
protected abstract fun syncFetchReadPosition(accountKeys: Array<UserKey>)
private fun storeActivities(details: AccountDetails, activities: List<ParcelableActivity>,
sinceId: String?, maxId: String?, loadItemLimit: Int, noItemsBefore: Boolean,

View File

@ -31,12 +31,12 @@ 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.fragment.HomeTimelineFragment
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
import org.mariotaku.twidere.util.Utils
import java.io.IOException
/**
@ -70,13 +70,13 @@ class GetHomeTimelineTask(context: Context) : GetStatusesTask(context) {
}
}
override fun setLocalReadPosition(accountKey: UserKey, details: AccountDetails) {
val syncManager = timelineSyncManagerFactory.get() ?: return
override fun syncFetchReadPosition(accountKeys: Array<UserKey>) {
val manager = timelineSyncManagerFactory.get() ?: return
val tag = HomeTimelineFragment.getTimelineSyncTag(accountKeys)
try {
val tag = Utils.getReadPositionTagWithAccount(ReadPositionTag.HOME_TIMELINE, accountKey)
syncManager.blockingGetPosition(ReadPositionTag.HOME_TIMELINE, tag)
}catch (e: IOException) {
manager.blockingGetPosition(ReadPositionTag.ACTIVITIES_ABOUT_ME, tag)
} catch (e: IOException) {
return
}
}
}

View File

@ -49,10 +49,9 @@ abstract class GetStatusesTask(
override fun doLongOperation(param: RefreshTaskParam): List<GetTimelineResult?> {
if (param.shouldAbort) return emptyList()
val accountKeys = param.accountKeys
val accountKeys = param.accountKeys.takeIf { it.isNotEmpty() } ?: return emptyList()
val loadItemLimit = preferences[loadItemLimitKey]
var saveReadPosition = false
return accountKeys.mapIndexed { i, accountKey ->
val result = accountKeys.mapIndexed { i, accountKey ->
val account = AccountUtils.getAccountDetails(AccountManager.get(context),
accountKey, true) ?: return@mapIndexed null
try {
@ -77,14 +76,10 @@ abstract class GetStatusesTask(
if (maxId == null) {
paging.setLatestResults(true)
}
saveReadPosition = true
}
val statuses = getStatuses(account, paging)
val storeResult = storeStatus(account, statuses, sinceId, maxId, sinceSortId,
maxSortId, loadItemLimit, false)
if (saveReadPosition) {
setLocalReadPosition(accountKey, account)
}
// TODO cache related data and preload
errorInfoStore.remove(errorInfoKey, accountKey.id)
if (storeResult != 0) {
@ -103,6 +98,10 @@ abstract class GetStatusesTask(
return@mapIndexed GetTimelineResult(e)
}
}
if (param.isBackground) {
syncFetchReadPosition(accountKeys)
}
return result
}
override fun afterExecute(handler: ((Boolean) -> Unit)?, result: List<GetTimelineResult?>) {
@ -119,7 +118,7 @@ abstract class GetStatusesTask(
@Throws(MicroBlogException::class)
protected abstract fun getStatuses(account: AccountDetails, paging: Paging): List<ParcelableStatus>
protected abstract fun setLocalReadPosition(accountKey: UserKey, details: AccountDetails)
protected abstract fun syncFetchReadPosition(accountKeys: Array<UserKey>)
private fun storeStatus(account: AccountDetails, statuses: List<ParcelableStatus>,
sinceId: String?, maxId: String?, sinceSortId: Long, maxSortId: Long,

View File

@ -30,7 +30,6 @@ import org.mariotaku.sqliteqb.library.Expression
import org.mariotaku.twidere.R
import org.mariotaku.twidere.annotation.AccountType
import org.mariotaku.twidere.extension.model.api.toParcelable
import org.mariotaku.twidere.extension.model.api.toParcelable
import org.mariotaku.twidere.extension.model.isOfficial
import org.mariotaku.twidere.extension.model.newMicroBlogInstance
import org.mariotaku.twidere.model.AccountDetails

View File

@ -42,7 +42,6 @@ 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.mastodon.toParcelable
import org.mariotaku.twidere.extension.model.api.microblog.toParcelable
import org.mariotaku.twidere.extension.model.newMicroBlogInstance
import org.mariotaku.twidere.model.*

View File

@ -3,7 +3,9 @@ package org.mariotaku.twidere.util
import android.content.Context
import android.content.SharedPreferences
import android.net.Uri
import android.os.Build
import android.util.Base64
import android.util.Log
import okhttp3.*
import org.mariotaku.kpreferences.get
import org.mariotaku.ktextension.toIntOr
@ -15,16 +17,10 @@ import org.mariotaku.twidere.util.dagger.DependencyHolder
import java.io.IOException
import java.net.InetSocketAddress
import java.net.Proxy
import java.util.concurrent.TimeUnit
import okhttp3.ConnectionSpec
import java.util.ArrayList
import android.util.Log
import java.security.NoSuchAlgorithmException
import java.util.*
import java.util.concurrent.TimeUnit
import javax.net.ssl.SSLContext
import android.os.Build
/**
* Created by mariotaku on 16/1/27.
@ -45,102 +41,6 @@ object HttpClientFactory {
DebugModeUtils.initForOkHttpClient(builder)
}
internal fun nougatECCFix(specList: ArrayList<ConnectionSpec>) {
// Shamelessly stolen from Tusky
if (Build.VERSION.SDK_INT != Build.VERSION_CODES.N) {
return
}
val sslContext: SSLContext
try {
sslContext = SSLContext.getInstance("TLS")
} catch (e: NoSuchAlgorithmException) {
Log.e("HttpClientFactory", "Failed obtaining TLS Context.")
return
}
sslContext.init(null, null, null)
val cipherSuites = sslContext.socketFactory.defaultCipherSuites
val allowedList = cipherSuites.filterNotTo(ArrayList<String>()) { it.contains("ECDH") }
val spec = ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
.cipherSuites(*allowedList.toTypedArray())
.supportsTlsExtensions(true)
.build()
specList.add(spec)
}
internal fun updateHttpClientConfiguration(builder: OkHttpClient.Builder, conf: HttpClientConfiguration,
dns: Dns, connectionPool: ConnectionPool, cache: Cache) {
conf.applyTo(builder)
builder.dns(dns)
builder.connectionPool(connectionPool)
builder.cache(cache)
}
internal fun updateTLSConnectionSpecs(builder: OkHttpClient.Builder) {
//Default spec list from OkHttpClient.DEFAULT_CONNECTION_SPECS
var specList: ArrayList<ConnectionSpec> = ArrayList()
specList.add(ConnectionSpec.MODERN_TLS)
nougatECCFix(specList)
specList.add(ConnectionSpec.CLEARTEXT)
builder.connectionSpecs(specList)
}
class HttpClientConfiguration(val prefs: SharedPreferences) {
var readTimeoutSecs: Long = -1
var writeTimeoutSecs: Long = -1
var connectionTimeoutSecs: Long = prefs.getInt(KEY_CONNECTION_TIMEOUT, 10).toLong()
var cacheSize: Int = prefs[cacheSizeLimitKey]
internal fun applyTo(builder: OkHttpClient.Builder) {
if (connectionTimeoutSecs >= 0) {
builder.connectTimeout(connectionTimeoutSecs, TimeUnit.SECONDS)
}
if (writeTimeoutSecs >= 0) {
builder.writeTimeout(writeTimeoutSecs, TimeUnit.SECONDS)
}
if (readTimeoutSecs >= 0) {
builder.readTimeout(readTimeoutSecs, TimeUnit.SECONDS)
}
if (prefs.getBoolean(KEY_ENABLE_PROXY, false)) {
configProxy(builder)
}
}
private fun configProxy(builder: OkHttpClient.Builder) {
val proxyType = prefs.getString(KEY_PROXY_TYPE, null) ?: return
val proxyHost = prefs.getString(KEY_PROXY_HOST, null)?.takeIf(String::isNotEmpty) ?: return
val proxyPort = prefs.getString(KEY_PROXY_PORT, null).toIntOr(-1)
val username = prefs.getString(KEY_PROXY_USERNAME, null)?.takeIf(String::isNotEmpty)
val password = prefs.getString(KEY_PROXY_PASSWORD, null)?.takeIf(String::isNotEmpty)
when (proxyType) {
"http" -> {
if (proxyPort !in (0..65535)) {
return
}
val address = InetSocketAddress.createUnresolved(proxyHost, proxyPort)
builder.proxy(Proxy(Proxy.Type.HTTP, address))
builder.authenticator { _, response ->
val b = response.request().newBuilder()
if (response.code() == 407) {
if (username != null && password != null) {
val credential = Credentials.basic(username, password)
b.header("Proxy-Authorization", credential)
}
}
b.build()
}
}
"reverse" -> {
builder.addInterceptor(ReverseProxyInterceptor(proxyHost, username, password))
}
}
}
}
fun reloadConnectivitySettings(context: Context) {
val holder = DependencyHolder.get(context)
val client = holder.restHttpClient as? OkHttpRestClient ?: return
@ -150,30 +50,6 @@ object HttpClientFactory {
client.client = builder.build()
}
/**
* Intercept and replace proxy patterns to real URL
*/
class ReverseProxyInterceptor(val proxyFormat: String, val proxyUsername: String?,
val proxyPassword: String?) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val url = request.url()
val builder = request.newBuilder()
val replacedUrl = HttpUrl.parse(replaceUrl(url, proxyFormat)) ?: run {
throw IOException("Invalid reverse proxy format")
}
builder.url(replacedUrl)
if (proxyUsername != null && proxyPassword != null) {
val headerValue = Base64.encodeToString("$proxyUsername:$proxyPassword".toByteArray(Charsets.UTF_8),
Base64.URL_SAFE)
builder.addHeader("Proxy-Authorization", headerValue)
}
return chain.proceed(builder.build())
}
}
/**
* # Supported patterns
*
@ -241,6 +117,45 @@ object HttpClientFactory {
return sb.toString()
}
private fun updateHttpClientConfiguration(builder: OkHttpClient.Builder, conf: HttpClientConfiguration,
dns: Dns, connectionPool: ConnectionPool, cache: Cache) {
conf.applyTo(builder)
builder.dns(dns)
builder.connectionPool(connectionPool)
builder.cache(cache)
}
private fun updateTLSConnectionSpecs(builder: OkHttpClient.Builder) {
//Default spec list from OkHttpClient.DEFAULT_CONNECTION_SPECS
val specList: ArrayList<ConnectionSpec> = ArrayList()
specList.add(ConnectionSpec.MODERN_TLS)
nougatECCFix(specList)
specList.add(ConnectionSpec.CLEARTEXT)
builder.connectionSpecs(specList)
}
private fun nougatECCFix(specList: ArrayList<ConnectionSpec>) {
// Shamelessly stolen from Tusky
if (Build.VERSION.SDK_INT != Build.VERSION_CODES.N) {
return
}
val sslContext = try {
SSLContext.getInstance("TLS")
} catch (e: NoSuchAlgorithmException) {
Log.e("HttpClientFactory", "Failed obtaining TLS Context.")
return
}
sslContext.init(null, null, null)
val cipherSuites = sslContext.socketFactory.defaultCipherSuites
val allowedList = cipherSuites.filterNotTo(ArrayList<String>()) { it.contains("ECDH") }
val spec = ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
.cipherSuites(*allowedList.toTypedArray())
.supportsTlsExtensions(true)
.build()
specList.add(spec)
}
private fun HttpUrl.authority(): String {
val host = host()
val port = port()
@ -255,4 +170,84 @@ object HttpClientFactory {
private val urlSupportedPatterns = listOf("[SCHEME]", "[HOST]", "[PORT]", "[AUTHORITY]",
"[PATH]", "[/PATH]", "[PATH_ENCODED]", "[QUERY]", "[?QUERY]", "[QUERY_ENCODED]",
"[FRAGMENT]", "[#FRAGMENT]", "[FRAGMENT_ENCODED]", "[URL_ENCODED]", "[URL_BASE64]")
class HttpClientConfiguration(val prefs: SharedPreferences) {
var readTimeoutSecs: Long = -1
var writeTimeoutSecs: Long = -1
var connectionTimeoutSecs: Long = prefs.getInt(KEY_CONNECTION_TIMEOUT, 10).toLong()
var cacheSize: Int = prefs[cacheSizeLimitKey]
internal fun applyTo(builder: OkHttpClient.Builder) {
if (connectionTimeoutSecs >= 0) {
builder.connectTimeout(connectionTimeoutSecs, TimeUnit.SECONDS)
}
if (writeTimeoutSecs >= 0) {
builder.writeTimeout(writeTimeoutSecs, TimeUnit.SECONDS)
}
if (readTimeoutSecs >= 0) {
builder.readTimeout(readTimeoutSecs, TimeUnit.SECONDS)
}
if (prefs.getBoolean(KEY_ENABLE_PROXY, false)) {
configProxy(builder)
}
}
private fun configProxy(builder: OkHttpClient.Builder) {
val proxyType = prefs.getString(KEY_PROXY_TYPE, null) ?: return
val proxyHost = prefs.getString(KEY_PROXY_HOST, null)?.takeIf(String::isNotEmpty) ?: return
val proxyPort = prefs.getString(KEY_PROXY_PORT, null).toIntOr(-1)
val username = prefs.getString(KEY_PROXY_USERNAME, null)?.takeIf(String::isNotEmpty)
val password = prefs.getString(KEY_PROXY_PASSWORD, null)?.takeIf(String::isNotEmpty)
when (proxyType) {
"http" -> {
if (proxyPort !in (0..65535)) {
return
}
val address = InetSocketAddress.createUnresolved(proxyHost, proxyPort)
builder.proxy(Proxy(Proxy.Type.HTTP, address))
builder.authenticator { _, response ->
val b = response.request().newBuilder()
if (response.code() == 407) {
if (username != null && password != null) {
val credential = Credentials.basic(username, password)
b.header("Proxy-Authorization", credential)
}
}
b.build()
}
}
"reverse" -> {
builder.addInterceptor(ReverseProxyInterceptor(proxyHost, username, password))
}
}
}
}
/**
* Intercept and replace proxy patterns to real URL
*/
class ReverseProxyInterceptor(val proxyFormat: String, val proxyUsername: String?,
val proxyPassword: String?) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val url = request.url()
val builder = request.newBuilder()
val replacedUrl = HttpUrl.parse(replaceUrl(url, proxyFormat)) ?: run {
throw IOException("Invalid reverse proxy format")
}
builder.url(replacedUrl)
if (proxyUsername != null && proxyPassword != null) {
val headerValue = Base64.encodeToString("$proxyUsername:$proxyPassword".toByteArray(Charsets.UTF_8),
Base64.URL_SAFE)
builder.addHeader("Proxy-Authorization", headerValue)
}
return chain.proceed(builder.build())
}
}
}

View File

@ -80,6 +80,9 @@ class TaskServiceRunner(
ACTION_REFRESH_DIRECT_MESSAGES -> {
val task = GetMessagesTask(context)
task.params = object : GetMessagesTask.RefreshNewTaskParam(context) {
override val isBackground: Boolean = true
override val accountKeys: Array<UserKey> by lazy {
AccountPreferences.getAccountPreferences(context, preferences,
DataStoreUtils.getAccountKeys(context)).filter {
@ -121,6 +124,7 @@ class TaskServiceRunner(
SinceMaxPagination().also { it.sinceId = sinceId }
}
override val isBackground: Boolean = true
}
companion object {

View File

@ -34,7 +34,10 @@ import org.mariotaku.twidere.fragment.BasePreferenceFragment
import org.mariotaku.twidere.fragment.ThemedPreferenceDialogFragmentCompat
import org.mariotaku.twidere.fragment.filter.FilteredUsersFragment
import org.mariotaku.twidere.fragment.media.ExoPlayerPageFragment
import org.mariotaku.twidere.loader.*
import org.mariotaku.twidere.loader.CacheUserSearchLoader
import org.mariotaku.twidere.loader.DefaultAPIConfigLoader
import org.mariotaku.twidere.loader.ParcelableStatusLoader
import org.mariotaku.twidere.loader.ParcelableUserLoader
import org.mariotaku.twidere.loader.statuses.AbsRequestStatusesLoader
import org.mariotaku.twidere.loader.userlists.BaseUserListsLoader
import org.mariotaku.twidere.preference.AccountsListPreference

View File

@ -20,9 +20,12 @@
package org.mariotaku.twidere.util.sync
import android.content.Context
import android.net.Uri
import android.support.annotation.UiThread
import android.support.annotation.WorkerThread
import android.support.v4.util.ArrayMap
import okio.ByteString
import org.mariotaku.twidere.TwidereConstants.TIMELINE_SYNC_CACHE_PREFERENCES_NAME
import org.mariotaku.twidere.annotation.ReadPositionTag
import java.io.IOException
import java.util.*
@ -34,7 +37,8 @@ import java.util.*
abstract class TimelineSyncManager(val context: Context) {
private val stagedCommits = ArrayMap<TimelineKey, Long>()
private val cachedPositions = ArrayMap<TimelineKey, Long>()
private val cachedPositions = context.getSharedPreferences(TIMELINE_SYNC_CACHE_PREFERENCES_NAME,
Context.MODE_PRIVATE)
fun setPosition(@ReadPositionTag positionTag: String, currentTag: String?, positionKey: Long) {
stagedCommits[TimelineKey(positionTag, currentTag)] = positionKey
@ -45,22 +49,21 @@ abstract class TimelineSyncManager(val context: Context) {
PositionData(key.positionTag, key.currentTag, value)
}.toTypedArray()
stagedCommits.clear()
if (data.isEmpty()) return
performSync(data)
}
fun blockingGetPosition(@ReadPositionTag positionTag: String, currentTag: String?): Long {
val position = fetchPosition(positionTag, currentTag)
synchronized(cachedPositions) {
cachedPositions[TimelineKey(positionTag, currentTag)] = position
}
cachedPositions.edit().putLong(cacheKey(positionTag, currentTag), position).apply()
return position
}
fun peekPosition(@ReadPositionTag positionTag: String, currentTag: String?): Long {
synchronized(cachedPositions) {
return cachedPositions[TimelineKey(positionTag, currentTag)] ?: -1
}
val cacheKey = cacheKey(positionTag, currentTag)
val position = cachedPositions.getLong(cacheKey, -1)
cachedPositions.edit().remove(cacheKey).apply()
return position
}
@ -93,6 +96,11 @@ abstract class TimelineSyncManager(val context: Context) {
companion object {
fun newFactory(): Factory = ServiceLoader.load(Factory::class.java).firstOrNull() ?: DummyFactory
private fun cacheKey(@ReadPositionTag positionTag: String, currentTag: String?): String {
if (currentTag == null) return positionTag
return ByteString.encodeUtf8("$positionTag:${Uri.encode(currentTag)}").sha1().hex()
}
}