diff --git a/accounts/.gitignore b/accounts/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/accounts/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/accounts/build.gradle.kts b/accounts/build.gradle.kts new file mode 100644 index 00000000..c0bca5ab --- /dev/null +++ b/accounts/build.gradle.kts @@ -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")) + +} \ No newline at end of file diff --git a/accounts/consumer-rules.pro b/accounts/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/accounts/proguard-rules.pro b/accounts/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/accounts/proguard-rules.pro @@ -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 \ No newline at end of file diff --git a/accounts/src/main/AndroidManifest.xml b/accounts/src/main/AndroidManifest.xml new file mode 100644 index 00000000..b69a17f8 --- /dev/null +++ b/accounts/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/accounts/src/main/java/de/mm20/launcher2/accounts/Account.kt b/accounts/src/main/java/de/mm20/launcher2/accounts/Account.kt new file mode 100644 index 00000000..89fe9412 --- /dev/null +++ b/accounts/src/main/java/de/mm20/launcher2/accounts/Account.kt @@ -0,0 +1,6 @@ +package de.mm20.launcher2.accounts + +data class Account( + val userName: String, + val type: AccountType, +) diff --git a/accounts/src/main/java/de/mm20/launcher2/accounts/AccountType.kt b/accounts/src/main/java/de/mm20/launcher2/accounts/AccountType.kt new file mode 100644 index 00000000..982c6b88 --- /dev/null +++ b/accounts/src/main/java/de/mm20/launcher2/accounts/AccountType.kt @@ -0,0 +1,8 @@ +package de.mm20.launcher2.accounts + +enum class AccountType { + Google, + Microsoft, + Nextcloud, + Owncloud, +} \ No newline at end of file diff --git a/accounts/src/main/java/de/mm20/launcher2/accounts/AccountsRepository.kt b/accounts/src/main/java/de/mm20/launcher2/accounts/AccountsRepository.kt new file mode 100644 index 00000000..4aa48d53 --- /dev/null +++ b/accounts/src/main/java/de/mm20/launcher2/accounts/AccountsRepository.kt @@ -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) + } + } + +} \ No newline at end of file diff --git a/accounts/src/main/java/de/mm20/launcher2/accounts/Module.kt b/accounts/src/main/java/de/mm20/launcher2/accounts/Module.kt new file mode 100644 index 00000000..777cb665 --- /dev/null +++ b/accounts/src/main/java/de/mm20/launcher2/accounts/Module.kt @@ -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 { AccountsRepositoryImpl(androidContext()) } +} \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 80c1bfcc..1db618bd 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -114,6 +114,7 @@ dependencies { implementation(libs.koin.android) + implementation(project(":accounts")) implementation(project(":applications")) implementation(project(":badges")) implementation(project(":base")) diff --git a/app/src/main/java/de/mm20/launcher2/LauncherApplication.kt b/app/src/main/java/de/mm20/launcher2/LauncherApplication.kt index 081abd5a..2701161b 100644 --- a/app/src/main/java/de/mm20/launcher2/LauncherApplication.kt +++ b/app/src/main/java/de/mm20/launcher2/LauncherApplication.kt @@ -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, diff --git a/build.gradle.kts b/build.gradle.kts index 513577fd..bdff23b7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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 } diff --git a/ms-services/src/main/java/de/mm20/launcher2/msservices/MicrosoftGraphApiHelper.kt b/ms-services/src/main/java/de/mm20/launcher2/msservices/MicrosoftGraphApiHelper.kt index d698a503..8866db77 100644 --- a/ms-services/src/main/java/de/mm20/launcher2/msservices/MicrosoftGraphApiHelper.kt +++ b/ms-services/src/main/java/de/mm20/launcher2/msservices/MicrosoftGraphApiHelper.kt @@ -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? { diff --git a/settings.gradle.kts b/settings.gradle.kts index 5049e108..8e0fc55e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -394,3 +394,4 @@ dependencyResolutionManagement { } } include(":notifications") +include(":accounts") diff --git a/ui/build.gradle.kts b/ui/build.gradle.kts index e9dfaf3e..5c24d149 100644 --- a/ui/build.gradle.kts +++ b/ui/build.gradle.kts @@ -132,5 +132,6 @@ dependencies { implementation(project(":g-services")) implementation(project(":ms-services")) implementation(project(":owncloud")) + implementation(project(":accounts")) } \ No newline at end of file diff --git a/ui/src/main/java/de/mm20/launcher2/ui/settings/SettingsActivity.kt b/ui/src/main/java/de/mm20/launcher2/ui/settings/SettingsActivity.kt index f164a3cb..58899df8 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/settings/SettingsActivity.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/settings/SettingsActivity.kt @@ -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() } diff --git a/ui/src/main/java/de/mm20/launcher2/ui/settings/accounts/AccountsSettingsScreen.kt b/ui/src/main/java/de/mm20/launcher2/ui/settings/accounts/AccountsSettingsScreen.kt new file mode 100644 index 00000000..3c573e81 --- /dev/null +++ b/ui/src/main/java/de/mm20/launcher2/ui/settings/accounts/AccountsSettingsScreen.kt @@ -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) + } + } + ) + } + } + } + } +} \ No newline at end of file diff --git a/ui/src/main/java/de/mm20/launcher2/ui/settings/accounts/AccountsSettingsScreenVM.kt b/ui/src/main/java/de/mm20/launcher2/ui/settings/accounts/AccountsSettingsScreenVM.kt new file mode 100644 index 00000000..24423059 --- /dev/null +++ b/ui/src/main/java/de/mm20/launcher2/ui/settings/accounts/AccountsSettingsScreenVM.kt @@ -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(null) + val msUser= MutableLiveData(null) + val nextcloudUser = MutableLiveData(null) + val owncloudUser = MutableLiveData(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 + } + } +} \ No newline at end of file diff --git a/ui/src/main/java/de/mm20/launcher2/ui/settings/services/ServicesSettingsScreenVM.kt b/ui/src/main/java/de/mm20/launcher2/ui/settings/services/ServicesSettingsScreenVM.kt deleted file mode 100644 index e23b30de..00000000 --- a/ui/src/main/java/de/mm20/launcher2/ui/settings/services/ServicesSettingsScreenVM.kt +++ /dev/null @@ -1,4 +0,0 @@ -package de.mm20.launcher2.ui.settings.services - -class ServicesSettingsScreenVM { -} \ No newline at end of file