Remove Google integration

This commit is contained in:
MM20 2025-02-09 12:54:20 +01:00
parent 3478411d7e
commit 45a19ce95e
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
36 changed files with 2 additions and 1047 deletions

View File

@ -143,7 +143,6 @@ dependencies {
implementation(project(":data:plugins"))
implementation(project(":data:themes"))
implementation(project(":data:files"))
implementation(project(":libs:g-services"))
implementation(project(":core:i18n"))
implementation(project(":services:icons"))
implementation(project(":core:ktx"))

View File

@ -146,7 +146,6 @@ dependencies {
implementation(project(":data:websites"))
implementation(project(":data:unitconverter"))
implementation(project(":libs:nextcloud"))
implementation(project(":libs:g-services"))
implementation(project(":libs:owncloud"))
implementation(project(":services:accounts"))
implementation(project(":services:plugins"))

View File

@ -48,7 +48,6 @@ import de.mm20.launcher2.ui.settings.favorites.FavoritesSettingsScreen
import de.mm20.launcher2.ui.settings.filesearch.FileSearchSettingsScreen
import de.mm20.launcher2.ui.settings.filterbar.FilterBarSettingsScreen
import de.mm20.launcher2.ui.settings.gestures.GestureSettingsScreen
import de.mm20.launcher2.ui.settings.google.GoogleSettingsScreen
import de.mm20.launcher2.ui.settings.hiddenitems.HiddenItemsSettingsScreen
import de.mm20.launcher2.ui.settings.homescreen.HomescreenSettingsScreen
import de.mm20.launcher2.ui.settings.icons.IconsSettingsScreen
@ -226,9 +225,6 @@ class SettingsActivity : BaseActivity() {
composable("settings/integrations/owncloud") {
OwncloudSettingsScreen()
}
composable("settings/integrations/google") {
GoogleSettingsScreen()
}
composable("settings/plugins") {
PluginsSettingsScreen()
}

View File

@ -1,22 +1,18 @@
package de.mm20.launcher2.ui.settings.buildinfo
import androidx.lifecycle.ViewModel
import de.mm20.launcher2.accounts.AccountType
import de.mm20.launcher2.accounts.AccountsRepository
import de.mm20.launcher2.weather.WeatherRepository
import kotlinx.coroutines.flow.map
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
class BuildInfoSettingsScreenVM : ViewModel(), KoinComponent {
private val accountsRepository: AccountsRepository by inject()
private val weatherRepository: WeatherRepository by inject()
private val availableWeatherProviders = weatherRepository.getProviders()
val buildFeatures = availableWeatherProviders.map {
mapOf(
"Accounts: Google" to accountsRepository.isSupported(AccountType.Google),
"Weather providers: HERE" to it.any { it.id == "here" },
"Weather providers: Met No" to it.any { it.id == "metno" },
"Weather providers: OpenWeatherMap" to it.any { it.id == "owm" },

View File

@ -153,40 +153,6 @@ fun FileSearchSettingsScreen() {
enabled = owncloudAccount != null
)
if (viewModel.googleAvailable) {
val gdrive by viewModel.gdrive.collectAsState()
val googleAccount by viewModel.googleAccount
AnimatedVisibility(googleAccount == null) {
Banner(
text = stringResource(R.string.no_account_google),
icon = Icons.Rounded.AccountBox,
primaryAction = {
TextButton(onClick = {
viewModel.login(
context as AppCompatActivity,
AccountType.Google
)
}) {
Text(
stringResource(R.string.connect_account),
)
}
},
modifier = Modifier.padding(16.dp)
)
}
SwitchPreference(
title = stringResource(R.string.preference_search_gdrive),
summary = googleAccount?.let {
stringResource(R.string.preference_search_gdrive_summary, it.userName)
} ?: stringResource(R.string.preference_summary_not_logged_in),
value = gdrive == true && googleAccount != null,
onValueChanged = {
viewModel.setGdrive(it)
},
enabled = googleAccount != null
)
}
for (plugin in plugins) {
val state = plugin.state
if (state is PluginState.SetupRequired) {

View File

@ -30,9 +30,6 @@ class FileSearchSettingsScreenVM : ViewModel(), KoinComponent {
val loading = mutableStateOf(true)
val nextcloudAccount = mutableStateOf<Account?>(null)
val owncloudAccount = mutableStateOf<Account?>(null)
val googleAccount = mutableStateOf<Account?>(null)
val googleAvailable = accountsRepository.isSupported(AccountType.Google)
val availablePlugins = pluginService.getPluginsWithState(
type = PluginType.FileSearch,
@ -48,7 +45,6 @@ class FileSearchSettingsScreenVM : ViewModel(), KoinComponent {
accountsRepository.getCurrentlySignedInAccount(AccountType.Nextcloud)
owncloudAccount.value =
accountsRepository.getCurrentlySignedInAccount(AccountType.Owncloud)
googleAccount.value = accountsRepository.getCurrentlySignedInAccount(AccountType.Google)
loading.value = false
}
}

View File

@ -1,157 +0,0 @@
package de.mm20.launcher2.ui.settings.google
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.Logout
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.repeatOnLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
import de.mm20.launcher2.ui.component.preferences.SwitchPreference
import de.mm20.launcher2.ui.locals.LocalNavController
import de.mm20.launcher2.ui.settings.integrations.GoogleSigninButton
@Composable
fun GoogleSettingsScreen() {
val viewModel: GoogleSettingsScreenVM = viewModel()
val lifecycleOwner = LocalLifecycleOwner.current
val navController = LocalNavController.current
val googleUser by viewModel.googleUser
val loading by viewModel.loading
val searchFiles by viewModel.searchFiles.collectAsState(null)
LaunchedEffect(null) {
lifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) {
viewModel.onResume()
}
}
PreferenceScreen(title = stringResource(R.string.preference_google)) {
if (loading) return@PreferenceScreen
if (googleUser != null) {
item {
Column(
modifier = Modifier
.background(MaterialTheme.colorScheme.secondaryContainer)
.fillParentMaxWidth()
.padding(vertical = 64.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.size(72.dp)
.background(MaterialTheme.colorScheme.secondary, CircleShape)
.border(
2.dp,
MaterialTheme.colorScheme.onSecondaryContainer,
CircleShape
),
) {
Text(
text = googleUser!!.userName.split(" ")
.map { it.first() }
.joinToString("").let {
if (it.length >= 2) it.first().toString() + it.last().toString()
else it.first().toString()
},
color = MaterialTheme.colorScheme.onSecondary,
style = MaterialTheme.typography.headlineMedium,
)
}
Text(
modifier = Modifier.padding(top = 24.dp),
text = stringResource(
R.string.preference_signin_user,
googleUser!!.userName
),
color = MaterialTheme.colorScheme.onSecondaryContainer,
style = MaterialTheme.typography.bodyLarge,
)
Button(
modifier = Modifier.padding(top = 32.dp),
onClick = {
viewModel.signOut()
},
contentPadding = ButtonDefaults.ButtonWithIconContentPadding,
) {
Icon(
Icons.AutoMirrored.Rounded.Logout,
modifier = Modifier
.padding(end = ButtonDefaults.IconSpacing)
.size(ButtonDefaults.IconSize),
contentDescription = null
)
Text(text = stringResource(R.string.preference_signout))
}
}
}
item {
HorizontalDivider()
}
item {
SwitchPreference(
title = stringResource(R.string.plugin_type_filesearch),
summary = stringResource(
R.string.preference_search_cloud_summary,
googleUser!!.userName
),
value = searchFiles == true,
onValueChanged = {
viewModel.setSearchFiles(it)
},
iconPadding = false,
)
}
} else {
item {
val activity = LocalContext.current as AppCompatActivity
Column(
modifier = Modifier
.fillParentMaxWidth()
.padding(vertical = 64.dp, horizontal = 24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
GoogleSigninButton(
onClick = {
viewModel.signIn(activity)
}
)
}
}
item {
HorizontalDivider()
}
}
}
}

View File

@ -1,44 +0,0 @@
package de.mm20.launcher2.ui.settings.google
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.runtime.mutableStateOf
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 de.mm20.launcher2.preferences.search.FileSearchSettings
import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
class GoogleSettingsScreenVM: ViewModel(), KoinComponent {
private val accountsRepository: AccountsRepository by inject()
private val fileSearchSettings: FileSearchSettings by inject()
val googleUser = mutableStateOf<Account?>(null)
val loading = mutableStateOf(true)
fun onResume() {
viewModelScope.launch {
loading.value = true
googleUser.value = accountsRepository.getCurrentlySignedInAccount(AccountType.Google)
loading.value = false
}
}
fun signIn(activity: AppCompatActivity) {
accountsRepository.signin(activity, AccountType.Google)
}
fun signOut() {
accountsRepository.signout(AccountType.Google)
googleUser.value = null
}
val searchFiles = fileSearchSettings.gdriveFiles
fun setSearchFiles(value: Boolean) {
fileSearchSettings.setGdriveFiles(value)
}
}

View File

@ -1,45 +1,16 @@
package de.mm20.launcher2.ui.settings.integrations
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.LightMode
import androidx.compose.material.icons.rounded.PlayCircleOutline
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.accounts.AccountType
import de.mm20.launcher2.icons.Google
import de.mm20.launcher2.icons.Nextcloud
import de.mm20.launcher2.icons.Owncloud
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
import de.mm20.launcher2.ui.locals.LocalNavController
@ -78,53 +49,6 @@ fun IntegrationsSettingsScreen() {
navController?.navigate("settings/integrations/owncloud")
}
)
if (viewModel.isGoogleAvailable) {
Preference(
title = stringResource(R.string.preference_google),
icon = Icons.Rounded.Google,
onClick = {
navController?.navigate("settings/integrations/google")
}
)
}
}
}
}
@Composable
fun GoogleSigninButton(
onClick: () -> Unit,
) {
Surface(
modifier = Modifier.height(40.dp),
shadowElevation = 1.dp,
color = Color.White,
shape = RoundedCornerShape(2.dp),
onClick = onClick
) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Box(
modifier = Modifier
.size(40.dp)
.background(Color.White),
contentAlignment = Alignment.Center
) {
Image(
modifier = Modifier.size(18.dp),
painter = painterResource(id = R.drawable.ic_google_g),
contentDescription = null
)
}
Text(
modifier = Modifier.padding(start = 13.dp, end = 8.dp),
text = stringResource(id = R.string.preference_google_signin),
fontFamily = FontFamily.SansSerif,
color = Color(0f, 0f, 0f, 0.54f),
fontSize = 14.sp,
fontWeight = FontWeight.Medium
)
}
}
}

View File

@ -7,7 +7,4 @@ import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
class IntegrationsSettingsScreenVM : ViewModel(), KoinComponent {
private val accountsRepository: AccountsRepository by inject()
val isGoogleAvailable = accountsRepository.isSupported(AccountType.Google)
}

View File

@ -107,21 +107,6 @@ val OpenSourceLicenses = arrayOf(
copyrightNote = "Copyright 2010 - 2020 Mariusz Gromada. All rights reserved.",
url = "https://mathparser.org/"
),
OpenSourceLibrary(
name = "Google Auth Library",
description = "Open source authentication client library for Java.",
licenseName = R.string.bsd_3clause_name,
licenseText = R.raw.license_bsd_3clause,
copyrightNote = "Copyright 2014, Google Inc. All rights reserved.",
url = "https://github.com/googleapis/google-auth-library-java"
),
OpenSourceLibrary(
name = "Google APIs Client Library for Android",
description = "The Google APIs Client Library for Java is a flexible, efficient, and powerful Java client library for accessing any HTTP-based API on the web, not just Google APIs.",
licenseName = R.string.apache_license_name,
licenseText = R.raw.license_apache_2,
url = "https://github.com/googleapis/google-api-java-client"
),
OpenSourceLibrary(
name = "CrashReporter",
description = "CrashReporter is a handy tool to capture app crashes and save them in a file.",

View File

@ -49,7 +49,6 @@ dependencies {
implementation(project(":core:base"))
implementation(project(":core:ktx"))
implementation(project(":libs:g-services"))
implementation(project(":libs:nextcloud"))
implementation(project(":libs:owncloud"))
implementation(project(":core:i18n"))

View File

@ -4,7 +4,6 @@ import android.content.Context
import android.net.Uri
import android.provider.MediaStore
import androidx.core.database.getStringOrNull
import de.mm20.launcher2.files.providers.GDriveFile
import de.mm20.launcher2.files.providers.LocalFile
import de.mm20.launcher2.files.providers.NextcloudFile
import de.mm20.launcher2.files.providers.OwncloudFile
@ -107,66 +106,6 @@ internal class LocalFileDeserializer(
}
}
internal class GDriveFileSerializer : SearchableSerializer {
override fun serialize(searchable: SavableSearchable): String {
searchable as GDriveFile
return jsonObjectOf(
"id" to searchable.fileId,
"label" to searchable.label,
"path" to searchable.path,
"mimeType" to searchable.mimeType,
"size" to searchable.size,
"directory" to searchable.isDirectory,
"color" to searchable.directoryColor,
"uri" to searchable.viewUri
).apply {
for ((k, v) in searchable.metaData) {
put(
when (k) {
FileMetaType.Owner -> "owner"
FileMetaType.Dimensions -> "dimensions"
else -> "other"
}, v
)
}
}.toString()
}
override val typePrefix: String
get() = "gdrive"
}
internal class GDriveFileDeserializer : SearchableDeserializer {
override suspend fun deserialize(serialized: String): SavableSearchable {
val json = JSONObject(serialized)
val id = json.getString("id")
val label = json.getString("label")
val path = json.getString("path")
val mimeType = json.getString("mimeType")
val size = json.getLong("size")
val directory = json.getBoolean("directory")
val color = json.optString("color")
val uri = json.getString("uri")
val owner = json.optString("owner")
val dimensions = json.optString("dimensions")
val metaData = mutableMapOf<FileMetaType, String>()
owner.takeIf { it.isNotEmpty() }?.let { metaData[FileMetaType.Owner] = it }
dimensions.takeIf { it.isNotEmpty() }
?.let { metaData[FileMetaType.Dimensions] = it }
return GDriveFile(
fileId = id,
label = label,
path = path,
mimeType = mimeType,
size = size,
directoryColor = color,
isDirectory = directory,
viewUri = uri,
metaData = metaData.toImmutableMap()
)
}
}
internal class NextcloudFileSerializer : SearchableSerializer {
override fun serialize(searchable: SavableSearchable): String {
searchable as NextcloudFile

View File

@ -1,7 +1,6 @@
package de.mm20.launcher2.files
import android.content.Context
import de.mm20.launcher2.files.providers.GDriveFileProvider
import de.mm20.launcher2.files.providers.LocalFileProvider
import de.mm20.launcher2.files.providers.NextcloudFileProvider
import de.mm20.launcher2.files.providers.OwncloudFileProvider
@ -64,7 +63,6 @@ internal class FileRepository(
permissionsManager
) else null
"gdrive" -> GDriveFileProvider(context)
"nextcloud" -> NextcloudFileProvider(nextcloudClient)
"owncloud" -> OwncloudFileProvider(owncloudClient)
else -> PluginFileProvider(context, it)

View File

@ -1,6 +1,5 @@
package de.mm20.launcher2.files
import de.mm20.launcher2.files.providers.GDriveFile
import de.mm20.launcher2.files.providers.LocalFile
import de.mm20.launcher2.files.providers.NextcloudFile
import de.mm20.launcher2.files.providers.OwncloudFile
@ -23,7 +22,6 @@ val filesModule = module {
factory<SearchableDeserializer>(named(LocalFile.Domain)) { LocalFileDeserializer(androidContext()) }
factory<SearchableDeserializer>(named(OwncloudFile.Domain)) { OwncloudFileDeserializer() }
factory<SearchableDeserializer>(named(NextcloudFile.Domain)) { NextcloudFileDeserializer() }
factory<SearchableDeserializer>(named(GDriveFile.Domain)) { GDriveFileDeserializer() }
factory<SearchableDeserializer>(named(PluginFile.Domain)) {
PluginFileDeserializer(
androidContext(),

View File

@ -1,56 +0,0 @@
package de.mm20.launcher2.files.providers
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import de.mm20.launcher2.files.GDriveFileSerializer
import de.mm20.launcher2.files.R
import de.mm20.launcher2.ktx.tryStartActivity
import de.mm20.launcher2.search.File
import de.mm20.launcher2.search.FileMetaType
import de.mm20.launcher2.search.SearchableSerializer
import kotlinx.collections.immutable.ImmutableMap
internal data class GDriveFile(
val fileId: String,
override val label: String,
override val path: String,
override val mimeType: String,
override val size: Long,
override val isDirectory: Boolean,
override val metaData: ImmutableMap<FileMetaType, String>,
val directoryColor: String?,
val viewUri: String,
override val labelOverride: String? = null,
) : File {
override fun overrideLabel(label: String): GDriveFile {
return this.copy(labelOverride = label)
}
override val domain: String = Domain
override val key: String = "$domain://$fileId"
override val providerIconRes = R.drawable.ic_badge_gdrive
private fun getLaunchIntent(): Intent {
return Intent(Intent.ACTION_VIEW).apply {
data = Uri.parse(viewUri)
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
}
override fun launch(context: Context, options: Bundle?): Boolean {
return context.tryStartActivity(getLaunchIntent(), options)
}
override fun getSerializer(): SearchableSerializer {
return GDriveFileSerializer()
}
companion object {
const val Domain = "gdrive"
}
}

View File

@ -1,42 +0,0 @@
package de.mm20.launcher2.files.providers
import android.content.Context
import de.mm20.launcher2.files.R
import de.mm20.launcher2.gservices.DriveFileMeta
import de.mm20.launcher2.gservices.GoogleApiHelper
import de.mm20.launcher2.search.File
import de.mm20.launcher2.search.FileMetaType
import kotlinx.collections.immutable.ImmutableMap
import kotlinx.collections.immutable.toImmutableMap
internal class GDriveFileProvider(
private val context: Context
) : FileProvider {
override suspend fun search(query: String, allowNetwork: Boolean): List<File> {
if (query.length < 4 || !allowNetwork) return emptyList()
val driveFiles = GoogleApiHelper.getInstance(context).queryGDriveFiles(query)
return driveFiles.map {
GDriveFile(
fileId = it.fileId,
label = it.label,
size = it.size,
mimeType = it.mimeType,
isDirectory = it.isDirectory,
path = "",
directoryColor = it.directoryColor,
viewUri = it.viewUri,
metaData = getMetadata(it.metadata)
)
}
}
private fun getMetadata(file: DriveFileMeta): ImmutableMap<FileMetaType, String> {
val metaData = mutableMapOf<FileMetaType, String>()
val owners = file.owners
metaData[FileMetaType.Owner] = owners.joinToString(separator = ", ")
val width = file.width
val height = file.height
if (width != null && height != null) metaData[FileMetaType.Dimensions] = "${width}x${height}"
return metaData.toImmutableMap()
}
}

View File

@ -1,24 +0,0 @@
# Google Cloud Services
Google Cloud Services are used for Google Drive search. To enable Google Drive integration in your builds, follow these steps:
1. Go to the [Google Cloud Console](https://console.cloud.google.com)
1. Create a new project.
1. Enable the Drive API:
1. Go to APIs & Services > Library and search for the Google Drive API.
1. Enable this API for your project.
1. Setup your OAuth consent screen
1. Go to APIs & Services > OAuth consent screen
1. On the Oauth consent screen page, fill out all the required fields
1. On the Scopes page, click add or remove scopes and add the following scopes: `userinfo.profile` and `drive.metadata.readonly`
1. Create a new Oauth 2.0 client (you need to do this twice, for debug builds and for release builds)
1. Go to APIs & Services > Credentials
1. Click on Create Credentials > OAuth client ID
1. Choose application type Android
1. Enter the package name (`de.mm20.launcher2.debug` for debug builds or `de.mm20.launcher2.release` for release builds)
1. Enter the SHA-1 certificate fingerprint of your APK signing key
1. Click on create
1. Download the client config file (repeat this step for both the debug and the release client)
1. On the APIs & Services > Credentials page, find your OAuth client in the list under OAuth 2.0 Client IDs.
1. Click on the download icon to download a `client_config.json`
1. Place this file under `g-services/src/debug/res/raw/g_services.json` or `g-services/src/release/res/raw/g_services.json`

View File

@ -127,11 +127,6 @@ stringsimilarity = { group = "com.aallam.similarity", name = "string-similarity-
# 4.4.2 is the last GPL compatible version, don't update to 5.x
mathparser = { group = "org.mariuszgromada.math", name = "MathParser.org-mXparser", version = "4.4.2" }
google-auth = { group = "com.google.auth", name = "google-auth-library-oauth2-http", version = "1.14.0" }
google-apiclient = { group = "com.google.api-client", name = "google-api-client", version = "2.1.2" }
google-drive = { group = "com.google.apis", name = "google-api-services-drive", version = "v3-rev20221219-2.0.0" }
google-oauth2 = { group = "com.google.apis", name = "google-api-services-oauth2", version = "v2-rev20200213-2.0.0" }
koin-android = { group = "io.insert-koin", name = "koin-android", version.ref = "koin" }
koin-androidxcompose = { group = "io.insert-koin", name = "koin-androidx-compose", version.ref = "koin" }

View File

@ -1,6 +1,6 @@
#Mon Dec 12 19:53:45 CET 2022
#Sun Feb 09 12:41:42 CET 2025
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@ -1,2 +0,0 @@
/build
*/**/g_services.json

View File

@ -1,51 +0,0 @@
plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.kotlin.android)
}
android {
compileSdk = libs.versions.compileSdk.get().toInt()
defaultConfig {
minSdk = libs.versions.minSdk.get().toInt()
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
}
buildTypes {
release {
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
consumerProguardFiles("proguard-rules.pro")
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
namespace = "de.mm20.launcher2.gservices"
}
dependencies {
implementation(libs.bundles.kotlin)
implementation(libs.androidx.core)
implementation(libs.androidx.appcompat)
implementation(libs.androidx.browser)
implementation(libs.bundles.androidx.lifecycle)
implementation(libs.google.auth)
implementation(libs.google.apiclient)
implementation(libs.google.drive)
implementation(libs.google.oauth2)
implementation(project(":core:i18n"))
implementation(project(":core:crashreporter"))
}

View File

@ -1,29 +0,0 @@
-keep class org.apache.** { *; }
-dontwarn edu.umd.cs.findbugs.annotations.Nullable
-dontwarn java.lang.reflect.AnnotatedType
-dontwarn javax.lang.model.element.Modifier
-dontwarn javax.script.ScriptEngine
-dontwarn javax.script.ScriptEngineManager
-dontwarn org.apache.http.client.config.RequestConfig$Builder
-dontwarn org.apache.http.client.config.RequestConfig
-dontwarn org.apache.http.client.methods.HttpPatch
-dontwarn org.apache.http.conn.DnsResolver
-dontwarn org.apache.http.conn.HttpClientConnectionManager
-dontwarn org.apache.http.conn.HttpConnectionFactory
-dontwarn org.apache.http.conn.SchemePortResolver
-dontwarn org.apache.http.conn.socket.LayeredConnectionSocketFactory
-dontwarn org.apache.http.conn.socket.PlainConnectionSocketFactory
-dontwarn org.apache.http.conn.ssl.SSLConnectionSocketFactory
-dontwarn org.apache.http.impl.client.CloseableHttpClient
-dontwarn org.apache.http.impl.client.HttpClientBuilder
-dontwarn org.apache.http.impl.conn.PoolingHttpClientConnectionManager
-dontwarn org.apache.http.impl.conn.SystemDefaultRoutePlanner
-dontwarn org.bouncycastle.jsse.BCSSLParameters
-dontwarn org.bouncycastle.jsse.BCSSLSocket
-dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider
-dontwarn org.conscrypt.Conscrypt$Version
-dontwarn org.conscrypt.Conscrypt
-dontwarn org.conscrypt.ConscryptHostnameVerifier
-dontwarn org.openjsse.javax.net.ssl.SSLParameters
-dontwarn org.openjsse.javax.net.ssl.SSLSocket
-dontwarn org.openjsse.net.ssl.OpenJSSE

View File

@ -1,23 +0,0 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.kts.
#
# 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
-keep class com.google.** { *; }

View File

@ -1,20 +0,0 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name="de.mm20.launcher2.gservices.GoogleAuthRedirectActivity"
android:theme="@style/GoogleSigninTheme"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:path="/google-auth-redirect"
android:scheme="${applicationId}" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@ -1,40 +0,0 @@
package de.mm20.launcher2.gservices
import com.google.api.services.drive.model.File
import java.util.*
data class DriveFile(
val fileId : String,
val label: String,
val size: Long,
val mimeType : String,
val isDirectory : Boolean,
val directoryColor: String?,
val viewUri: String,
val metadata: DriveFileMeta
) {
companion object {
fun fromApiDriveFile(file: File): DriveFile {
return DriveFile(
fileId = file.id,
label = file.name,
size = file.getSize() ?: 0,
isDirectory = file.mimeType == "application/vnd.google-apps.folder",
mimeType = file.mimeType,
metadata = DriveFileMeta(
owners = file.owners?.map { it.displayName ?: it.emailAddress ?: "" } ?: emptyList(),
width = file.imageMediaMetadata?.width ?: file.videoMediaMetadata?.width,
height = file.imageMediaMetadata?.height ?: file.videoMediaMetadata?.height
),
directoryColor = file.folderColorRgb?.lowercase(Locale.ROOT),
viewUri = file.webViewLink ?: ""
)
}
}
}
data class DriveFileMeta(
val owners : List<String>,
val width: Int?,
val height: Int?
)

View File

@ -1,5 +0,0 @@
package de.mm20.launcher2.gservices
data class GoogleAccount(
val name: String
)

View File

@ -1,251 +0,0 @@
package de.mm20.launcher2.gservices
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.util.Log
import androidx.browser.customtabs.*
import androidx.core.content.edit
import com.google.api.client.auth.oauth2.Credential
import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeFlow
import com.google.api.client.googleapis.auth.oauth2.GoogleClientSecrets
import com.google.api.client.http.HttpRequestInitializer
import com.google.api.client.http.javanet.NetHttpTransport
import com.google.api.client.json.gson.GsonFactory
import com.google.api.client.util.store.FileDataStoreFactory
import com.google.api.services.drive.Drive
import com.google.api.services.oauth2.Oauth2
import de.mm20.launcher2.crashreporter.CrashReporter
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
import java.io.File
import java.io.IOException
class GoogleApiHelper private constructor(private val context: Context) {
val transport by lazy {
NetHttpTransport()
}
suspend fun queryGDriveFiles(query: String): List<DriveFile> {
val requestInitializer = getRequestInitializer() ?: return emptyList()
val jsonFactory = GsonFactory.getDefaultInstance()
return withContext(Dispatchers.IO) {
try {
val drive =
Drive.Builder(transport, jsonFactory, requestInitializer).build()
val request = drive.files().list()
request.q = "name contains '${query.replace("'", "")}'"
request.pageSize = 10
request.fields =
"files(id, webViewLink, size, name, mimeType, owners, imageMediaMetadata, videoMediaMetadata, folderColorRgb)"
request.corpora = "user"
val response = request.execute()
val files = response.files ?: return@withContext emptyList()
files.map { DriveFile.fromApiDriveFile(it) }
} catch (e: IOException) {
emptyList()
} catch (e: Error) {
emptyList()
}
}
}
private suspend fun getCredential(): Credential? {
val authFlow = getAuthFlow() ?: return null
return withContext(Dispatchers.IO) {
val credential: Credential? = authFlow.loadCredential(USER_ID)
if ((credential?.expiresInSeconds ?: 0) < 5 * 60) {
try {
if (credential?.refreshToken() == false) return@withContext null
} catch (e: IOException) {
CrashReporter.logException(e)
}
}
return@withContext credential
}
}
private suspend fun getRequestInitializer(): HttpRequestInitializer? {
val credential = getCredential() ?: return null
return HttpRequestInitializer { request ->
credential.initialize(request)
request?.connectTimeout = 5000
request?.readTimeout = 10000
}
}
suspend fun getAccount(): GoogleAccount? {
val name = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).getString(
PREF_ACCOUNT_NAME,
null
) ?: loadAccountName()
return name?.let {
GoogleAccount(name = it)
}
}
fun isAvailable(): Boolean {
return getConfigResId() != 0
}
private fun getConfigResId(): Int {
return context.resources.getIdentifier("g_services", "raw", context.packageName)
}
private fun getAuthFlow(throwErrors: Boolean = false): GoogleAuthorizationCodeFlow? {
val configResId = getConfigResId()
if (configResId == 0) return null
val jsonFactory = GsonFactory.getDefaultInstance()
try {
return GoogleAuthorizationCodeFlow.Builder(
NetHttpTransport(),
jsonFactory,
GoogleClientSecrets.load(
jsonFactory,
context.resources.openRawResource(configResId).reader()
),
SCOPES
)
.setCredentialDataStore(
FileDataStoreFactory(context.filesDir).getDataStore(
"google_signin"
)
)
.build()
} catch (e: IOException) {
if (throwErrors) throw e
else {
File(context.filesDir, "google_signin").delete()
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit {
putString(PREF_ACCOUNT_NAME, null)
}
Log.e("MM20", "Google account has been reset because data store couldn't be readg")
CrashReporter.logException(e)
return getAuthFlow(true)
}
}
}
private var callback: (() -> Unit)? = null
suspend fun login(activity: Activity) {
val authFlow = getAuthFlow() ?: return
suspendCancellableCoroutine<Unit> {
val url = authFlow
.newAuthorizationUrl()
.setRedirectUri(getRedirectUri())
.toString()
val themeColor = 0xFF4285f4.toInt()
val customTabsIntent = CustomTabsIntent
.Builder()
.setDefaultColorSchemeParams(
CustomTabColorSchemeParams.Builder()
.setToolbarColor(themeColor)
.setNavigationBarColor(themeColor)
.build()
)
.build()
callingActivity = activity.javaClass
callback = {
it.resumeWith(Result.success(Unit))
}
it.invokeOnCancellation {
callback = null
Log.d("MM20", "Google Signin has been canceled")
}
customTabsIntent.intent.flags = Intent.FLAG_ACTIVITY_NO_HISTORY
customTabsIntent.launchUrl(activity, Uri.parse(url))
}
}
suspend fun finishAuthFlow(activity: Activity, code: String) {
val authFlow = getAuthFlow() ?: return
withContext(Dispatchers.IO) {
val tokenResponse = try {
authFlow.newTokenRequest(code).setRedirectUri(getRedirectUri()).execute()
} catch (e: IOException) {
CrashReporter.logException(e)
return@withContext
}
authFlow.createAndStoreCredential(tokenResponse, USER_ID)
}
loadAccountName()
returnToPreviousActivity(activity)
}
fun cancelAuthFlow(activity: Activity) {
returnToPreviousActivity(activity)
}
private fun returnToPreviousActivity(activity: Activity) {
val intent = Intent(activity, callingActivity)
callingActivity = null
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
activity.startActivity(intent)
callback?.invoke()
}
private suspend fun loadAccountName(): String? {
val requestInitializer = getRequestInitializer() ?: return null
val jsonFactory = GsonFactory.getDefaultInstance()
val oauth2 = Oauth2.Builder(transport, jsonFactory, requestInitializer).build()
try {
val meResponse = withContext(Dispatchers.IO) {
oauth2.userinfo().v2().me().get().execute()
}
if (meResponse != null) {
val name = meResponse.name
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit {
putString(PREF_ACCOUNT_NAME, name)
}
return name
}
} catch (e: IOException) {
CrashReporter.logException(e)
}
return null
}
fun logout() {
val authFlow = getAuthFlow() ?: return
authFlow.credentialDataStore.clear()
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit {
putString(PREF_ACCOUNT_NAME, null)
}
}
private fun getRedirectUri(): String {
return "${context.packageName}:/google-auth-redirect"
}
companion object {
private lateinit var instance: GoogleApiHelper
fun getInstance(context: Context): GoogleApiHelper {
if (!::instance.isInitialized) instance = GoogleApiHelper(context.applicationContext)
return instance
}
val SCOPES = setOf("https://www.googleapis.com/auth/drive.metadata.readonly", "profile")
const val USER_ID = "google-user"
const val PREFS = "google-account"
const val PREF_ACCOUNT_NAME = "name"
private var callingActivity: Class<Activity>? = null
}
}

View File

@ -1,24 +0,0 @@
package de.mm20.launcher2.gservices
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch
class GoogleAuthRedirectActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val gServiceHelper = GoogleApiHelper.getInstance(this)
val code = intent.data?.getQueryParameter("code")
if (code == null) {
gServiceHelper.cancelAuthFlow(this)
finish()
}
else {
lifecycleScope.launch {
gServiceHelper.finishAuthFlow(this@GoogleAuthRedirectActivity, code)
finish()
}
}
}
}

View File

@ -1,30 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="46dp"
android:height="46dp"
android:viewportWidth="46"
android:viewportHeight="46">
<path
android:pathData="m45.265,23.521c0,-1.624 -0.146,-3.186 -0.416,-4.685L23.278,18.836L23.278,27.695L35.604,27.695c-0.531,2.863 -2.145,5.288 -4.57,6.912v5.747h7.402c4.331,-3.987 6.829,-9.859 6.829,-16.834z"
android:strokeWidth="2.54476"
android:fillColor="#4285f4"
android:fillType="evenOdd"
android:strokeColor="#00000000"/>
<path
android:pathData="m23.278,45.903c6.184,0 11.368,-2.051 15.157,-5.549l-7.402,-5.747c-2.051,1.374 -4.674,2.186 -7.756,2.186 -5.965,0 -11.014,-4.029 -12.815,-9.442h-7.652v5.934C6.58,40.77 14.325,45.903 23.278,45.903Z"
android:strokeWidth="2.54476"
android:fillColor="#34a853"
android:fillType="evenOdd"
android:strokeColor="#00000000"/>
<path
android:pathData="M10.463,27.352C10.005,25.977 9.745,24.509 9.745,23c0,-1.51 0.26,-2.977 0.718,-4.352L10.463,12.715L2.811,12.715C1.26,15.806 0.375,19.304 0.375,23c0,3.696 0.885,7.194 2.436,10.285z"
android:strokeWidth="2.54476"
android:fillColor="#fbbc05"
android:fillType="evenOdd"
android:strokeColor="#00000000"/>
<path
android:pathData="m23.278,9.206c3.363,0 6.382,1.156 8.755,3.425l6.569,-6.569C34.636,2.367 29.451,0.097 23.278,0.097c-8.953,0 -16.698,5.132 -20.467,12.617l7.652,5.934c1.801,-5.413 6.85,-9.442 12.815,-9.442z"
android:strokeWidth="2.54476"
android:fillColor="#ea4335"
android:fillType="evenOdd"
android:strokeColor="#00000000"/>
</vector>

View File

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="GoogleSigninTheme" parent="@style/Theme.AppCompat.NoActionBar">
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:windowContentOverlay">@null</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowIsFloating">true</item>
</style>
</resources>

View File

@ -1 +0,0 @@
{"installed":{"client_id":"xxxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.apps.googleusercontent.com","project_id":"xxxxx-xxxxxxxxxxxxx","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://oauth2.googleapis.com/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","redirect_uris":["urn:ietf:wg:oauth:2.0:oob","http://localhost"]}}

View File

@ -39,7 +39,6 @@ dependencies {
implementation(libs.koin.android)
implementation(project(":libs:g-services"))
implementation(project(":libs:owncloud"))
implementation(project(":libs:nextcloud"))

View File

@ -1,7 +1,6 @@
package de.mm20.launcher2.accounts
enum class AccountType {
Google,
Nextcloud,
Owncloud,
}

View File

@ -2,7 +2,6 @@ package de.mm20.launcher2.accounts
import android.app.Activity
import android.content.Context
import de.mm20.launcher2.gservices.GoogleApiHelper
import de.mm20.launcher2.nextcloud.NextcloudApiHelper
import de.mm20.launcher2.owncloud.OwncloudClient
import kotlinx.coroutines.CoroutineScope
@ -27,17 +26,11 @@ internal class AccountsRepositoryImpl(
) : AccountsRepository {
private val scope = CoroutineScope(Job() + Dispatchers.Default)
private val googleApiHelper = GoogleApiHelper.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.Nextcloud ->
scope.launch {
nextcloudApiHelper.login(context)
@ -51,9 +44,6 @@ internal class AccountsRepositoryImpl(
override fun signout(accountType: AccountType) {
when (accountType) {
AccountType.Google -> {
googleApiHelper.logout()
}
AccountType.Nextcloud -> {
scope.launch {
nextcloudApiHelper.logout()
@ -67,7 +57,6 @@ internal class AccountsRepositoryImpl(
override fun isSupported(accountType: AccountType): Boolean {
return when (accountType) {
AccountType.Google -> googleApiHelper.isAvailable()
AccountType.Nextcloud -> true
AccountType.Owncloud -> true
}
@ -75,9 +64,6 @@ internal class AccountsRepositoryImpl(
override suspend fun getCurrentlySignedInAccount(accountType: AccountType): Account? {
return when (accountType) {
AccountType.Google -> {
getGoogleAccount()
}
AccountType.Nextcloud -> {
getNextcloudAccount()
}
@ -87,12 +73,6 @@ internal class AccountsRepositoryImpl(
}
}
private suspend fun getGoogleAccount(): Account? {
return googleApiHelper.getAccount()?.let {
Account(it.name, AccountType.Google)
}
}
private suspend fun getNextcloudAccount(): Account? {
return nextcloudApiHelper.getLoggedInUser()?.let {
Account(it.displayName, AccountType.Nextcloud)

View File

@ -57,7 +57,6 @@ include(":libs:material-color-utilities")
include(":libs:nextcloud")
include(":libs:owncloud")
include(":libs:webdav")
include(":libs:g-services")
include(":libs:address-formatter")
include(":services:global-actions")
include(":services:widgets")