improve logout (#2579)
* improve logout * fix tests * add db migration * delete wrongly committed file again * improve LogoutUsecase
This commit is contained in:
parent
0574f0d096
commit
f419e83c16
|
@ -61,13 +61,10 @@ import com.keylesspalace.tusky.components.account.AccountActivity
|
||||||
import com.keylesspalace.tusky.components.announcements.AnnouncementsActivity
|
import com.keylesspalace.tusky.components.announcements.AnnouncementsActivity
|
||||||
import com.keylesspalace.tusky.components.compose.ComposeActivity
|
import com.keylesspalace.tusky.components.compose.ComposeActivity
|
||||||
import com.keylesspalace.tusky.components.compose.ComposeActivity.Companion.canHandleMimeType
|
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.drafts.DraftsActivity
|
||||||
import com.keylesspalace.tusky.components.login.LoginActivity
|
import com.keylesspalace.tusky.components.login.LoginActivity
|
||||||
import com.keylesspalace.tusky.components.notifications.NotificationHelper
|
import com.keylesspalace.tusky.components.notifications.NotificationHelper
|
||||||
import com.keylesspalace.tusky.components.notifications.disableAllNotifications
|
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.enablePushNotificationsWithFallback
|
||||||
import com.keylesspalace.tusky.components.notifications.showMigrationNoticeIfNecessary
|
import com.keylesspalace.tusky.components.notifications.showMigrationNoticeIfNecessary
|
||||||
import com.keylesspalace.tusky.components.preference.PreferencesActivity
|
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.interfaces.ReselectableFragment
|
||||||
import com.keylesspalace.tusky.pager.MainPagerAdapter
|
import com.keylesspalace.tusky.pager.MainPagerAdapter
|
||||||
import com.keylesspalace.tusky.settings.PrefKeys
|
import com.keylesspalace.tusky.settings.PrefKeys
|
||||||
|
import com.keylesspalace.tusky.usecase.LogoutUsecase
|
||||||
import com.keylesspalace.tusky.util.ThemeUtils
|
import com.keylesspalace.tusky.util.ThemeUtils
|
||||||
import com.keylesspalace.tusky.util.deleteStaleCachedMedia
|
import com.keylesspalace.tusky.util.deleteStaleCachedMedia
|
||||||
import com.keylesspalace.tusky.util.emojify
|
import com.keylesspalace.tusky.util.emojify
|
||||||
import com.keylesspalace.tusky.util.hide
|
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.updateShortcut
|
||||||
import com.keylesspalace.tusky.util.viewBinding
|
import com.keylesspalace.tusky.util.viewBinding
|
||||||
import com.keylesspalace.tusky.util.visible
|
import com.keylesspalace.tusky.util.visible
|
||||||
|
@ -135,10 +133,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
lateinit var cacheUpdater: CacheUpdater
|
lateinit var cacheUpdater: CacheUpdater
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var conversationRepository: ConversationsRepository
|
lateinit var logoutUsecase: LogoutUsecase
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var draftHelper: DraftHelper
|
|
||||||
|
|
||||||
private val binding by viewBinding(ActivityMainBinding::inflate)
|
private val binding by viewBinding(ActivityMainBinding::inflate)
|
||||||
|
|
||||||
|
@ -664,28 +659,18 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
.setTitle(R.string.action_logout)
|
.setTitle(R.string.action_logout)
|
||||||
.setMessage(getString(R.string.action_logout_confirm, activeAccount.fullName))
|
.setMessage(getString(R.string.action_logout_confirm, activeAccount.fullName))
|
||||||
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
|
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
|
||||||
|
binding.appBar.hide()
|
||||||
|
binding.viewPager.hide()
|
||||||
|
binding.progressBar.show()
|
||||||
|
binding.bottomNav.hide()
|
||||||
|
binding.composeButton.hide()
|
||||||
|
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
// Only disable UnifiedPush for this account -- do not call disableNotifications(),
|
val otherAccountAvailable = logoutUsecase.logout()
|
||||||
// which unnecessarily disables it for all accounts and then re-enables it again at
|
val intent = if (otherAccountAvailable) {
|
||||||
// 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 {
|
|
||||||
Intent(this@MainActivity, MainActivity::class.java)
|
Intent(this@MainActivity, MainActivity::class.java)
|
||||||
|
} else {
|
||||||
|
LoginActivity.getIntent(this@MainActivity, LoginActivity.MODE_DEFAULT)
|
||||||
}
|
}
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
finishWithoutSlideOutAnimation()
|
finishWithoutSlideOutAnimation()
|
||||||
|
|
|
@ -9,7 +9,7 @@ import javax.inject.Inject
|
||||||
class CacheUpdater @Inject constructor(
|
class CacheUpdater @Inject constructor(
|
||||||
eventHub: EventHub,
|
eventHub: EventHub,
|
||||||
private val accountManager: AccountManager,
|
private val accountManager: AccountManager,
|
||||||
private val appDatabase: AppDatabase,
|
appDatabase: AppDatabase,
|
||||||
gson: Gson
|
gson: Gson
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
@ -44,8 +44,4 @@ class CacheUpdater @Inject constructor(
|
||||||
fun stop() {
|
fun stop() {
|
||||||
this.disposable.dispose()
|
this.disposable.dispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun clearForUser(accountId: Long) {
|
|
||||||
appDatabase.timelineDao().removeAll(accountId)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 <http://www.gnu.org/licenses>. */
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -26,7 +26,7 @@ import androidx.paging.map
|
||||||
import com.keylesspalace.tusky.db.AccountManager
|
import com.keylesspalace.tusky.db.AccountManager
|
||||||
import com.keylesspalace.tusky.db.AppDatabase
|
import com.keylesspalace.tusky.db.AppDatabase
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
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.flow.map
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.rx3.await
|
import kotlinx.coroutines.rx3.await
|
||||||
|
|
|
@ -236,7 +236,13 @@ class LoginActivity : BaseActivity(), Injectable {
|
||||||
domain, clientId, clientSecret, oauthRedirectUri, code, "authorization_code"
|
domain, clientId, clientSecret, oauthRedirectUri, code, "authorization_code"
|
||||||
).fold(
|
).fold(
|
||||||
{ accessToken ->
|
{ 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)
|
val intent = Intent(this, MainActivity::class.java)
|
||||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||||
|
|
|
@ -27,7 +27,7 @@ import com.keylesspalace.tusky.entity.DeletedStatus
|
||||||
import com.keylesspalace.tusky.entity.Poll
|
import com.keylesspalace.tusky.entity.Poll
|
||||||
import com.keylesspalace.tusky.entity.Status
|
import com.keylesspalace.tusky.entity.Status
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
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.RxAwareViewModel
|
||||||
import com.keylesspalace.tusky.util.toViewData
|
import com.keylesspalace.tusky.util.toViewData
|
||||||
import com.keylesspalace.tusky.viewdata.StatusViewData
|
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||||
|
|
|
@ -51,6 +51,10 @@ class CachedTimelineRemoteMediator(
|
||||||
state: PagingState<Int, TimelineStatusWithAccount>
|
state: PagingState<Int, TimelineStatusWithAccount>
|
||||||
): MediatorResult {
|
): MediatorResult {
|
||||||
|
|
||||||
|
if (!activeAccount.isLoggedIn()) {
|
||||||
|
return MediatorResult.Success(endOfPaginationReached = true)
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
var dbEmpty = false
|
var dbEmpty = false
|
||||||
|
|
||||||
|
|
|
@ -42,7 +42,7 @@ import com.keylesspalace.tusky.db.TimelineStatusWithAccount
|
||||||
import com.keylesspalace.tusky.entity.Poll
|
import com.keylesspalace.tusky.entity.Poll
|
||||||
import com.keylesspalace.tusky.network.FilterModel
|
import com.keylesspalace.tusky.network.FilterModel
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
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 com.keylesspalace.tusky.viewdata.StatusViewData
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.asExecutor
|
import kotlinx.coroutines.asExecutor
|
||||||
|
|
|
@ -34,7 +34,7 @@ import com.keylesspalace.tusky.entity.Poll
|
||||||
import com.keylesspalace.tusky.entity.Status
|
import com.keylesspalace.tusky.entity.Status
|
||||||
import com.keylesspalace.tusky.network.FilterModel
|
import com.keylesspalace.tusky.network.FilterModel
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
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.getDomain
|
||||||
import com.keylesspalace.tusky.util.isLessThan
|
import com.keylesspalace.tusky.util.isLessThan
|
||||||
import com.keylesspalace.tusky.util.isLessThanOrEqual
|
import com.keylesspalace.tusky.util.isLessThanOrEqual
|
||||||
|
|
|
@ -39,8 +39,8 @@ import com.keylesspalace.tusky.entity.Filter
|
||||||
import com.keylesspalace.tusky.entity.Poll
|
import com.keylesspalace.tusky.entity.Poll
|
||||||
import com.keylesspalace.tusky.network.FilterModel
|
import com.keylesspalace.tusky.network.FilterModel
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
import com.keylesspalace.tusky.network.TimelineCases
|
|
||||||
import com.keylesspalace.tusky.settings.PrefKeys
|
import com.keylesspalace.tusky.settings.PrefKeys
|
||||||
|
import com.keylesspalace.tusky.usecase.TimelineCases
|
||||||
import com.keylesspalace.tusky.viewdata.StatusViewData
|
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
|
@ -37,6 +37,8 @@ data class AccountEntity(
|
||||||
@field:PrimaryKey(autoGenerate = true) var id: Long,
|
@field:PrimaryKey(autoGenerate = true) var id: Long,
|
||||||
val domain: String,
|
val domain: String,
|
||||||
var accessToken: String,
|
var accessToken: String,
|
||||||
|
var clientId: String?, // nullable for backward compatibility
|
||||||
|
var clientSecret: String?, // nullable for backward compatibility
|
||||||
var isActive: Boolean,
|
var isActive: Boolean,
|
||||||
var accountId: String = "",
|
var accountId: String = "",
|
||||||
var username: String = "",
|
var username: String = "",
|
||||||
|
@ -81,6 +83,15 @@ data class AccountEntity(
|
||||||
val fullName: String
|
val fullName: String
|
||||||
get() = "@$username@$domain"
|
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 {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (this === other) return true
|
if (this === other) return true
|
||||||
if (javaClass != other?.javaClass) return false
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
|
@ -54,7 +54,13 @@ class AccountManager @Inject constructor(db: AppDatabase) {
|
||||||
* @param accessToken the access token for the new account
|
* @param accessToken the access token for the new account
|
||||||
* @param domain the domain of the accounts Mastodon instance
|
* @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 {
|
activeAccount?.let {
|
||||||
it.isActive = false
|
it.isActive = false
|
||||||
|
@ -66,8 +72,13 @@ class AccountManager @Inject constructor(db: AppDatabase) {
|
||||||
val maxAccountId = accounts.maxByOrNull { it.id }?.id ?: 0
|
val maxAccountId = accounts.maxByOrNull { it.id }?.id ?: 0
|
||||||
val newAccountId = maxAccountId + 1
|
val newAccountId = maxAccountId + 1
|
||||||
activeAccount = AccountEntity(
|
activeAccount = AccountEntity(
|
||||||
id = newAccountId, domain = domain.lowercase(Locale.ROOT),
|
id = newAccountId,
|
||||||
accessToken = accessToken, oauthScopes = oauthScopes, isActive = true
|
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? {
|
fun logActiveAccountOut(): AccountEntity? {
|
||||||
|
|
||||||
if (activeAccount == null) {
|
return activeAccount?.let { account ->
|
||||||
return null
|
|
||||||
} else {
|
account.logout()
|
||||||
accounts.remove(activeAccount!!)
|
|
||||||
accountDao.delete(activeAccount!!)
|
accounts.remove(account)
|
||||||
|
accountDao.delete(account)
|
||||||
|
|
||||||
if (accounts.size > 0) {
|
if (accounts.size > 0) {
|
||||||
accounts[0].isActive = true
|
accounts[0].isActive = true
|
||||||
|
@ -103,7 +115,7 @@ class AccountManager @Inject constructor(db: AppDatabase) {
|
||||||
} else {
|
} else {
|
||||||
activeAccount = null
|
activeAccount = null
|
||||||
}
|
}
|
||||||
return activeAccount
|
activeAccount
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -31,7 +31,7 @@ import java.io.File;
|
||||||
*/
|
*/
|
||||||
@Database(entities = { DraftEntity.class, AccountEntity.class, InstanceEntity.class, TimelineStatusEntity.class,
|
@Database(entities = { DraftEntity.class, AccountEntity.class, InstanceEntity.class, TimelineStatusEntity.class,
|
||||||
TimelineAccountEntity.class, ConversationEntity.class
|
TimelineAccountEntity.class, ConversationEntity.class
|
||||||
}, version = 38)
|
}, version = 39)
|
||||||
public abstract class AppDatabase extends RoomDatabase {
|
public abstract class AppDatabase extends RoomDatabase {
|
||||||
|
|
||||||
public abstract AccountDao accountDao();
|
public abstract AccountDao accountDao();
|
||||||
|
@ -573,4 +573,12 @@ public abstract class AppDatabase extends RoomDatabase {
|
||||||
database.execSQL("DELETE FROM `TimelineStatusEntity`");
|
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");
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,7 +64,8 @@ class AppModule {
|
||||||
AppDatabase.MIGRATION_26_27, AppDatabase.MIGRATION_27_28, AppDatabase.MIGRATION_28_29,
|
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_29_30, AppDatabase.MIGRATION_30_31, AppDatabase.MIGRATION_31_32,
|
||||||
AppDatabase.MIGRATION_32_33, AppDatabase.MIGRATION_33_34, AppDatabase.MIGRATION_34_35,
|
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()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,7 +56,7 @@ import com.keylesspalace.tusky.di.Injectable;
|
||||||
import com.keylesspalace.tusky.entity.Attachment;
|
import com.keylesspalace.tusky.entity.Attachment;
|
||||||
import com.keylesspalace.tusky.entity.Status;
|
import com.keylesspalace.tusky.entity.Status;
|
||||||
import com.keylesspalace.tusky.network.MastodonApi;
|
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.LinkHelper;
|
||||||
import com.keylesspalace.tusky.util.StatusParsingHelper;
|
import com.keylesspalace.tusky.util.StatusParsingHelper;
|
||||||
import com.keylesspalace.tusky.view.MuteAccountDialog;
|
import com.keylesspalace.tusky.view.MuteAccountDialog;
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky.network;
|
package com.keylesspalace.tusky.network;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import com.keylesspalace.tusky.db.AccountEntity;
|
import com.keylesspalace.tusky.db.AccountEntity;
|
||||||
|
@ -22,22 +23,20 @@ import com.keylesspalace.tusky.db.AccountManager;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
import okhttp3.HttpUrl;
|
import okhttp3.*;
|
||||||
import okhttp3.Interceptor;
|
|
||||||
import okhttp3.Request;
|
|
||||||
import okhttp3.Response;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Created by charlag on 31/10/17.
|
* Created by charlag on 31/10/17.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
public final class InstanceSwitchAuthInterceptor implements Interceptor {
|
public final class InstanceSwitchAuthInterceptor implements Interceptor {
|
||||||
private AccountManager accountManager;
|
private final AccountManager accountManager;
|
||||||
|
|
||||||
public InstanceSwitchAuthInterceptor(AccountManager accountManager) {
|
public InstanceSwitchAuthInterceptor(AccountManager accountManager) {
|
||||||
this.accountManager = accountManager;
|
this.accountManager = accountManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public Response intercept(@NonNull Chain chain) throws IOException {
|
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.url(swapHost(originalRequest.url(), instanceHeader));
|
||||||
builder.removeHeader(MastodonApi.DOMAIN_HEADER);
|
builder.removeHeader(MastodonApi.DOMAIN_HEADER);
|
||||||
} else if (currentAccount != null) {
|
} else if (currentAccount != null) {
|
||||||
|
String accessToken = currentAccount.getAccessToken();
|
||||||
|
if (!accessToken.isEmpty()) {
|
||||||
//use domain of current account
|
//use domain of current account
|
||||||
builder.url(swapHost(originalRequest.url(), currentAccount.getDomain()))
|
builder.url(swapHost(originalRequest.url(), currentAccount.getDomain()))
|
||||||
.header("Authorization",
|
.header("Authorization",
|
||||||
String.format("Bearer %s", currentAccount.getAccessToken()));
|
String.format("Bearer %s", currentAccount.getAccessToken()));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
Request newRequest = builder.build();
|
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);
|
return chain.proceed(newRequest);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -459,6 +459,14 @@ interface MastodonApi {
|
||||||
@Field("grant_type") grantType: String
|
@Field("grant_type") grantType: String
|
||||||
): NetworkResult<AccessToken>
|
): NetworkResult<AccessToken>
|
||||||
|
|
||||||
|
@FormUrlEncoded
|
||||||
|
@POST("oauth/revoke")
|
||||||
|
suspend fun revokeOAuthToken(
|
||||||
|
@Field("client_id") clientId: String,
|
||||||
|
@Field("client_secret") clientSecret: String,
|
||||||
|
@Field("token") token: String
|
||||||
|
): NetworkResult<Unit>
|
||||||
|
|
||||||
@GET("/api/v1/lists")
|
@GET("/api/v1/lists")
|
||||||
suspend fun getLists(): NetworkResult<List<MastoList>>
|
suspend fun getLists(): NetworkResult<List<MastoList>>
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,7 +13,7 @@
|
||||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||||
* see <http://www.gnu.org/licenses>. */
|
* see <http://www.gnu.org/licenses>. */
|
||||||
|
|
||||||
package com.keylesspalace.tusky.network
|
package com.keylesspalace.tusky.usecase
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.keylesspalace.tusky.appstore.BlockEvent
|
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.DeletedStatus
|
||||||
import com.keylesspalace.tusky.entity.Poll
|
import com.keylesspalace.tusky.entity.Poll
|
||||||
import com.keylesspalace.tusky.entity.Status
|
import com.keylesspalace.tusky.entity.Status
|
||||||
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
import io.reactivex.rxjava3.core.Single
|
import io.reactivex.rxjava3.core.Single
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||||
import io.reactivex.rxjava3.kotlin.addTo
|
import io.reactivex.rxjava3.kotlin.addTo
|
|
@ -13,6 +13,7 @@
|
||||||
tools:context="com.keylesspalace.tusky.MainActivity">
|
tools:context="com.keylesspalace.tusky.MainActivity">
|
||||||
|
|
||||||
<com.google.android.material.appbar.AppBarLayout
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
|
android:id="@+id/appBar"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:elevation="@dimen/actionbar_elevation"
|
android:elevation="@dimen/actionbar_elevation"
|
||||||
|
@ -75,6 +76,13 @@
|
||||||
|
|
||||||
<include layout="@layout/item_status_bottom_sheet" />
|
<include layout="@layout/item_status_bottom_sheet" />
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/progressBar"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:visibility="gone"
|
||||||
|
android:layout_gravity="center" />
|
||||||
|
|
||||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
|
|
||||||
<com.mikepenz.materialdrawer.widget.MaterialDrawerSliderView
|
<com.mikepenz.materialdrawer.widget.MaterialDrawerSliderView
|
||||||
|
|
|
@ -68,6 +68,8 @@ class ComposeActivityTest {
|
||||||
id = 1,
|
id = 1,
|
||||||
domain = instanceDomain,
|
domain = instanceDomain,
|
||||||
accessToken = "token",
|
accessToken = "token",
|
||||||
|
clientId = "id",
|
||||||
|
clientSecret = "secret",
|
||||||
isActive = true,
|
isActive = true,
|
||||||
accountId = "1",
|
accountId = "1",
|
||||||
username = "username",
|
username = "username",
|
||||||
|
|
|
@ -46,6 +46,8 @@ class CachedTimelineRemoteMediatorTest {
|
||||||
id = 1,
|
id = 1,
|
||||||
domain = "mastodon.example",
|
domain = "mastodon.example",
|
||||||
accessToken = "token",
|
accessToken = "token",
|
||||||
|
clientId = "id",
|
||||||
|
clientSecret = "secret",
|
||||||
isActive = true
|
isActive = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,6 +38,8 @@ class NetworkTimelineRemoteMediatorTest {
|
||||||
id = 1,
|
id = 1,
|
||||||
domain = "mastodon.example",
|
domain = "mastodon.example",
|
||||||
accessToken = "token",
|
accessToken = "token",
|
||||||
|
clientId = "id",
|
||||||
|
clientSecret = "secret",
|
||||||
isActive = true
|
isActive = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue