Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

prevent mixup of account timelines #4599

Merged
merged 9 commits into from
Aug 14, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 18 additions & 4 deletions app/src/main/java/com/keylesspalace/tusky/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {

@SuppressLint("RestrictedApi")
override fun onCreate(savedInstanceState: Bundle?) {
Log.w(TAG, "MainActivity onCreate $this $savedInstanceState")
// Newer Android versions don't need to install the compat Splash Screen
// and it can cause theming bugs.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
Expand Down Expand Up @@ -363,6 +364,12 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
}
}

override fun onDestroy() {
super.onDestroy()
cacheUpdater.stop()
connyduck marked this conversation as resolved.
Show resolved Hide resolved
Log.w(TAG, "MainActivity onDestroy $this")
}

/** Handle an incoming Intent,
* @returns true when the intent is coming from an notification and the interface should show the notification tab.
*/
Expand Down Expand Up @@ -390,6 +397,8 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
val accountRequested = tuskyAccountId != -1L
if (accountRequested && tuskyAccountId != activeAccount.id) {
accountManager.setActiveAccount(tuskyAccountId)
changeAccount(tuskyAccountId, intent, withAnimation = false)
return false
}

val openDrafts = intent.getBooleanExtra(OPEN_DRAFTS, false)
Expand Down Expand Up @@ -567,7 +576,6 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
}
}
startActivity(composeIntent)
finish()
}

private fun setupDrawer(
Expand Down Expand Up @@ -985,11 +993,17 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
return false
}

