From f419e83c16438456559163463785ec68ef98fdd3 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Mon, 20 Jun 2022 16:45:54 +0200 Subject: [PATCH] improve logout (#2579) * improve logout * fix tests * add db migration * delete wrongly committed file again * improve LogoutUsecase --- .../com/keylesspalace/tusky/MainActivity.kt | 41 ++++-------- .../tusky/appstore/CacheUpdater.kt | 6 +- .../conversation/ConversationsRepository.kt | 30 --------- .../conversation/ConversationsViewModel.kt | 2 +- .../tusky/components/login/LoginActivity.kt | 8 ++- .../components/search/SearchViewModel.kt | 2 +- .../viewmodel/CachedTimelineRemoteMediator.kt | 4 ++ .../viewmodel/CachedTimelineViewModel.kt | 2 +- .../viewmodel/NetworkTimelineViewModel.kt | 2 +- .../timeline/viewmodel/TimelineViewModel.kt | 2 +- .../keylesspalace/tusky/db/AccountEntity.kt | 11 ++++ .../keylesspalace/tusky/db/AccountManager.kt | 30 ++++++--- .../keylesspalace/tusky/db/AppDatabase.java | 10 ++- .../com/keylesspalace/tusky/di/AppModule.kt | 3 +- .../tusky/fragment/SFragment.java | 2 +- .../InstanceSwitchAuthInterceptor.java | 30 ++++++--- .../tusky/network/MastodonApi.kt | 8 +++ .../tusky/usecase/LogoutUsecase.kt | 66 +++++++++++++++++++ .../{network => usecase}/TimelineCases.kt | 3 +- app/src/main/res/layout/activity_main.xml | 8 +++ .../tusky/ComposeActivityTest.kt | 2 + .../CachedTimelineRemoteMediatorTest.kt | 2 + .../NetworkTimelineRemoteMediatorTest.kt | 2 + 23 files changed, 185 insertions(+), 91 deletions(-) delete mode 100644 app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsRepository.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/usecase/LogoutUsecase.kt rename app/src/main/java/com/keylesspalace/tusky/{network => usecase}/TimelineCases.kt (98%) diff --git a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt index 4fe8df6d..fd2e15b7 100644 --- a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt @@ -61,13 +61,10 @@ import com.keylesspalace.tusky.components.account.AccountActivity import com.keylesspalace.tusky.components.announcements.AnnouncementsActivity import com.keylesspalace.tusky.components.compose.ComposeActivity import com.keylesspalace.tusky.components.compose.ComposeActivity.Companion.canHandleMimeType -import com.keylesspalace.tusky.components.conversation.ConversationsRepository -import com.keylesspalace.tusky.components.drafts.DraftHelper import com.keylesspalace.tusky.components.drafts.DraftsActivity import com.keylesspalace.tusky.components.login.LoginActivity import com.keylesspalace.tusky.components.notifications.NotificationHelper import com.keylesspalace.tusky.components.notifications.disableAllNotifications -import com.keylesspalace.tusky.components.notifications.disableUnifiedPushNotificationsForAccount import com.keylesspalace.tusky.components.notifications.enablePushNotificationsWithFallback import com.keylesspalace.tusky.components.notifications.showMigrationNoticeIfNecessary import com.keylesspalace.tusky.components.preference.PreferencesActivity @@ -81,11 +78,12 @@ import com.keylesspalace.tusky.interfaces.ActionButtonActivity import com.keylesspalace.tusky.interfaces.ReselectableFragment import com.keylesspalace.tusky.pager.MainPagerAdapter import com.keylesspalace.tusky.settings.PrefKeys +import com.keylesspalace.tusky.usecase.LogoutUsecase import com.keylesspalace.tusky.util.ThemeUtils import com.keylesspalace.tusky.util.deleteStaleCachedMedia import com.keylesspalace.tusky.util.emojify import com.keylesspalace.tusky.util.hide -import com.keylesspalace.tusky.util.removeShortcut +import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.updateShortcut import com.keylesspalace.tusky.util.viewBinding import com.keylesspalace.tusky.util.visible @@ -135,10 +133,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje lateinit var cacheUpdater: CacheUpdater @Inject - lateinit var conversationRepository: ConversationsRepository - - @Inject - lateinit var draftHelper: DraftHelper + lateinit var logoutUsecase: LogoutUsecase private val binding by viewBinding(ActivityMainBinding::inflate) @@ -664,28 +659,18 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje .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 { - // Only disable UnifiedPush for this account -- do not call disableNotifications(), - // which unnecessarily disables it for all accounts and then re-enables it again at - // the next launch - disableUnifiedPushNotificationsForAccount(this@MainActivity, activeAccount) - NotificationHelper.deleteNotificationChannelsForAccount(activeAccount, this@MainActivity) - cacheUpdater.clearForUser(activeAccount.id) - conversationRepository.deleteCacheForAccount(activeAccount.id) - draftHelper.deleteAllDraftsAndAttachmentsForAccount(activeAccount.id) - removeShortcut(this@MainActivity, activeAccount) - val newAccount = accountManager.logActiveAccountOut() - if (!NotificationHelper.areNotificationsEnabled( - this@MainActivity, - accountManager - ) - ) { - NotificationHelper.disablePullNotifications(this@MainActivity) - } - val intent = if (newAccount == null) { - LoginActivity.getIntent(this@MainActivity, LoginActivity.MODE_DEFAULT) - } else { + val otherAccountAvailable = logoutUsecase.logout() + val intent = if (otherAccountAvailable) { Intent(this@MainActivity, MainActivity::class.java) + } else { + LoginActivity.getIntent(this@MainActivity, LoginActivity.MODE_DEFAULT) } startActivity(intent) finishWithoutSlideOutAnimation() diff --git a/app/src/main/java/com/keylesspalace/tusky/appstore/CacheUpdater.kt b/app/src/main/java/com/keylesspalace/tusky/appstore/CacheUpdater.kt index 12cb4a69..66ae898b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/appstore/CacheUpdater.kt +++ b/app/src/main/java/com/keylesspalace/tusky/appstore/CacheUpdater.kt @@ -9,7 +9,7 @@ import javax.inject.Inject class CacheUpdater @Inject constructor( eventHub: EventHub, private val accountManager: AccountManager, - private val appDatabase: AppDatabase, + appDatabase: AppDatabase, gson: Gson ) { @@ -44,8 +44,4 @@ class CacheUpdater @Inject constructor( fun stop() { this.disposable.dispose() } - - suspend fun clearForUser(accountId: Long) { - appDatabase.timelineDao().removeAll(accountId) - } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsRepository.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsRepository.kt deleted file mode 100644 index 3f074be5..00000000 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsRepository.kt +++ /dev/null @@ -1,30 +0,0 @@ -/* Copyright 2021 Tusky Contributors - * - * This file is a part of Tusky. - * - * 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. - * - * Tusky 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 Tusky; if not, - * see . */ - -package com.keylesspalace.tusky.components.conversation - -import com.keylesspalace.tusky.db.AppDatabase -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class ConversationsRepository @Inject constructor( - val db: AppDatabase -) { - - suspend fun deleteCacheForAccount(accountId: Long) { - db.conversationDao().deleteForAccount(accountId) - } -} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsViewModel.kt index 684c6f01..735aa26c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsViewModel.kt @@ -26,7 +26,7 @@ import androidx.paging.map import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.db.AppDatabase import com.keylesspalace.tusky.network.MastodonApi -import com.keylesspalace.tusky.network.TimelineCases +import com.keylesspalace.tusky.usecase.TimelineCases import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.coroutines.rx3.await 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 4b82340b..e55bbd71 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 @@ -236,7 +236,13 @@ class LoginActivity : BaseActivity(), Injectable { domain, clientId, clientSecret, oauthRedirectUri, code, "authorization_code" ).fold( { accessToken -> - accountManager.addAccount(accessToken.accessToken, domain, OAUTH_SCOPES) + accountManager.addAccount( + accessToken = accessToken.accessToken, + domain = domain, + clientId = clientId, + clientSecret = clientSecret, + oauthScopes = OAUTH_SCOPES + ) val intent = Intent(this, MainActivity::class.java) intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/SearchViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/SearchViewModel.kt index 065aa040..af886cdd 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/SearchViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/SearchViewModel.kt @@ -27,7 +27,7 @@ import com.keylesspalace.tusky.entity.DeletedStatus import com.keylesspalace.tusky.entity.Poll import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.network.MastodonApi -import com.keylesspalace.tusky.network.TimelineCases +import com.keylesspalace.tusky.usecase.TimelineCases import com.keylesspalace.tusky.util.RxAwareViewModel import com.keylesspalace.tusky.util.toViewData import com.keylesspalace.tusky.viewdata.StatusViewData diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/CachedTimelineRemoteMediator.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/CachedTimelineRemoteMediator.kt index c4aa2c72..ebab4440 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/CachedTimelineRemoteMediator.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/CachedTimelineRemoteMediator.kt @@ -51,6 +51,10 @@ class CachedTimelineRemoteMediator( state: PagingState ): MediatorResult { + if (!activeAccount.isLoggedIn()) { + return MediatorResult.Success(endOfPaginationReached = true) + } + try { var dbEmpty = false diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/CachedTimelineViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/CachedTimelineViewModel.kt index bec96234..97bc625b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/CachedTimelineViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/CachedTimelineViewModel.kt @@ -42,7 +42,7 @@ import com.keylesspalace.tusky.db.TimelineStatusWithAccount import com.keylesspalace.tusky.entity.Poll import com.keylesspalace.tusky.network.FilterModel import com.keylesspalace.tusky.network.MastodonApi -import com.keylesspalace.tusky.network.TimelineCases +import com.keylesspalace.tusky.usecase.TimelineCases import com.keylesspalace.tusky.viewdata.StatusViewData import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.asExecutor diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/NetworkTimelineViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/NetworkTimelineViewModel.kt index 9b9ee5b9..8c81df1d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/NetworkTimelineViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/NetworkTimelineViewModel.kt @@ -34,7 +34,7 @@ import com.keylesspalace.tusky.entity.Poll import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.network.FilterModel import com.keylesspalace.tusky.network.MastodonApi -import com.keylesspalace.tusky.network.TimelineCases +import com.keylesspalace.tusky.usecase.TimelineCases import com.keylesspalace.tusky.util.getDomain import com.keylesspalace.tusky.util.isLessThan import com.keylesspalace.tusky.util.isLessThanOrEqual diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/TimelineViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/TimelineViewModel.kt index 32c430f6..d640f64f 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/TimelineViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/TimelineViewModel.kt @@ -39,8 +39,8 @@ import com.keylesspalace.tusky.entity.Filter import com.keylesspalace.tusky.entity.Poll import com.keylesspalace.tusky.network.FilterModel import com.keylesspalace.tusky.network.MastodonApi -import com.keylesspalace.tusky.network.TimelineCases import com.keylesspalace.tusky.settings.PrefKeys +import com.keylesspalace.tusky.usecase.TimelineCases import com.keylesspalace.tusky.viewdata.StatusViewData import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow diff --git a/app/src/main/java/com/keylesspalace/tusky/db/AccountEntity.kt b/app/src/main/java/com/keylesspalace/tusky/db/AccountEntity.kt index 0b717120..5ffc9021 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/AccountEntity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/AccountEntity.kt @@ -37,6 +37,8 @@ data class AccountEntity( @field:PrimaryKey(autoGenerate = true) var id: Long, val domain: String, var accessToken: String, + var clientId: String?, // nullable for backward compatibility + var clientSecret: String?, // nullable for backward compatibility var isActive: Boolean, var accountId: String = "", var username: String = "", @@ -81,6 +83,15 @@ data class AccountEntity( val fullName: String get() = "@$username@$domain" + fun logout() { + // deleting credentials so they cannot be used again + accessToken = "" + clientId = null + clientSecret = null + } + + fun isLoggedIn() = accessToken.isNotEmpty() + override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false 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 2ddbe522..6048c855 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/AccountManager.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/AccountManager.kt @@ -54,7 +54,13 @@ class AccountManager @Inject constructor(db: AppDatabase) { * @param accessToken the access token for the new account * @param domain the domain of the accounts Mastodon instance */ - fun addAccount(accessToken: String, domain: String, oauthScopes: String) { + fun addAccount( + accessToken: String, + domain: String, + clientId: String, + clientSecret: String, + oauthScopes: String + ) { activeAccount?.let { it.isActive = false @@ -66,8 +72,13 @@ class AccountManager @Inject constructor(db: AppDatabase) { val maxAccountId = accounts.maxByOrNull { it.id }?.id ?: 0 val newAccountId = maxAccountId + 1 activeAccount = AccountEntity( - id = newAccountId, domain = domain.lowercase(Locale.ROOT), - accessToken = accessToken, oauthScopes = oauthScopes, isActive = true + id = newAccountId, + domain = domain.lowercase(Locale.ROOT), + accessToken = accessToken, + clientId = clientId, + clientSecret = clientSecret, + oauthScopes = oauthScopes, + isActive = true ) } @@ -89,11 +100,12 @@ class AccountManager @Inject constructor(db: AppDatabase) { */ fun logActiveAccountOut(): AccountEntity? { - if (activeAccount == null) { - return null - } else { - accounts.remove(activeAccount!!) - accountDao.delete(activeAccount!!) + return activeAccount?.let { account -> + + account.logout() + + accounts.remove(account) + accountDao.delete(account) if (accounts.size > 0) { accounts[0].isActive = true @@ -103,7 +115,7 @@ class AccountManager @Inject constructor(db: AppDatabase) { } else { activeAccount = null } - return activeAccount + activeAccount } } diff --git a/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java b/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java index 5bce5334..c43b3652 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java +++ b/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java @@ -31,7 +31,7 @@ import java.io.File; */ @Database(entities = { DraftEntity.class, AccountEntity.class, InstanceEntity.class, TimelineStatusEntity.class, TimelineAccountEntity.class, ConversationEntity.class - }, version = 38) + }, version = 39) public abstract class AppDatabase extends RoomDatabase { public abstract AccountDao accountDao(); @@ -573,4 +573,12 @@ public abstract class AppDatabase extends RoomDatabase { database.execSQL("DELETE FROM `TimelineStatusEntity`"); } }; + + public static final Migration MIGRATION_38_39 = new Migration(38, 39) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `clientId` TEXT"); + database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `clientSecret` TEXT"); + } + }; } diff --git a/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt index b14c5392..e17cb3cf 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt @@ -64,7 +64,8 @@ class AppModule { AppDatabase.MIGRATION_26_27, AppDatabase.MIGRATION_27_28, AppDatabase.MIGRATION_28_29, AppDatabase.MIGRATION_29_30, AppDatabase.MIGRATION_30_31, AppDatabase.MIGRATION_31_32, AppDatabase.MIGRATION_32_33, AppDatabase.MIGRATION_33_34, AppDatabase.MIGRATION_34_35, - AppDatabase.MIGRATION_35_36, AppDatabase.MIGRATION_36_37, AppDatabase.MIGRATION_37_38 + AppDatabase.MIGRATION_35_36, AppDatabase.MIGRATION_36_37, AppDatabase.MIGRATION_37_38, + AppDatabase.MIGRATION_38_39 ) .build() } diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java index ad81abe3..01a08c20 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java @@ -56,7 +56,7 @@ import com.keylesspalace.tusky.di.Injectable; import com.keylesspalace.tusky.entity.Attachment; import com.keylesspalace.tusky.entity.Status; import com.keylesspalace.tusky.network.MastodonApi; -import com.keylesspalace.tusky.network.TimelineCases; +import com.keylesspalace.tusky.usecase.TimelineCases; import com.keylesspalace.tusky.util.LinkHelper; import com.keylesspalace.tusky.util.StatusParsingHelper; import com.keylesspalace.tusky.view.MuteAccountDialog; diff --git a/app/src/main/java/com/keylesspalace/tusky/network/InstanceSwitchAuthInterceptor.java b/app/src/main/java/com/keylesspalace/tusky/network/InstanceSwitchAuthInterceptor.java index 2dcedd87..a3e1a815 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/InstanceSwitchAuthInterceptor.java +++ b/app/src/main/java/com/keylesspalace/tusky/network/InstanceSwitchAuthInterceptor.java @@ -15,6 +15,7 @@ package com.keylesspalace.tusky.network; +import android.util.Log; import androidx.annotation.NonNull; import com.keylesspalace.tusky.db.AccountEntity; @@ -22,22 +23,20 @@ import com.keylesspalace.tusky.db.AccountManager; import java.io.IOException; -import okhttp3.HttpUrl; -import okhttp3.Interceptor; -import okhttp3.Request; -import okhttp3.Response; +import okhttp3.*; /** * Created by charlag on 31/10/17. */ public final class InstanceSwitchAuthInterceptor implements Interceptor { - private AccountManager accountManager; + private final AccountManager accountManager; public InstanceSwitchAuthInterceptor(AccountManager accountManager) { this.accountManager = accountManager; } + @NonNull @Override public Response intercept(@NonNull Chain chain) throws IOException { @@ -55,13 +54,26 @@ public final class InstanceSwitchAuthInterceptor implements Interceptor { builder.url(swapHost(originalRequest.url(), instanceHeader)); builder.removeHeader(MastodonApi.DOMAIN_HEADER); } else if (currentAccount != null) { - //use domain of current account - builder.url(swapHost(originalRequest.url(), currentAccount.getDomain())) - .header("Authorization", - String.format("Bearer %s", currentAccount.getAccessToken())); + String accessToken = currentAccount.getAccessToken(); + if (!accessToken.isEmpty()) { + //use domain of current account + builder.url(swapHost(originalRequest.url(), currentAccount.getDomain())) + .header("Authorization", + String.format("Bearer %s", currentAccount.getAccessToken())); + } } Request newRequest = builder.build(); + if (MastodonApi.PLACEHOLDER_DOMAIN.equals(newRequest.url().host())) { + Log.w("ISAInterceptor", "no user logged in or no domain header specified - can't make request to " + newRequest.url()); + return new Response.Builder() + .code(400) + .message("Bad Request") + .protocol(Protocol.HTTP_2) + .body(ResponseBody.create("", MediaType.parse("text/plain"))) + .request(chain.request()) + .build(); + } return chain.proceed(newRequest); } else { diff --git a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt index 12d7cdcb..b8834a54 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt +++ b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt @@ -459,6 +459,14 @@ interface MastodonApi { @Field("grant_type") grantType: String ): NetworkResult + @FormUrlEncoded + @POST("oauth/revoke") + suspend fun revokeOAuthToken( + @Field("client_id") clientId: String, + @Field("client_secret") clientSecret: String, + @Field("token") token: String + ): NetworkResult + @GET("/api/v1/lists") suspend fun getLists(): NetworkResult> diff --git a/app/src/main/java/com/keylesspalace/tusky/usecase/LogoutUsecase.kt b/app/src/main/java/com/keylesspalace/tusky/usecase/LogoutUsecase.kt new file mode 100644 index 00000000..f8d3b11c --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/usecase/LogoutUsecase.kt @@ -0,0 +1,66 @@ +package com.keylesspalace.tusky.usecase + +import android.content.Context +import com.keylesspalace.tusky.components.drafts.DraftHelper +import com.keylesspalace.tusky.components.notifications.NotificationHelper +import com.keylesspalace.tusky.components.notifications.disableUnifiedPushNotificationsForAccount +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.db.AppDatabase +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.util.removeShortcut +import javax.inject.Inject + +class LogoutUsecase @Inject constructor( + private val context: Context, + private val api: MastodonApi, + private val db: AppDatabase, + private val accountManager: AccountManager, + private val draftHelper: DraftHelper +) { + + /** + * 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 + db.timelineDao().removeAll(activeAccount.id) + db.conversationDao().deleteForAccount(activeAccount.id) + draftHelper.deleteAllDraftsAndAttachmentsForAccount(activeAccount.id) + + // remove shortcut associated with the account + removeShortcut(context, activeAccount) + + return otherAccountAvailable + } + return false + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/network/TimelineCases.kt b/app/src/main/java/com/keylesspalace/tusky/usecase/TimelineCases.kt similarity index 98% rename from app/src/main/java/com/keylesspalace/tusky/network/TimelineCases.kt rename to app/src/main/java/com/keylesspalace/tusky/usecase/TimelineCases.kt index 86148e51..8f114434 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/TimelineCases.kt +++ b/app/src/main/java/com/keylesspalace/tusky/usecase/TimelineCases.kt @@ -13,7 +13,7 @@ * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ -package com.keylesspalace.tusky.network +package com.keylesspalace.tusky.usecase import android.util.Log import com.keylesspalace.tusky.appstore.BlockEvent @@ -29,6 +29,7 @@ import com.keylesspalace.tusky.appstore.StatusDeletedEvent import com.keylesspalace.tusky.entity.DeletedStatus import com.keylesspalace.tusky.entity.Poll import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.network.MastodonApi import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.disposables.CompositeDisposable import io.reactivex.rxjava3.kotlin.addTo diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 39fc717f..2f877709 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -13,6 +13,7 @@ tools:context="com.keylesspalace.tusky.MainActivity"> + +