Use AccountManager
This commit is contained in:
@@ -15,16 +15,27 @@
|
|||||||
android:roundIcon="@mipmap/ic_launcher"
|
android:roundIcon="@mipmap/ic_launcher"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
tools:ignore="UnusedAttribute"
|
tools:ignore="UnusedAttribute"
|
||||||
android:theme="@style/Theme.Mealient">
|
android:theme="@style/Theme.Mealient">
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
</application>
|
|
||||||
|
<service
|
||||||
|
android:name=".service.auth.AuthenticationService"
|
||||||
|
android:exported="false">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.accounts.AccountAuthenticator" />
|
||||||
|
</intent-filter>
|
||||||
|
<meta-data
|
||||||
|
android:name="android.accounts.AccountAuthenticator"
|
||||||
|
android:resource="@xml/account_authenticator" />
|
||||||
|
</service>
|
||||||
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -1,15 +1,18 @@
|
|||||||
package gq.kirmanak.mealient.data.auth.impl
|
package gq.kirmanak.mealient.data.auth.impl
|
||||||
|
|
||||||
import androidx.datastore.preferences.core.Preferences
|
import androidx.datastore.preferences.core.Preferences
|
||||||
|
import dagger.hilt.android.scopes.ActivityScoped
|
||||||
import gq.kirmanak.mealient.data.auth.AuthStorage
|
import gq.kirmanak.mealient.data.auth.AuthStorage
|
||||||
import gq.kirmanak.mealient.data.storage.PreferencesStorage
|
import gq.kirmanak.mealient.data.storage.PreferencesStorage
|
||||||
|
import gq.kirmanak.mealient.service.auth.AccountManagerInteractor
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
|
||||||
|
|
||||||
@Singleton
|
@ActivityScoped
|
||||||
class AuthStorageImpl @Inject constructor(
|
class AuthStorageImpl @Inject constructor(
|
||||||
|
private val accountManagerInteractorImpl: AccountManagerInteractor,
|
||||||
private val preferencesStorage: PreferencesStorage,
|
private val preferencesStorage: PreferencesStorage,
|
||||||
) : AuthStorage {
|
) : AuthStorage {
|
||||||
|
|
||||||
@@ -30,6 +33,18 @@ class AuthStorageImpl @Inject constructor(
|
|||||||
return token
|
return token
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun authHeaderObservable(): Flow<String?> {
|
||||||
|
Timber.v("authHeaderObservable() called")
|
||||||
|
return accountManagerInteractorImpl.accountUpdatesFlow()
|
||||||
|
.map { it.firstOrNull() }
|
||||||
|
.map { account ->
|
||||||
|
account ?: return@map null
|
||||||
|
runCatching { accountManagerInteractorImpl.getAuthToken(account) }
|
||||||
|
.onFailure { Timber.e(it, "authHeaderObservable: can't get token") }
|
||||||
|
.getOrNull()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun clearAuthData() {
|
override suspend fun clearAuthData() {
|
||||||
Timber.v("clearAuthData() called")
|
Timber.v("clearAuthData() called")
|
||||||
preferencesStorage.removeValues(authHeaderKey)
|
preferencesStorage.removeValues(authHeaderKey)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import dagger.Module
|
|||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.components.SingletonComponent
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
import gq.kirmanak.mealient.R
|
||||||
import gq.kirmanak.mealient.data.auth.AuthDataSource
|
import gq.kirmanak.mealient.data.auth.AuthDataSource
|
||||||
import gq.kirmanak.mealient.data.auth.AuthRepo
|
import gq.kirmanak.mealient.data.auth.AuthRepo
|
||||||
import gq.kirmanak.mealient.data.auth.AuthStorage
|
import gq.kirmanak.mealient.data.auth.AuthStorage
|
||||||
@@ -16,6 +17,7 @@ import gq.kirmanak.mealient.data.baseurl.BaseURLStorage
|
|||||||
import gq.kirmanak.mealient.data.network.RetrofitBuilder
|
import gq.kirmanak.mealient.data.network.RetrofitBuilder
|
||||||
import gq.kirmanak.mealient.data.network.ServiceFactory
|
import gq.kirmanak.mealient.data.network.ServiceFactory
|
||||||
import gq.kirmanak.mealient.data.network.createServiceFactory
|
import gq.kirmanak.mealient.data.network.createServiceFactory
|
||||||
|
import gq.kirmanak.mealient.service.auth.AccountParameters
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
@@ -24,12 +26,26 @@ interface AuthModule {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
|
const val ACCOUNT_TYPE = "Mealient"
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideAuthServiceFactory(
|
fun provideAuthServiceFactory(retrofitBuilder: RetrofitBuilder,
|
||||||
retrofitBuilder: RetrofitBuilder,
|
|
||||||
baseURLStorage: BaseURLStorage,
|
baseURLStorage: BaseURLStorage,
|
||||||
): ServiceFactory<AuthService> = retrofitBuilder.createServiceFactory(baseURLStorage)
|
): ServiceFactory<AuthService> = retrofitBuilder.createServiceFactory(baseURLStorage)
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideAccountManager(@ApplicationContext context: Context): AccountManager {
|
||||||
|
return AccountManager.get(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun provideAccountType(@ApplicationContext context: Context) = AccountParameters(
|
||||||
|
accountType = context.getString(R.string.account_type),
|
||||||
|
authTokenType = context.getString(R.string.auth_token_type),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
|
|||||||
@@ -0,0 +1,141 @@
|
|||||||
|
package gq.kirmanak.mealient.service.auth
|
||||||
|
|
||||||
|
import android.accounts.*
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import gq.kirmanak.mealient.data.auth.AuthDataSource
|
||||||
|
import gq.kirmanak.mealient.data.auth.impl.AuthenticationError
|
||||||
|
import gq.kirmanak.mealient.ui.auth.AuthenticatorActivity
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class AccountAuthenticatorImpl @Inject constructor(
|
||||||
|
@ApplicationContext private val context: Context,
|
||||||
|
private val authDataSource: AuthDataSource,
|
||||||
|
private val accountParameters: AccountParameters,
|
||||||
|
private val accountManager: AccountManager,
|
||||||
|
) : AbstractAccountAuthenticator(context) {
|
||||||
|
|
||||||
|
override fun addAccount(
|
||||||
|
response: AccountAuthenticatorResponse,
|
||||||
|
accountType: String,
|
||||||
|
authTokenType: String,
|
||||||
|
requiredFeatures: Array<out String>?,
|
||||||
|
options: Bundle,
|
||||||
|
): Bundle {
|
||||||
|
Timber.v("addAccount() called with: response = $response, accountType = $accountType, authTokenType = $authTokenType, requiredFeatures = $requiredFeatures, options = $options")
|
||||||
|
|
||||||
|
try {
|
||||||
|
checkAccountType(accountType)
|
||||||
|
checkAuthTokenType(authTokenType)
|
||||||
|
} catch (e: AuthenticatorException) {
|
||||||
|
Timber.e(e, "addAccount: validation failure")
|
||||||
|
return e.bundle
|
||||||
|
}
|
||||||
|
|
||||||
|
val intent = Intent(context, AuthenticatorActivity::class.java)
|
||||||
|
return Bundle().apply { putParcelable(AccountManager.KEY_INTENT, intent) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getAuthToken(
|
||||||
|
response: AccountAuthenticatorResponse,
|
||||||
|
account: Account,
|
||||||
|
authTokenType: String,
|
||||||
|
options: Bundle
|
||||||
|
): Bundle {
|
||||||
|
Timber.v("getAuthToken() called with: response = $response, account = $account, authTokenType = $authTokenType, options = $options")
|
||||||
|
|
||||||
|
val password: String?
|
||||||
|
val baseUrl: String?
|
||||||
|
try {
|
||||||
|
checkAccountType(account.type)
|
||||||
|
checkAuthTokenType(authTokenType)
|
||||||
|
password = accountManager.getPassword(account)
|
||||||
|
?: throw AuthenticatorException.AccountNotFound(account)
|
||||||
|
baseUrl = options.getString(KEY_BASE_URL) ?: throw AuthenticatorException.NoBaseUrl
|
||||||
|
} catch (e: AuthenticatorException) {
|
||||||
|
Timber.e(e, "getAuthToken: validation failure")
|
||||||
|
return e.bundle
|
||||||
|
}
|
||||||
|
|
||||||
|
val token = try {
|
||||||
|
runBlocking { authDataSource.authenticate(account.name, password, baseUrl) }
|
||||||
|
} catch (e: RuntimeException) {
|
||||||
|
return when (e) {
|
||||||
|
is AuthenticationError.NotMealie -> AuthenticatorException.NotMealie.bundle
|
||||||
|
is AuthenticationError.Unauthorized -> AuthenticatorException.Unauthorized.bundle
|
||||||
|
else -> throw NetworkErrorException(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Bundle().apply {
|
||||||
|
putString(AccountManager.KEY_ACCOUNT_NAME, account.name)
|
||||||
|
putString(AccountManager.KEY_ACCOUNT_TYPE, accountParameters.accountType)
|
||||||
|
putString(AccountManager.KEY_AUTHTOKEN, token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// region Unsupported operations
|
||||||
|
override fun confirmCredentials(
|
||||||
|
response: AccountAuthenticatorResponse?,
|
||||||
|
account: Account?,
|
||||||
|
options: Bundle?
|
||||||
|
): Bundle {
|
||||||
|
Timber.v("confirmCredentials() called with: response = $response, account = $account, options = $options")
|
||||||
|
return AuthenticatorException.UnsupportedOperation("confirmCredentials").bundle
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun editProperties(
|
||||||
|
response: AccountAuthenticatorResponse,
|
||||||
|
accountType: String,
|
||||||
|
): Bundle? {
|
||||||
|
Timber.v("editProperties() called with: response = $response, accountType = $accountType")
|
||||||
|
response.onError(
|
||||||
|
AccountManager.ERROR_CODE_UNSUPPORTED_OPERATION,
|
||||||
|
"editProperties is not supported"
|
||||||
|
)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getAuthTokenLabel(authTokenType: String?): String? {
|
||||||
|
Timber.v("getAuthTokenLabel() called with: authTokenType = $authTokenType")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun updateCredentials(
|
||||||
|
response: AccountAuthenticatorResponse?,
|
||||||
|
account: Account?,
|
||||||
|
authTokenType: String?,
|
||||||
|
options: Bundle?
|
||||||
|
): Bundle {
|
||||||
|
Timber.v("updateCredentials() called with: response = $response, account = $account, authTokenType = $authTokenType, options = $options")
|
||||||
|
return AuthenticatorException.UnsupportedOperation("updateCredentials").bundle
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hasFeatures(
|
||||||
|
response: AccountAuthenticatorResponse?,
|
||||||
|
account: Account?,
|
||||||
|
features: Array<out String>?
|
||||||
|
): Bundle {
|
||||||
|
Timber.v("hasFeatures() called with: response = $response, account = $account, features = $features")
|
||||||
|
return Bundle().apply { putBoolean(AccountManager.KEY_BOOLEAN_RESULT, true) }
|
||||||
|
}
|
||||||
|
// end region
|
||||||
|
|
||||||
|
private fun checkAccountType(accountType: String) {
|
||||||
|
if (accountType != accountParameters.accountType) {
|
||||||
|
throw AuthenticatorException.UnsupportedAccountType(accountType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkAuthTokenType(authTokenType: String) {
|
||||||
|
if (authTokenType != accountParameters.accountType) {
|
||||||
|
throw AuthenticatorException.UnsupportedAccountType(authTokenType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package gq.kirmanak.mealient.service.auth
|
||||||
|
|
||||||
|
import android.accounts.Account
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
interface AccountManagerInteractor {
|
||||||
|
|
||||||
|
fun getAccounts(): Array<Account>
|
||||||
|
|
||||||
|
suspend fun addAccount(): Account
|
||||||
|
|
||||||
|
suspend fun getAuthToken(account: Account): String
|
||||||
|
|
||||||
|
fun accountUpdatesFlow(): Flow<Array<Account>>
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package gq.kirmanak.mealient.service.auth
|
||||||
|
|
||||||
|
import android.accounts.Account
|
||||||
|
import android.accounts.AccountManager
|
||||||
|
import android.app.Activity
|
||||||
|
import dagger.hilt.android.scopes.ActivityScoped
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ActivityScoped
|
||||||
|
class AccountManagerInteractorImpl @Inject constructor(
|
||||||
|
private val accountManager: AccountManager,
|
||||||
|
private val accountParameters: AccountParameters,
|
||||||
|
private val activity: Activity
|
||||||
|
) : AccountManagerInteractor {
|
||||||
|
|
||||||
|
override fun getAccounts(): Array<Account> {
|
||||||
|
Timber.v("getAccounts() called")
|
||||||
|
val accounts = accountManager.getAccountsByType(accountParameters.accountType)
|
||||||
|
Timber.v("getAccounts() returned: $accounts")
|
||||||
|
return accounts
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun addAccount(): Account {
|
||||||
|
Timber.v("addAccount() called")
|
||||||
|
val bundle = accountManager.addAccount(
|
||||||
|
accountParameters.accountType,
|
||||||
|
accountParameters.authTokenType,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
activity,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
).await()
|
||||||
|
return bundle.toAccount()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getAuthToken(account: Account): String {
|
||||||
|
Timber.v("getAuthToken() called with: account = $account")
|
||||||
|
val bundle = accountManager.getAuthToken(
|
||||||
|
account,
|
||||||
|
accountParameters.authTokenType,
|
||||||
|
null,
|
||||||
|
activity,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
).await()
|
||||||
|
val receivedAccount = bundle.toAccount()
|
||||||
|
check(account == receivedAccount) { "Received account ($receivedAccount) differs from requested ($account)" }
|
||||||
|
val token = bundle.authToken()
|
||||||
|
Timber.v("getAuthToken() returned: $token")
|
||||||
|
return token
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun accountUpdatesFlow(): Flow<Array<Account>> {
|
||||||
|
return accountManager.accountUpdatesFlow(accountParameters.accountType)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package gq.kirmanak.mealient.service.auth
|
||||||
|
|
||||||
|
data class AccountParameters(
|
||||||
|
val accountType: String,
|
||||||
|
val authTokenType: String,
|
||||||
|
)
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package gq.kirmanak.mealient.service.auth
|
||||||
|
|
||||||
|
import android.accounts.Account
|
||||||
|
import android.accounts.AccountManager
|
||||||
|
import android.accounts.AccountManager.*
|
||||||
|
import android.accounts.AccountManagerFuture
|
||||||
|
import android.accounts.OnAccountsUpdateListener
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.channels.awaitClose
|
||||||
|
import kotlinx.coroutines.channels.onFailure
|
||||||
|
import kotlinx.coroutines.channels.onSuccess
|
||||||
|
import kotlinx.coroutines.channels.trySendBlocking
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.callbackFlow
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
internal const val KEY_BASE_URL = "mealientBaseUrl"
|
||||||
|
|
||||||
|
internal suspend fun <T> AccountManagerFuture<T>.await(): T = withContext(Dispatchers.IO) { result }
|
||||||
|
|
||||||
|
internal fun Bundle.toAccount(): Account = Account(accountName(), accountType())
|
||||||
|
|
||||||
|
internal fun Bundle.accountType(): String = string(KEY_ACCOUNT_TYPE) { "Account type is null" }
|
||||||
|
|
||||||
|
internal fun Bundle.accountName(): String = string(KEY_ACCOUNT_NAME) { "Account name is null" }
|
||||||
|
|
||||||
|
internal fun Bundle.authToken(): String = string(KEY_AUTHTOKEN) { "Auth token is null" }
|
||||||
|
|
||||||
|
private fun Bundle.string(key: String, error: () -> String) = checkNotNull(getString(key), error)
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
internal fun AccountManager.accountUpdatesFlow(vararg types: String): Flow<Array<Account>> =
|
||||||
|
callbackFlow {
|
||||||
|
Timber.v("accountUpdatesFlow() called")
|
||||||
|
val listener = OnAccountsUpdateListener { accounts ->
|
||||||
|
Timber.d("accountUpdatesFlow: updated accounts = $accounts")
|
||||||
|
val filtered = accounts.filter { types.contains(it.type) }.toTypedArray()
|
||||||
|
Timber.d("accountUpdatesFlow: filtered accounts = $filtered")
|
||||||
|
trySendBlocking(filtered)
|
||||||
|
.onSuccess { Timber.d("accountUpdatesFlow: sent accounts update") }
|
||||||
|
.onFailure { Timber.e(it, "accountUpdatesFlow: failed to send update") }
|
||||||
|
}
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
addOnAccountsUpdatedListener(listener, null, true, types)
|
||||||
|
} else {
|
||||||
|
addOnAccountsUpdatedListener(listener, null, true)
|
||||||
|
}
|
||||||
|
awaitClose {
|
||||||
|
Timber.d("accountUpdatesFlow: cancelled")
|
||||||
|
removeOnAccountsUpdatedListener(listener)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package gq.kirmanak.mealient.service.auth
|
||||||
|
|
||||||
|
import android.app.Service
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.IBinder
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class AuthenticationService : Service() {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var accountAuthenticatorImpl: AccountAuthenticatorImpl
|
||||||
|
|
||||||
|
override fun onBind(intent: Intent?): IBinder? = accountAuthenticatorImpl.iBinder
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package gq.kirmanak.mealient.service.auth
|
||||||
|
|
||||||
|
import android.accounts.Account
|
||||||
|
import android.accounts.AccountManager
|
||||||
|
import android.os.Bundle
|
||||||
|
|
||||||
|
sealed class AuthenticatorException(
|
||||||
|
val bundle: Bundle
|
||||||
|
) : RuntimeException() {
|
||||||
|
|
||||||
|
constructor(errorCode: Int, errorMessage: String) : this(
|
||||||
|
Bundle().apply {
|
||||||
|
putInt(AccountManager.KEY_ERROR_CODE, errorCode)
|
||||||
|
putString(AccountManager.KEY_ERROR_MESSAGE, errorMessage)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
class UnsupportedAuthTokenType(received: String?) : AuthenticatorException(
|
||||||
|
errorCode = AccountManager.ERROR_CODE_BAD_ARGUMENTS,
|
||||||
|
errorMessage = "Received auth token type = $received"
|
||||||
|
)
|
||||||
|
|
||||||
|
class UnsupportedAccountType(received: String?) : AuthenticatorException(
|
||||||
|
errorCode = AccountManager.ERROR_CODE_BAD_ARGUMENTS,
|
||||||
|
errorMessage = "Received account type = $received"
|
||||||
|
)
|
||||||
|
|
||||||
|
class UnsupportedOperation(operation: String) : AuthenticatorException(
|
||||||
|
errorCode = AccountManager.ERROR_CODE_UNSUPPORTED_OPERATION,
|
||||||
|
errorMessage = "$operation is not supported"
|
||||||
|
)
|
||||||
|
|
||||||
|
class AccountNotFound(account: Account) : AuthenticatorException(
|
||||||
|
errorCode = AccountManager.ERROR_CODE_BAD_ARGUMENTS,
|
||||||
|
errorMessage = "$account not found"
|
||||||
|
)
|
||||||
|
|
||||||
|
object NoBaseUrl : AuthenticatorException(
|
||||||
|
errorCode = AccountManager.ERROR_CODE_BAD_ARGUMENTS,
|
||||||
|
errorMessage = "Base URL was not provided"
|
||||||
|
)
|
||||||
|
|
||||||
|
object Unauthorized : AuthenticatorException(
|
||||||
|
errorCode = AccountManager.ERROR_CODE_BAD_ARGUMENTS,
|
||||||
|
errorMessage = "E-mail or password weren't correct"
|
||||||
|
)
|
||||||
|
|
||||||
|
object NotMealie : AuthenticatorException(
|
||||||
|
errorCode = AccountManager.ERROR_CODE_BAD_ARGUMENTS,
|
||||||
|
errorMessage = "Base URL must be pointing at a non Mealie server"
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package gq.kirmanak.mealient.ui.auth
|
||||||
|
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class AuthenticatorActivity : AppCompatActivity() {
|
||||||
|
|
||||||
|
}
|
||||||
38
app/src/main/res/layout/authenticator_activity.xml
Normal file
38
app/src/main/res/layout/authenticator_activity.xml
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
tools:context=".ui.auth.AuthenticatorActivity">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
|
android:id="@+id/toolbar_holder"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:liftOnScroll="true">
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.MaterialToolbar
|
||||||
|
android:id="@+id/toolbar"
|
||||||
|
style="@style/Widget.MaterialComponents.Toolbar.Primary"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="?actionBarSize"
|
||||||
|
app:layout_scrollFlags="scroll|snap" />
|
||||||
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
|
<androidx.fragment.app.FragmentContainerView
|
||||||
|
android:id="@+id/nav_host"
|
||||||
|
android:name="androidx.navigation.fragment.NavHostFragment"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
app:defaultNavHost="true"
|
||||||
|
app:layout_behavior="@string/appbar_scrolling_view_behavior"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/toolbar_holder"
|
||||||
|
app:navGraph="@navigation/authenticator_graph" />
|
||||||
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
13
app/src/main/res/navigation/authenticator_graph.xml
Normal file
13
app/src/main/res/navigation/authenticator_graph.xml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/authenticator_graph"
|
||||||
|
app:startDestination="@id/authenticationFragment2">
|
||||||
|
|
||||||
|
<fragment
|
||||||
|
android:id="@+id/authenticationFragment2"
|
||||||
|
android:name="gq.kirmanak.mealient.ui.auth.AuthenticationFragment"
|
||||||
|
android:label="fragment_authentication"
|
||||||
|
tools:layout="@layout/fragment_authentication" />
|
||||||
|
</navigation>
|
||||||
@@ -12,18 +12,20 @@
|
|||||||
<string name="fragment_recipe_info_ingredients_header">Ingredients</string>
|
<string name="fragment_recipe_info_ingredients_header">Ingredients</string>
|
||||||
<string name="fragment_recipe_info_instructions_header">Instructions</string>
|
<string name="fragment_recipe_info_instructions_header">Instructions</string>
|
||||||
<string name="fragment_disclaimer_main_text">This project is developed independently from the core Mealie project. It is NOT associated with the core Mealie developers. Any issues must be reported to the Mealient repository, NOT the Mealie repository.</string>
|
<string name="fragment_disclaimer_main_text">This project is developed independently from the core Mealie project. It is NOT associated with the core Mealie developers. Any issues must be reported to the Mealient repository, NOT the Mealie repository.</string>
|
||||||
<string name="fragment_disclaimer_button_okay">Okay</string>
|
|
||||||
<string name="view_holder_recipe_instructions_step">Step: %d</string>
|
|
||||||
<string name="fragment_authentication_email_input_empty">E-mail can\'t be empty</string>
|
|
||||||
<string name="fragment_authentication_password_input_empty">Password can\'t be empty</string>
|
|
||||||
<string name="fragment_baseurl_url_input_empty">URL can\'t be empty</string>
|
<string name="fragment_baseurl_url_input_empty">URL can\'t be empty</string>
|
||||||
<string name="fragment_authentication_credentials_incorrect">E-mail or password is incorrect.</string>
|
|
||||||
<string name="fragment_base_url_no_connection">Can\'t connect, check address.</string>
|
<string name="fragment_base_url_no_connection">Can\'t connect, check address.</string>
|
||||||
<string name="fragment_base_url_unexpected_response">Unexpected response. Is it Mealie?</string>
|
<string name="fragment_base_url_unexpected_response">Unexpected response. Is it Mealie?</string>
|
||||||
<string name="fragment_authentication_unknown_error">Something went wrong, please try again.</string>
|
|
||||||
<string name="fragment_base_url_malformed_url">Check URL format: %s</string>
|
<string name="fragment_base_url_malformed_url">Check URL format: %s</string>
|
||||||
<string name="fragment_base_url_save">Proceed</string>
|
<string name="fragment_base_url_save">Proceed</string>
|
||||||
<string name="fragment_base_url_unknown_error" translatable="false">@string/fragment_authentication_unknown_error</string>
|
<string name="fragment_base_url_unknown_error" translatable="false">@string/fragment_authentication_unknown_error</string>
|
||||||
<string name="menu_main_toolbar_content_description_login" translatable="false">@string/menu_main_toolbar_login</string>
|
<string name="menu_main_toolbar_content_description_login" translatable="false">@string/menu_main_toolbar_login</string>
|
||||||
<string name="menu_main_toolbar_login">Login</string>
|
<string name="menu_main_toolbar_login">Login</string>
|
||||||
|
<string name="fragment_disclaimer_button_okay">Okay</string>
|
||||||
|
<string name="view_holder_recipe_instructions_step">Step: %d</string>
|
||||||
|
<string name="fragment_authentication_email_input_empty">E-mail can\'t be empty</string>
|
||||||
|
<string name="fragment_authentication_password_input_empty">Password can\'t be empty</string>
|
||||||
|
<string name="fragment_authentication_credentials_incorrect">E-mail or password is incorrect.</string>
|
||||||
|
<string name="fragment_authentication_unknown_error">Something went wrong, please try again.</string>
|
||||||
|
<string name="account_type" translatable="false">Mealient</string>
|
||||||
|
<string name="auth_token_type" translatable="false">mealientAuthToken</string>
|
||||||
</resources>
|
</resources>
|
||||||
6
app/src/main/res/xml/account_authenticator.xml
Normal file
6
app/src/main/res/xml/account_authenticator.xml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<account-authenticator xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:accountType="@string/account_type"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:label="@string/app_name" />
|
||||||
Reference in New Issue
Block a user