private fun changeAccount(newSelectedId: Long, forward: Intent?) {
private fun changeAccount(
newSelectedId: Long,
forward: Intent?,
withAnimation: Boolean = true
) {
cacheUpdater.stop()
accountManager.setActiveAccount(newSelectedId)
val intent = Intent(this, MainActivity::class.java)
intent.putExtra(OPEN_WITH_EXPLODE_ANIMATION, true)
if (withAnimation) {
intent.putExtra(OPEN_WITH_EXPLODE_ANIMATION, true)
}
if (forward != null) {
intent.type = forward.type
intent.action = forward.action
Expand Down Expand Up @@ -1240,6 +1254,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
fun accountSwitchIntent(context: Context, tuskyAccountId: Long): Intent {
return Intent(context, MainActivity::class.java).apply {
putExtra(TUSKY_ACCOUNT_ID, tuskyAccountId)
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
}

Expand Down Expand Up @@ -1286,7 +1301,6 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
fun redirectIntent(context: Context, tuskyAccountId: Long, url: String): Intent {
return accountSwitchIntent(context, tuskyAccountId).apply {
putExtra(REDIRECT_URL, url)
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
}

Expand Down
110 changes: 90 additions & 20 deletions app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,15 @@ import android.content.Context
import android.content.SharedPreferences
import android.os.Build
import android.util.Log
import androidx.annotation.VisibleForTesting
import at.connyduck.calladapter.networkresult.NetworkResultCallAdapterFactory
import com.keylesspalace.tusky.BuildConfig
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.entity.Notification
import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.json.GuardedAdapter
import com.keylesspalace.tusky.network.InstanceSwitchAuthInterceptor
import com.keylesspalace.tusky.network.FailingCall
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.network.MediaUploadApi
import com.keylesspalace.tusky.settings.PrefKeys.HTTP_PROXY_ENABLED
Expand All @@ -47,6 +48,7 @@ import java.net.InetSocketAddress
import java.net.Proxy
import java.util.Date
import java.util.concurrent.TimeUnit
import javax.inject.Named
import javax.inject.Singleton
import okhttp3.Cache
import okhttp3.OkHttp
Expand All @@ -66,6 +68,18 @@ object NetworkModule {

private const val TAG = "NetworkModule"

@Provides
@Named("defaultPort")
fun providesDefaultPort(): Int {
return 443
}

@Provides
@Named("defaultScheme")
fun providesDefaultScheme(): String {
return "https://"
}

Comment on lines +69 to +80
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is kinda weird, but the only way I found to have tests run on a random port without https.
Better ideas very welcome!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could we provide the whole URL depending on the current account?

@Provides
@Singleton
fun providesMoshi(): Moshi = Moshi.Builder()
Expand All @@ -92,7 +106,6 @@ object NetworkModule {
@Provides
@Singleton
fun providesHttpClient(
accountManager: AccountManager,
@ApplicationContext context: Context,
preferences: SharedPreferences
): OkHttpClient {
Expand Down Expand Up @@ -125,43 +138,100 @@ object NetworkModule {
builder.proxy(Proxy(Proxy.Type.HTTP, address))
} ?: Log.w(TAG, "Invalid proxy configuration: ($httpServer, $httpPort)")
}

return builder
.apply {
addInterceptor(InstanceSwitchAuthInterceptor(accountManager))
if (BuildConfig.DEBUG) {
addInterceptor(
HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BASIC }
)
}
}
.build()
if (BuildConfig.DEBUG) {
builder.addInterceptor(
HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BASIC }
)
}
return builder.build()
}

@Provides
@Singleton
fun providesRetrofit(httpClient: OkHttpClient, moshi: Moshi): Retrofit {
return Retrofit.Builder().baseUrl("https://" + MastodonApi.PLACEHOLDER_DOMAIN)
fun providesRetrofit(
httpClient: OkHttpClient,
moshi: Moshi,
@Named("defaultPort") defaultPort: Int,
@Named("defaultScheme") defaultScheme: String
): Retrofit {
return Retrofit.Builder()
.baseUrl("$defaultScheme${MastodonApi.PLACEHOLDER_DOMAIN}:$defaultPort")
.client(httpClient)
.addConverterFactory(MoshiConverterFactory.create(moshi))
.addCallAdapterFactory(NetworkResultCallAdapterFactory.create())
.build()
}

@Provides
@Singleton
fun providesApi(retrofit: Retrofit): MastodonApi = retrofit.create()
fun providesMastodonApi(
httpClient: OkHttpClient,
retrofit: Retrofit,
accountManager: AccountManager,
@Named("defaultPort") defaultPort: Int,
@Named("defaultScheme") defaultScheme: String
): MastodonApi {
return provideApi(httpClient, retrofit, accountManager, defaultPort, defaultScheme)
}

@Provides
@Singleton
fun providesMediaUploadApi(retrofit: Retrofit, okHttpClient: OkHttpClient): MediaUploadApi {
fun providesMediaUploadApi(
retrofit: Retrofit,
okHttpClient: OkHttpClient,
accountManager: AccountManager,
@Named("defaultPort") defaultPort: Int,
@Named("defaultScheme") defaultScheme: String
): MediaUploadApi {
val longTimeOutOkHttpClient = okHttpClient.newBuilder()
.readTimeout(100, TimeUnit.SECONDS)
.writeTimeout(100, TimeUnit.SECONDS)
.build()

return provideApi(longTimeOutOkHttpClient, retrofit, accountManager, defaultPort, defaultScheme)
}

@VisibleForTesting
inline fun <reified T> provideApi(
httpClient: OkHttpClient,
retrofit: Retrofit,
accountManager: AccountManager,
@Named("defaultPort") defaultPort: Int,
@Named("defaultScheme") defaultScheme: String
): T {
val currentAccount = accountManager.activeAccount
return retrofit.newBuilder()
.client(longTimeOutOkHttpClient)
.apply {
if (currentAccount != null) {
baseUrl("$defaultScheme${currentAccount.domain}:$defaultPort")
}
}
.callFactory { originalRequest ->
var request = originalRequest
connyduck marked this conversation as resolved.
Show resolved Hide resolved

val domainHeader = originalRequest.header(MastodonApi.DOMAIN_HEADER)
if (domainHeader != null) {
request = originalRequest.newBuilder()
.url(
originalRequest.url.newBuilder().host(domainHeader).build()
)
.removeHeader(MastodonApi.DOMAIN_HEADER)
.build()
}

if (currentAccount != null && request.url.host == currentAccount.domain) {
request = request.newBuilder()
.header("Authorization", "Bearer %s".format(currentAccount.accessToken))
.build()
}

print(originalRequest)
print(request)
connyduck marked this conversation as resolved.
Show resolved Hide resolved

if (request.url.host == MastodonApi.PLACEHOLDER_DOMAIN) {
FailingCall(request)
} else {
httpClient.newCall(request)
}
}
.build()
.create()
}
Expand Down
50 changes: 50 additions & 0 deletions app/src/main/java/com/keylesspalace/tusky/network/FailingCall.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.keylesspalace.tusky.network

import okhttp3.Call
import okhttp3.Callback
import okhttp3.Protocol
import okhttp3.Request
import okhttp3.Response
import okhttp3.ResponseBody.Companion.toResponseBody
import okio.Timeout

class FailingCall(private val request: Request) : Call {

private var isExecuted: Boolean = false

override fun cancel() { }

override fun clone(): Call {
return FailingCall(request())
}

override fun enqueue(responseCallback: Callback) {
isExecuted = true
responseCallback.onResponse(this, failingResponse())
}

override fun execute(): Response {
isExecuted = true
return failingResponse()
}

override fun isCanceled(): Boolean = false

override fun isExecuted(): Boolean = isExecuted

override fun request(): Request = request

override fun timeout(): Timeout {
return Timeout.NONE
}

private fun failingResponse(): Response {
return Response.Builder()
.request(request)
.code(400)
.message("Bad Request")
.protocol(Protocol.HTTP_1_1)
.body("".toResponseBody())
.build()
}
}

This file was deleted.

Loading
Loading