diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6901f3576..668d0ba30 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -42,6 +42,7 @@ android:name=".MainActivity" android:configChanges="orientation|screenSize|keyboardHidden|screenLayout|smallestScreenSize" android:exported="true" + android:launchMode="singleTask" android:theme="@style/SplashTheme"> @@ -106,7 +107,6 @@ (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 } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt index d391bfcb6..2b26d3300 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt @@ -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 { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/login/LoginActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/login/LoginActivity.kt index da2828577..83f1558f7 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/login/LoginActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/login/LoginActivity.kt @@ -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 = diff --git a/app/src/main/java/com/keylesspalace/tusky/db/AccountManager.kt b/app/src/main/java/com/keylesspalace/tusky/db/AccountManager.kt index 32e50f74b..c286d47b1 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/AccountManager.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/AccountManager.kt @@ -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) } /** diff --git a/app/src/main/java/com/keylesspalace/tusky/network/ApiFactory.kt b/app/src/main/java/com/keylesspalace/tusky/network/ApiFactory.kt index 9db7ed643..8bc70b5ff 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/ApiFactory.kt +++ b/app/src/main/java/com/keylesspalace/tusky/network/ApiFactory.kt @@ -38,9 +38,7 @@ inline fun 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() diff --git a/app/src/main/java/com/keylesspalace/tusky/usecase/LogoutUsecase.kt b/app/src/main/java/com/keylesspalace/tusky/usecase/LogoutUsecase.kt index e1556b070..55ac3fdeb 100644 --- a/app/src/main/java/com/keylesspalace/tusky/usecase/LogoutUsecase.kt +++ b/app/src/main/java/com/keylesspalace/tusky/usecase/LogoutUsecase.kt @@ -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 } } diff --git a/app/src/test/java/com/keylesspalace/tusky/network/RetrofitApiTest.kt b/app/src/test/java/com/keylesspalace/tusky/network/ApiFactoryTest.kt similarity index 98% rename from app/src/test/java/com/keylesspalace/tusky/network/RetrofitApiTest.kt rename to app/src/test/java/com/keylesspalace/tusky/network/ApiFactoryTest.kt index ae0024fa7..67b789ae3 100644 --- a/app/src/test/java/com/keylesspalace/tusky/network/RetrofitApiTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/network/ApiFactoryTest.kt @@ -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",