You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
183 lines
6.7 KiB
Kotlin
183 lines
6.7 KiB
Kotlin
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
package org.mozilla.fenix.components
|
|
|
|
import android.content.Context
|
|
import android.content.SharedPreferences
|
|
import android.os.StrictMode
|
|
import androidx.annotation.GuardedBy
|
|
import androidx.annotation.VisibleForTesting
|
|
import kotlinx.coroutines.Dispatchers
|
|
import mozilla.components.concept.sync.AccountObserver
|
|
import mozilla.components.concept.sync.AuthType
|
|
import mozilla.components.concept.sync.OAuthAccount
|
|
import mozilla.components.lib.crash.CrashReporter
|
|
import mozilla.components.service.fxa.manager.FxaAccountManager
|
|
import mozilla.components.support.base.log.logger.Logger
|
|
import org.mozilla.fenix.ext.resetPoliciesAfter
|
|
import kotlin.coroutines.CoroutineContext
|
|
|
|
/**
|
|
* Miscellaneous FxA-related abnormalities.
|
|
*/
|
|
@VisibleForTesting
|
|
internal abstract class AbnormalFxaEvent : Exception() {
|
|
/**
|
|
* Indicates an overlapping sign-out request.
|
|
*/
|
|
class OverlappingFxaLogoutRequest : AbnormalFxaEvent()
|
|
|
|
/**
|
|
* Indicates an onLogout callback which was received without a preceding onAuthenticated callback.
|
|
*/
|
|
class LogoutWithoutAuth : AbnormalFxaEvent()
|
|
|
|
/**
|
|
* Indicates an unexpected sign-out event. All events must be user-triggered; this exception is
|
|
* logged when a sign-out event was detected without a corresponding user action.
|
|
*/
|
|
class UnexpectedFxaLogout : AbnormalFxaEvent()
|
|
|
|
/**
|
|
* Indicates an account that's missing after startup, while it was expected to be present.
|
|
*/
|
|
class MissingExpectedAccountAfterStartup : AbnormalFxaEvent()
|
|
}
|
|
|
|
/**
|
|
* Observes account-related events, and reports any detected abnormalities via [crashReporter].
|
|
*
|
|
* See [AbnormalFxaEvent] for types of abnormal events this class detects.
|
|
*
|
|
* @param crashReporter An instance of [CrashReporter] used for reporting detected abnormalities.
|
|
* @param coroutineContext A [CoroutineContext] used for executing async tasks. Defaults to [Dispatchers.IO].
|
|
*/
|
|
class AccountAbnormalities(
|
|
context: Context,
|
|
private val crashReporter: CrashReporter,
|
|
private val coroutineContext: CoroutineContext = Dispatchers.IO
|
|
) : AccountObserver {
|
|
companion object {
|
|
private const val PREF_FXA_ABNORMALITIES = "fxa_abnormalities"
|
|
private const val KEY_HAS_ACCOUNT = "has_account"
|
|
}
|
|
|
|
@GuardedBy("this")
|
|
private var isLoggingOut = false
|
|
|
|
@Volatile
|
|
private var accountManagerConfigured = false
|
|
|
|
@Volatile
|
|
private var onAuthenticatedCalled = false
|
|
|
|
private val logger = Logger("AccountAbnormalities")
|
|
|
|
private val prefs: SharedPreferences
|
|
private val hadAccountPrior: Boolean
|
|
|
|
init {
|
|
val prefPair = StrictMode.allowThreadDiskReads().resetPoliciesAfter {
|
|
val p = context.getSharedPreferences(PREF_FXA_ABNORMALITIES, Context.MODE_PRIVATE)
|
|
val a = p.getBoolean(KEY_HAS_ACCOUNT, false)
|
|
Pair(p, a)
|
|
}
|
|
prefs = prefPair.first
|
|
hadAccountPrior = prefPair.second
|
|
}
|
|
|
|
/**
|
|
* Once [accountManager] is initialized, queries it to detect abnormal account states.
|
|
* Call this right after running [FxaAccountManager.initAsync].
|
|
*
|
|
* @param accountManager An instance of [FxaAccountManager].
|
|
* @param initResult A deferred result of initializing [accountManager].
|
|
* @return A [Unit] deferred, resolved once [initResult] is resolved and state is processed for abnormalities.
|
|
*/
|
|
fun accountManagerStarted(
|
|
accountManager: FxaAccountManager
|
|
) {
|
|
check(!accountManagerConfigured) { "accountManagerStarted called twice" }
|
|
accountManagerConfigured = true
|
|
|
|
// Behaviour considered abnormal:
|
|
// - we had an account before, and it's no longer present during startup
|
|
|
|
// We use a flag in prefs to keep track of the fact that we have an authenticated
|
|
// account. This works because our account state is persisted in the application's
|
|
// directory, same as SharedPreferences. If user clears application data, both the
|
|
// fxa state and our flag will be removed.
|
|
val hasAccountNow = accountManager.authenticatedAccount() != null
|
|
if (hadAccountPrior && !hasAccountNow) {
|
|
prefs.edit().putBoolean(KEY_HAS_ACCOUNT, false).apply()
|
|
|
|
logger.warn("Missing expected account on startup")
|
|
|
|
crashReporter.submitCaughtException(
|
|
AbnormalFxaEvent.MissingExpectedAccountAfterStartup()
|
|
)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Keeps track of user requests to logout.
|
|
*/
|
|
fun userRequestedLogout() {
|
|
check(accountManagerConfigured) {
|
|
"userRequestedLogout before account manager was configured"
|
|
}
|
|
|
|
// Expecting to have seen an onAuthenticated callback before a logout can be triggered.
|
|
if (!onAuthenticatedCalled) {
|
|
crashReporter.submitCaughtException(AbnormalFxaEvent.LogoutWithoutAuth())
|
|
}
|
|
|
|
// If we're not already in the process of logging out, do nothing.
|
|
synchronized(this) {
|
|
if (!isLoggingOut) {
|
|
isLoggingOut = true
|
|
return
|
|
}
|
|
}
|
|
|
|
logger.warn("Overlapping logout request")
|
|
|
|
// Otherwise, this is an unexpected logout request - there shouldn't be a legitimate way for
|
|
// the user to request multiple overlapping logouts. Log an exception.
|
|
crashReporter.submitCaughtException(AbnormalFxaEvent.OverlappingFxaLogoutRequest())
|
|
}
|
|
|
|
override fun onAuthenticated(account: OAuthAccount, authType: AuthType) {
|
|
// Not checking state of accountManagerConfigured because we'll race against account manager's start.
|
|
onAuthenticatedCalled = true
|
|
|
|
// We don't check if KEY_HAS_ACCOUNT was already true: we will see onAuthenticated on every
|
|
// startup, so any combination of "new value" and "previous value" for this flag is normal.
|
|
prefs.edit().putBoolean(KEY_HAS_ACCOUNT, true).apply()
|
|
}
|
|
|
|
override fun onLoggedOut() {
|
|
check(accountManagerConfigured) { "onLoggedOut before account manager was configured" }
|
|
|
|
onAuthenticatedCalled = false
|
|
|
|
prefs.edit().putBoolean(KEY_HAS_ACCOUNT, false).apply()
|
|
|
|
// If we're in the process of logging out (via userRequestedLogout), do nothing.
|
|
synchronized(this) {
|
|
if (isLoggingOut) {
|
|
isLoggingOut = false
|
|
return
|
|
}
|
|
}
|
|
|
|
logger.warn("Unexpected sign-out")
|
|
|
|
// Otherwise, this is an unexpected logout event - all logout events are expected to be
|
|
// user-triggered. Log an exception.
|
|
crashReporter.submitCaughtException(AbnormalFxaEvent.UnexpectedFxaLogout())
|
|
}
|
|
}
|