fix account switching (#4636)

closes #4631 
closes #4629 

and other weirdness introduced in Tusky 26.1.
I did a lot of testing on 2 physical devices and multiple emulators. It
definitely is better than before, but probably still not perfect.
This commit is contained in:
Konrad Pozniak 2024-09-02 19:49:22 +02:00 committed by GitHub
parent 74d479c3dc
commit 31e4f08966
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 121 additions and 145 deletions

View File

@ -42,6 +42,7 @@
android:name=".MainActivity"
android:configChanges="orientation|screenSize|keyboardHidden|screenLayout|smallestScreenSize"
android:exported="true"
android:launchMode="singleTask"
android:theme="@style/SplashTheme">
<intent-filter>
@ -106,7 +107,6 @@
<activity
android:name=".components.compose.ComposeActivity"
android:theme="@style/TuskyDialogActivityTheme"
android:excludeFromRecents="true"
android:windowSoftInputMode="stateVisible|adjustResize" />
<activity
android:name=".components.viewthread.ViewThreadActivity"

View File

@ -176,6 +176,8 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
private val binding by viewBinding(ActivityMainBinding::inflate)
private lateinit var activeAccount: AccountEntity
private lateinit var header: AccountHeaderView
private var onTabSelectedListener: OnTabSelectedListener? = null
@ -209,7 +211,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
super.onCreate(savedInstanceState)
// will be redirected to LoginActivity by BaseActivity
val activeAccount = accountManager.activeAccount ?: return
activeAccount = accountManager.activeAccount ?: return
if (explodeAnimationWasRequested()) {
overrideActivityTransitionCompat(
@ -224,7 +226,12 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
// check for savedInstanceState in order to not handle intent events more than once
if (intent != null && savedInstanceState == null) {
showNotificationTab = handleIntent(intent, activeAccount)
if (isFinishing) {
// handleIntent() finished this activity and started a new one - no need to continue initialization
return
}
}
window.statusBarColor = Color.TRANSPARENT // don't draw a status bar, the DrawerLayout and the MaterialDrawerLayout have their own
setContentView(binding.root)
@ -258,10 +265,10 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
setupDrawer(
savedInstanceState,
addSearchButton = hideTopToolbar,
addTrendingTagsButton = !accountManager.activeAccount!!.tabPreferences.hasTab(
addTrendingTagsButton = !activeAccount.tabPreferences.hasTab(
TRENDING_TAGS
),
addTrendingStatusesButton = !accountManager.activeAccount!!.tabPreferences.hasTab(
addTrendingStatusesButton = !activeAccount.tabPreferences.hasTab(
TRENDING_STATUSES
)
)
@ -352,7 +359,6 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
val activeAccount = accountManager.activeAccount ?: return
val showNotificationTab = handleIntent(intent, activeAccount)
if (showNotificationTab) {
val tabs = activeAccount.tabPreferences
@ -394,8 +400,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
}
val accountRequested = tuskyAccountId != -1L
if (accountRequested && tuskyAccountId != activeAccount.id) {
accountManager.setActiveAccount(tuskyAccountId)
changeAccount(tuskyAccountId, intent, withAnimation = false)
changeAccount(tuskyAccountId, intent)
return false
}
@ -451,11 +456,9 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
// TODO a bit cumbersome (also for resetting)
lifecycleScope.launch(Dispatchers.IO) {
accountManager.activeAccount?.let {
if (it.hasDirectMessageBadge != showBadge) {
it.hasDirectMessageBadge = showBadge
accountManager.saveAccount(it)
}
if (activeAccount.hasDirectMessageBadge != showBadge) {
activeAccount.hasDirectMessageBadge = showBadge
accountManager.saveAccount(activeAccount)
}
}
}
@ -562,7 +565,6 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
private fun forwardToComposeActivity(intent: Intent) {
val composeOptions =
intent.getParcelableExtraCompat<ComposeActivity.ComposeOptions>(COMPOSE_OPTIONS)
val composeIntent = if (composeOptions != null) {
ComposeActivity.startIntent(this, composeOptions)
} else {
@ -570,7 +572,6 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
action = intent.action
type = intent.type
putExtras(intent)
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
}
startActivity(composeIntent)
@ -831,11 +832,9 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
0 -> {
Log.d(TAG, "Creating \"Load more\" gap")
lifecycleScope.launch {
accountManager.activeAccount?.let {
developerToolsUseCase.createLoadMoreGap(
it.id
)
}
developerToolsUseCase.createLoadMoreGap(
activeAccount.id
)
}
}
}
@ -864,7 +863,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
// Save the previous tab so it can be restored later
val previousTab = tabAdapter.tabs.getOrNull(binding.viewPager.currentItem)
val tabs = accountManager.activeAccount!!.tabPreferences
val tabs = activeAccount.tabPreferences
// Detach any existing mediator before changing tab contents and attaching a new mediator
tabLayoutMediator?.detach()
@ -883,7 +882,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
}
if (tabs[position].id == DIRECT) {
val badge = tab.orCreateBadge
badge.isVisible = accountManager.activeAccount?.hasDirectMessageBadge ?: false
badge.isVisible = activeAccount.hasDirectMessageBadge
badge.backgroundColor = MaterialColors.getColor(binding.mainDrawer, materialR.attr.colorPrimary)
directMessageTab = tab
}
@ -921,11 +920,9 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
if (tab == directMessageTab) {
tab.badge?.isVisible = false
accountManager.activeAccount?.let {
if (it.hasDirectMessageBadge) {
it.hasDirectMessageBadge = false
accountManager.saveAccount(it)
}
if (activeAccount.hasDirectMessageBadge) {
activeAccount.hasDirectMessageBadge = false
accountManager.saveAccount(activeAccount)
}
}
}
@ -971,10 +968,8 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
}
private fun handleProfileClick(profile: IProfile, current: Boolean): Boolean {
val activeAccount = accountManager.activeAccount
// open profile when active image was clicked
if (current && activeAccount != null) {
if (current) {
val intent = AccountActivity.getIntent(this, activeAccount.accountId)
startActivityWithSlideInAnimation(intent)
return false
@ -994,49 +989,43 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
private fun changeAccount(
newSelectedId: Long,
forward: Intent?,
withAnimation: Boolean = true
) {
cacheUpdater.stop()
accountManager.setActiveAccount(newSelectedId)
val intent = Intent(this, MainActivity::class.java)
if (withAnimation) {
intent.putExtra(OPEN_WITH_EXPLODE_ANIMATION, true)
}
if (forward != null) {
intent.type = forward.type
intent.action = forward.action
intent.putExtras(forward)
}
startActivity(intent)
finish()
startActivity(intent)
}
private fun logout() {
accountManager.activeAccount?.let { activeAccount ->
AlertDialog.Builder(this)
.setTitle(R.string.action_logout)
.setMessage(getString(R.string.action_logout_confirm, activeAccount.fullName))
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
binding.appBar.hide()
binding.viewPager.hide()
binding.progressBar.show()
binding.bottomNav.hide()
binding.composeButton.hide()
AlertDialog.Builder(this)
.setTitle(R.string.action_logout)
.setMessage(getString(R.string.action_logout_confirm, activeAccount.fullName))
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
binding.appBar.hide()
binding.viewPager.hide()
binding.progressBar.show()
binding.bottomNav.hide()
binding.composeButton.hide()
lifecycleScope.launch {
val otherAccountAvailable = logoutUsecase.logout()
val intent = if (otherAccountAvailable) {
Intent(this@MainActivity, MainActivity::class.java)
} else {
LoginActivity.getIntent(this@MainActivity, LoginActivity.MODE_DEFAULT)
}
startActivity(intent)
finish()
lifecycleScope.launch {
val otherAccountAvailable = logoutUsecase.logout(activeAccount)
val intent = if (otherAccountAvailable) {
Intent(this@MainActivity, MainActivity::class.java)
} else {
LoginActivity.getIntent(this@MainActivity, LoginActivity.MODE_DEFAULT)
}
startActivity(intent)
finish()
}
.setNegativeButton(android.R.string.cancel, null)
.show()
}
}
.setNegativeButton(android.R.string.cancel, null)
.show()
}
private fun fetchUserInfo() = lifecycleScope.launch {
@ -1058,11 +1047,8 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
loadDrawerAvatar(me.avatar, false)
accountManager.updateActiveAccount(me)
NotificationHelper.createNotificationChannelsForAccount(
accountManager.activeAccount!!,
this
)
accountManager.updateAccount(activeAccount, me)
NotificationHelper.createNotificationChannelsForAccount(activeAccount, this)
// Setup push notifications
showMigrationNoticeIfNecessary(
@ -1217,9 +1203,9 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
}
header.clear()
header.profiles = profiles
header.setActiveProfile(accountManager.activeAccount!!.id)
header.setActiveProfile(activeAccount.id)
binding.mainToolbar.subtitle = if (accountManager.shouldDisplaySelfUsername()) {
accountManager.activeAccount!!.fullName
activeAccount.fullName
} else {
null
}

View File

@ -221,9 +221,9 @@ class ComposeActivity :
}
}
} else if (result == CropImage.CancelledResult) {
Log.w("ComposeActivity", "Edit image cancelled by user")
Log.w(TAG, "Edit image cancelled by user")
} else {
Log.w("ComposeActivity", "Edit image failed: " + result.error)
Log.w(TAG, "Edit image failed: " + result.error)
displayTransientMessage(R.string.error_image_edit_failed)
}
viewModel.cropImageItemOld = null
@ -940,7 +940,7 @@ class ComposeActivity :
val errorMessage =
getString(
R.string.error_no_custom_emojis,
accountManager.activeAccount!!.domain
activeAccount
)
displayTransientMessage(errorMessage)
} else {

View File

@ -303,11 +303,10 @@ class LoginActivity : BaseActivity() {
oauthScopes = OAUTH_SCOPES,
newAccount = newAccount
)
finishAffinity()
val intent = Intent(this, MainActivity::class.java)
intent.putExtra(MainActivity.OPEN_WITH_EXPLODE_ANIMATION, true)
startActivity(intent)
finishAffinity()
}, { e ->
setLoading(false)
binding.domainTextInputLayout.error =

View File

@ -106,7 +106,7 @@ class AccountManager @Inject constructor(
}
activeAccount = newAccountEntity
updateActiveAccount(newAccount)
updateAccount(newAccountEntity, newAccount)
}
/**
@ -122,49 +122,45 @@ class AccountManager @Inject constructor(
}
/**
* Logs the current account out by deleting all data of the account.
* Logs an account out by deleting all its data.
* @return the new active account, or null if no other account was found
*/
fun logActiveAccountOut(): AccountEntity? {
return activeAccount?.let { account ->
fun logout(account: AccountEntity): AccountEntity? {
account.logout()
account.logout()
accounts.remove(account)
accountDao.delete(account)
accounts.remove(account)
accountDao.delete(account)
if (accounts.size > 0) {
accounts[0].isActive = true
activeAccount = accounts[0]
Log.d(TAG, "logActiveAccountOut: saving account with id " + accounts[0].id)
accountDao.insertOrReplace(accounts[0])
} else {
activeAccount = null
}
activeAccount
if (accounts.size > 0) {
accounts[0].isActive = true
activeAccount = accounts[0]
Log.d(TAG, "logActiveAccountOut: saving account with id " + accounts[0].id)
accountDao.insertOrReplace(accounts[0])
} else {
activeAccount = null
}
return activeAccount
}
/**
* updates the current account with new information from the mastodon api
* and saves it in the database
* @param account the [Account] object returned from the api
* Updates an account with new information from the Mastodon api
* and saves it in the database.
* @param accountEntity the [AccountEntity] to update
* @param account the [Account] object which the newest data from the api
*/
fun updateActiveAccount(account: Account) {
activeAccount?.let {
it.accountId = account.id
it.username = account.username
it.displayName = account.name
it.profilePictureUrl = account.avatar
it.defaultPostPrivacy = account.source?.privacy ?: Status.Visibility.PUBLIC
it.defaultPostLanguage = account.source?.language.orEmpty()
it.defaultMediaSensitivity = account.source?.sensitive ?: false
it.emojis = account.emojis
it.locked = account.locked
fun updateAccount(accountEntity: AccountEntity, account: Account) {
accountEntity.accountId = account.id
accountEntity.username = account.username
accountEntity.displayName = account.name
accountEntity.profilePictureUrl = account.avatar
accountEntity.defaultPostPrivacy = account.source?.privacy ?: Status.Visibility.PUBLIC
accountEntity.defaultPostLanguage = account.source?.language.orEmpty()
accountEntity.defaultMediaSensitivity = account.source?.sensitive ?: false
accountEntity.emojis = account.emojis
accountEntity.locked = account.locked
Log.d(TAG, "updateActiveAccount: saving account with id " + it.id)
accountDao.insertOrReplace(it)
}
Log.d(TAG, "updateAccount: saving account with id " + accountEntity.id)
accountDao.insertOrReplace(accountEntity)
}
/**

View File

@ -38,9 +38,7 @@ inline fun <reified T> apiForAccount(
)
.removeHeader(MastodonApi.DOMAIN_HEADER)
.build()
}
if (account != null && request.url.host == account.domain) {
} else if (account != null && request.url.host == account.domain) {
request = request.newBuilder()
.header("Authorization", "Bearer ${account.accessToken}")
.build()

View File

@ -6,6 +6,7 @@ import com.keylesspalace.tusky.components.systemnotifications.NotificationHelper
import com.keylesspalace.tusky.components.systemnotifications.disableUnifiedPushNotificationsForAccount
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.db.DatabaseCleaner
import com.keylesspalace.tusky.db.entity.AccountEntity
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.ShareShortcutHelper
import dagger.hilt.android.qualifiers.ApplicationContext
@ -24,44 +25,40 @@ class LogoutUsecase @Inject constructor(
* Logs the current account out and clears all caches associated with it
* @return true if the user is logged in with other accounts, false if it was the only one
*/
suspend fun logout(): Boolean {
accountManager.activeAccount?.let { activeAccount ->
// invalidate the oauth token, if we have the client id & secret
// (could be missing if user logged in with a previous version of Tusky)
val clientId = activeAccount.clientId
val clientSecret = activeAccount.clientSecret
if (clientId != null && clientSecret != null) {
api.revokeOAuthToken(
clientId = clientId,
clientSecret = clientSecret,
token = activeAccount.accessToken
)
}
// disable push notifications
disableUnifiedPushNotificationsForAccount(context, activeAccount)
// disable pull notifications
if (!NotificationHelper.areNotificationsEnabled(context, accountManager)) {
NotificationHelper.disablePullNotifications(context)
}
// clear notification channels
NotificationHelper.deleteNotificationChannelsForAccount(activeAccount, context)
// remove account from local AccountManager
val otherAccountAvailable = accountManager.logActiveAccountOut() != null
// clear the database - this could trigger network calls so do it last when all tokens are gone
databaseCleaner.cleanupEverything(activeAccount.id)
draftHelper.deleteAllDraftsAndAttachmentsForAccount(activeAccount.id)
// remove shortcut associated with the account
shareShortcutHelper.removeShortcut(activeAccount)
return otherAccountAvailable
suspend fun logout(account: AccountEntity): Boolean {
// invalidate the oauth token, if we have the client id & secret
// (could be missing if user logged in with a previous version of Tusky)
val clientId = account.clientId
val clientSecret = account.clientSecret
if (clientId != null && clientSecret != null) {
api.revokeOAuthToken(
clientId = clientId,
clientSecret = clientSecret,
token = account.accessToken
)
}
return false
// disable push notifications
disableUnifiedPushNotificationsForAccount(context, account)
// disable pull notifications
if (!NotificationHelper.areNotificationsEnabled(context, accountManager)) {
NotificationHelper.disablePullNotifications(context)
}
// clear notification channels
NotificationHelper.deleteNotificationChannelsForAccount(account, context)
// remove account from local AccountManager
val otherAccountAvailable = accountManager.logout(account) != null
// clear the database - this could trigger network calls so do it last when all tokens are gone
databaseCleaner.cleanupEverything(account.id)
draftHelper.deleteAllDraftsAndAttachmentsForAccount(account.id)
// remove shortcut associated with the account
shareShortcutHelper.removeShortcut(account)
return otherAccountAvailable
}
}

View File

@ -17,7 +17,7 @@ import org.junit.Test
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
class RetrofitApiTest {
class ApiFactoryTest {
private val mockWebServer = MockWebServer()
private val okHttpClient = OkHttpClient.Builder().build()
@ -68,7 +68,7 @@ class RetrofitApiTest {
val account = AccountEntity(
id = 1,
domain = "test.domain",
domain = mockWebServer.hostName,
accessToken = "fakeToken",
clientId = "fakeId",
clientSecret = "fakeSecret",