Migrate account settings

This commit is contained in:
MM20 2022-01-15 19:01:12 +01:00
parent 0902ee2583
commit 5b9ce4d4ae
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
19 changed files with 442 additions and 8 deletions

1
accounts/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

48
accounts/build.gradle.kts Normal file
View File

@ -0,0 +1,48 @@
plugins {
id("com.android.library")
id("kotlin-android")
}
android {
compileSdk = sdk.versions.compileSdk.get().toInt()
defaultConfig {
minSdk = sdk.versions.minSdk.get().toInt()
targetSdk = sdk.versions.targetSdk.get().toInt()
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
}
dependencies {
implementation(libs.bundles.kotlin)
implementation(libs.androidx.core)
implementation(libs.koin.android)
implementation(project(":g-services"))
implementation(project(":ms-services"))
implementation(project(":owncloud"))
implementation(project(":nextcloud"))
}

View File

21
accounts/proguard-rules.pro vendored Normal file
View File

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="de.mm20.launcher2.accounts">
</manifest>

View File

@ -0,0 +1,6 @@
package de.mm20.launcher2.accounts
data class Account(
val userName: String,
val type: AccountType,
)

View File

@ -0,0 +1,8 @@
package de.mm20.launcher2.accounts
enum class AccountType {
Google,
Microsoft,
Nextcloud,
Owncloud,
}

View File

@ -0,0 +1,132 @@
package de.mm20.launcher2.accounts
import android.app.Activity
import android.content.Context
import de.mm20.launcher2.gservices.GoogleApiHelper
import de.mm20.launcher2.msservices.MicrosoftGraphApiHelper
import de.mm20.launcher2.nextcloud.NextcloudApiHelper
import de.mm20.launcher2.owncloud.OwncloudClient
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
interface AccountsRepository {
fun signin(context: Activity, accountType: AccountType)
fun signout(accountType: AccountType)
/**
* Whether support for this account type is enabled in this build
*/
fun isSupported(accountType: AccountType): Boolean
suspend fun getCurrentlySignedInAccount(accountType: AccountType): Account?
}
internal class AccountsRepositoryImpl(
private val context: Context
) : AccountsRepository {
private val scope = CoroutineScope(Job() + Dispatchers.Main)
private val googleApiHelper = GoogleApiHelper.getInstance(context)
private val msGraphApiHelper = MicrosoftGraphApiHelper.getInstance(context)
private val nextcloudApiHelper = NextcloudApiHelper(context)
private val owncloudApiHelper = OwncloudClient(context)
override fun signin(context: Activity, accountType: AccountType) {
when (accountType) {
AccountType.Google -> {
scope.launch {
googleApiHelper.login(context)
}
}
AccountType.Microsoft -> {
scope.launch {
msGraphApiHelper.login(context)
}
}
AccountType.Nextcloud ->
scope.launch {
nextcloudApiHelper.login(context)
}
AccountType.Owncloud ->
scope.launch {
owncloudApiHelper.login(context, 0)
}
}
}
override fun signout(accountType: AccountType) {
when (accountType) {
AccountType.Google -> {
googleApiHelper.logout()
}
AccountType.Microsoft -> {
scope.launch {
msGraphApiHelper.logout()
}
}
AccountType.Nextcloud -> {
scope.launch {
nextcloudApiHelper.logout()
}
}
AccountType.Owncloud -> {
owncloudApiHelper.logout()
}
}
}
override fun isSupported(accountType: AccountType): Boolean {
return when (accountType) {
AccountType.Google -> googleApiHelper.isAvailable()
AccountType.Microsoft -> msGraphApiHelper.isAvailable()
AccountType.Nextcloud -> true
AccountType.Owncloud -> true
}
}
override suspend fun getCurrentlySignedInAccount(accountType: AccountType): Account? {
return when (accountType) {
AccountType.Google -> {
getGoogleAccount()
}
AccountType.Microsoft -> {
getMicrosoftAccount()
}
AccountType.Nextcloud -> {
getNextcloudAccount()
}
AccountType.Owncloud -> {
getOwncloudAccount()
}
}
}
private suspend fun getGoogleAccount(): Account? {
return googleApiHelper.getAccount()?.let {
Account(it.name, AccountType.Google)
}
}
private suspend fun getMicrosoftAccount(): Account? {
return msGraphApiHelper.getUser()?.let {
Account(it.name, AccountType.Microsoft)
}
}
private suspend fun getNextcloudAccount(): Account? {
return nextcloudApiHelper.getLoggedInUser()?.let {
Account(it.displayName, AccountType.Nextcloud)
}
}
private suspend fun getOwncloudAccount(): Account? {
return owncloudApiHelper.getLoggedInUser()?.let {
Account(it.displayName, AccountType.Owncloud)
}
}
}

View File

@ -0,0 +1,8 @@
package de.mm20.launcher2.accounts
import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module
val accountsModule = module {
factory<AccountsRepository> { AccountsRepositoryImpl(androidContext()) }
}

View File

@ -114,6 +114,7 @@ dependencies {
implementation(libs.koin.android)
implementation(project(":accounts"))
implementation(project(":applications"))
implementation(project(":badges"))
implementation(project(":base"))

View File

@ -2,6 +2,7 @@ package de.mm20.launcher2
import android.app.Application
import androidx.appcompat.app.AppCompatDelegate
import de.mm20.launcher2.accounts.accountsModule
import de.mm20.launcher2.applications.applicationsModule
import de.mm20.launcher2.badges.badgesModule
import de.mm20.launcher2.calculator.calculatorModule
@ -58,6 +59,7 @@ class LauncherApplication : Application(), CoroutineScope {
androidContext(this@LauncherApplication)
modules(
listOf(
accountsModule,
applicationsModule,
calculatorModule,
badgesModule,

View File

@ -8,7 +8,7 @@ buildscript {
dependencies {
classpath("com.android.tools.build:gradle:7.1.0-alpha03")
classpath(libs.kotlin.gradle)
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.30")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10")
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}

View File

@ -13,6 +13,7 @@ import com.microsoft.identity.client.AuthenticationCallback
import com.microsoft.identity.client.IAuthenticationResult
import com.microsoft.identity.client.ISingleAccountPublicClientApplication
import com.microsoft.identity.client.PublicClientApplication
import com.microsoft.identity.client.exception.MsalClientException
import com.microsoft.identity.client.exception.MsalException
import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.preferences.LauncherPreferences
@ -76,7 +77,6 @@ class MicrosoftGraphApiHelper(val context: Context) {
clientApplication.signIn(context, "", SCOPES, object : AuthenticationCallback {
override fun onSuccess(authenticationResult: IAuthenticationResult?) {
accessToken = authenticationResult?.accessToken
LauncherPreferences.instance.searchOneDrive = true
it.resume(authenticationResult)
}
@ -96,11 +96,16 @@ class MicrosoftGraphApiHelper(val context: Context) {
suspend fun logout() {
accessToken = null
LauncherPreferences.instance.searchOneDrive = false
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit {
putString(PREF_ACCOUNT_NAME, null)
}
withContext(Dispatchers.IO) { getClientApplication()?.signOut() }
withContext(Dispatchers.IO) {
try {
getClientApplication()?.signOut()
} catch (e: MsalClientException) {
CrashReporter.logException(e)
}
}
}
suspend fun getUser(): MsUser? {

View File

@ -394,3 +394,4 @@ dependencyResolutionManagement {
}
}
include(":notifications")
include(":accounts")

View File

@ -132,5 +132,6 @@ dependencies {
implementation(project(":g-services"))
implementation(project(":ms-services"))
implementation(project(":owncloud"))
implementation(project(":accounts"))
}

View File

@ -31,6 +31,7 @@ import de.mm20.launcher2.ui.settings.license.LicenseScreen
import de.mm20.launcher2.ui.settings.main.MainSettingsScreen
import de.mm20.launcher2.ui.settings.musicwidget.MusicWidgetSettingsScreen
import de.mm20.launcher2.ui.settings.search.SearchSettingsScreen
import de.mm20.launcher2.ui.settings.accounts.AccountsSettingsScreen
import de.mm20.launcher2.ui.settings.weatherwidget.WeatherWidgetSettingsScreen
import de.mm20.launcher2.ui.settings.widgets.WidgetsSettingsScreen
import de.mm20.launcher2.ui.settings.wikipedia.WikipediaSettingsScreen
@ -113,6 +114,9 @@ class SettingsActivity : BaseActivity() {
composable("settings/badges") {
BadgeSettingsScreen()
}
composable("settings/accounts") {
AccountsSettingsScreen()
}
composable("settings/about") {
AboutSettingsScreen()
}

View File

@ -0,0 +1,143 @@
package de.mm20.launcher2.ui.settings.accounts
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.LinearProgressIndicator
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.accounts.AccountType
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.preferences.Preference
import de.mm20.launcher2.ui.component.preferences.PreferenceCategory
import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
@Composable
fun AccountsSettingsScreen() {
val viewModel: AccountsSettingsScreenVM = viewModel()
val context = LocalContext.current
val lifecycleOwner = LocalLifecycleOwner.current
LaunchedEffect(null) {
lifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) {
viewModel.onResume()
}
}
val loading by viewModel.loading.observeAsState(true)
PreferenceScreen(title = stringResource(R.string.preference_screen_services)) {
if (loading) {
item {
LinearProgressIndicator(
modifier = Modifier.fillMaxWidth()
)
}
return@PreferenceScreen
}
item {
PreferenceCategory(title = stringResource(R.string.preference_category_services_nextcloud)) {
val account by viewModel.nextcloudUser.observeAsState()
Preference(
title = if (account != null) {
stringResource(R.string.preference_signin_logout)
} else {
stringResource(R.string.preference_nextcloud_signin)
},
summary = account?.let {
stringResource(R.string.preference_signin_user, it.userName)
} ?: stringResource(R.string.preference_nextcloud_signin_summary),
onClick = {
if (account != null) {
viewModel.signOut(AccountType.Nextcloud)
} else {
viewModel.signIn(context as AppCompatActivity, AccountType.Nextcloud)
}
}
)
}
}
item {
PreferenceCategory(title = stringResource(R.string.preference_category_services_owncloud)) {
val account by viewModel.owncloudUser.observeAsState()
Preference(
title = if (account != null) {
stringResource(R.string.preference_signin_logout)
} else {
stringResource(R.string.preference_owncloud_signin)
},
summary = account?.let {
stringResource(R.string.preference_signin_user, it.userName)
} ?: stringResource(R.string.preference_owncloud_signin_summary),
onClick = {
if (account != null) {
viewModel.signOut(AccountType.Owncloud)
} else {
viewModel.signIn(context as AppCompatActivity, AccountType.Owncloud)
}
}
)
}
}
if (viewModel.isMicrosoftAvailable) {
item {
PreferenceCategory(title = stringResource(R.string.preference_category_services_microsoft)) {
val account by viewModel.msUser.observeAsState()
Preference(
title = if (account != null) {
stringResource(R.string.preference_signin_logout)
} else {
stringResource(R.string.preference_ms_signin)
},
summary = account?.let {
stringResource(R.string.preference_signin_user, it.userName)
} ?: stringResource(R.string.preference_ms_signin_summary),
onClick = {
if (account != null) {
viewModel.signOut(AccountType.Microsoft)
} else {
viewModel.signIn(context as AppCompatActivity, AccountType.Microsoft)
}
}
)
}
}
}
if (viewModel.isGoogleAvailable) {
item {
PreferenceCategory(title = stringResource(R.string.preference_category_services_google)) {
val account by viewModel.googleUser.observeAsState()
Preference(
title = if (account != null) {
stringResource(R.string.preference_signin_logout)
} else {
stringResource(R.string.preference_google_signin)
},
summary = account?.let {
stringResource(R.string.preference_signin_user, it.userName)
} ?: stringResource(R.string.preference_google_signin_summary),
onClick = {
if (account != null) {
viewModel.signOut(AccountType.Google)
} else {
viewModel.signIn(context as AppCompatActivity, AccountType.Google)
}
}
)
}
}
}
}
}

View File

@ -0,0 +1,52 @@
package de.mm20.launcher2.ui.settings.accounts
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.accounts.Account
import de.mm20.launcher2.accounts.AccountType
import de.mm20.launcher2.accounts.AccountsRepository
import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
class AccountsSettingsScreenVM : ViewModel(), KoinComponent {
private val accountsRepository: AccountsRepository by inject()
val isGoogleAvailable = accountsRepository.isSupported(AccountType.Google)
val isMicrosoftAvailable = accountsRepository.isSupported(AccountType.Microsoft)
val googleUser = MutableLiveData<Account?>(null)
val msUser= MutableLiveData<Account?>(null)
val nextcloudUser = MutableLiveData<Account?>(null)
val owncloudUser = MutableLiveData<Account?>(null)
val loading = MutableLiveData(true)
fun onResume() {
viewModelScope.launch {
loading.value = true
googleUser.value = accountsRepository.getCurrentlySignedInAccount(AccountType.Google)
nextcloudUser.value = accountsRepository.getCurrentlySignedInAccount(AccountType.Nextcloud)
msUser.value = accountsRepository.getCurrentlySignedInAccount(AccountType.Microsoft)
owncloudUser.value = accountsRepository.getCurrentlySignedInAccount(AccountType.Owncloud)
loading.value = false
}
}
fun signIn(activity: AppCompatActivity, accountType: AccountType) {
accountsRepository.signin(activity, accountType)
}
fun signOut(accountType: AccountType) {
accountsRepository.signout(accountType)
when(accountType){
AccountType.Google -> googleUser.value = null
AccountType.Microsoft -> msUser.value = null
AccountType.Nextcloud -> nextcloudUser.value = null
AccountType.Owncloud -> owncloudUser.value = null
}
}
}

View File

@ -1,4 +0,0 @@
package de.mm20.launcher2.ui.settings.services
class ServicesSettingsScreenVM {
}