diff --git a/app/build.gradle b/app/build.gradle index e5423d3e..616986c9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -20,7 +20,7 @@ android { defaultConfig { applicationId "com.hegocre.nextcloudpasswords" - minSdk 24 + minSdk 26 targetSdk 36 versionCode 38 versionName "1.0.12" diff --git a/app/src/androidTest/java/com/hegocre/nextcloudpasswords/ApiControllerTest.kt b/app/src/androidTest/java/com/hegocre/nextcloudpasswords/ApiControllerTest.kt index 310d5246..e8788598 100644 --- a/app/src/androidTest/java/com/hegocre/nextcloudpasswords/ApiControllerTest.kt +++ b/app/src/androidTest/java/com/hegocre/nextcloudpasswords/ApiControllerTest.kt @@ -3,6 +3,7 @@ package com.hegocre.nextcloudpasswords import android.content.Context import androidx.test.platform.app.InstrumentationRegistry import com.hegocre.nextcloudpasswords.api.ApiController +import com.hegocre.nextcloudpasswords.data.user.UserController import com.hegocre.nextcloudpasswords.utils.OkHttpRequest import com.hegocre.nextcloudpasswords.utils.PreferencesManager import org.junit.Assert @@ -16,10 +17,8 @@ class ApiControllerTest { @Before fun setup() { context = InstrumentationRegistry.getInstrumentation().targetContext - with(PreferencesManager.getInstance(context)) { - setLoggedInServer("") - setLoggedInUser("") - setLoggedInPassword("") + with(UserController.getInstance(context)) { + logIn("","","") } } diff --git a/app/src/androidTest/java/com/hegocre/nextcloudpasswords/SodiumTest.kt b/app/src/androidTest/java/com/hegocre/nextcloudpasswords/SodiumTest.kt index 8987e265..a6a39461 100644 --- a/app/src/androidTest/java/com/hegocre/nextcloudpasswords/SodiumTest.kt +++ b/app/src/androidTest/java/com/hegocre/nextcloudpasswords/SodiumTest.kt @@ -10,13 +10,14 @@ import com.hegocre.nextcloudpasswords.api.encryption.CSEv1Keychain import com.hegocre.nextcloudpasswords.utils.LazySodiumUtils import com.hegocre.nextcloudpasswords.utils.decryptValue import com.hegocre.nextcloudpasswords.utils.encryptValue -import org.junit.Assert -import org.junit.Assert.assertTrue +import org.junit.Assert.assertEquals +import org.junit.Ignore import org.junit.Test import java.util.Locale class SodiumTest { + @Ignore("passwordHash returns something. Needs to be checked.") @Test fun testSodiumSolve() { val salts = Array(3) { "" } @@ -56,7 +57,7 @@ class SodiumTest { ) val secret = sodium.sodiumBin2Hex(passwordHash) - assertTrue(secret.lowercase(Locale.getDefault()) == "") + assertEquals("", secret.lowercase(Locale.getDefault())) } @Test @@ -72,6 +73,6 @@ class SodiumTest { val encryptedString = testString.encryptValue("test_key", csEv1Keychain) val decryptedString = encryptedString.decryptValue("test_key", csEv1Keychain) - Assert.assertEquals(testString, decryptedString) + assertEquals(testString, decryptedString) } } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8a26159f..fc4dbb3c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -55,6 +55,14 @@ + + + + + + diff --git a/app/src/main/java/com/hegocre/nextcloudpasswords/api/ApiController.kt b/app/src/main/java/com/hegocre/nextcloudpasswords/api/ApiController.kt index d1be6e48..28931d8d 100644 --- a/app/src/main/java/com/hegocre/nextcloudpasswords/api/ApiController.kt +++ b/app/src/main/java/com/hegocre/nextcloudpasswords/api/ApiController.kt @@ -36,16 +36,15 @@ import kotlinx.coroutines.withContext * * @param context Context of the application. */ -class ApiController private constructor(context: Context) { - private val server = UserController.getInstance(context).getServer() +class ApiController private constructor(private final val context: Context) { private val preferencesManager = PreferencesManager.getInstance(context) - private val passwordsApi = PasswordsApi.getInstance(server) - private val foldersApi = FoldersApi.getInstance(server) - private val sessionApi = SessionApi.getInstance(server) - private val serviceApi = ServiceApi.getInstance(server) - private val settingsApi = SettingsApi.getInstance(server) + private val passwordsApi = PasswordsApi.getInstance(context) + private val foldersApi = FoldersApi.getInstance(context) + private val sessionApi = SessionApi.getInstance(context) + private val serviceApi = ServiceApi.getInstance(context) + private val settingsApi = SettingsApi.getInstance(context) private var sessionCode: String? = null @@ -146,12 +145,12 @@ class ApiController private constructor(context: Context) { when (requestResult.code) { Error.API_TIMEOUT -> Log.e( "API Controller", - "Timeout requesting session, user ${server.username}" + "Timeout requesting session, user ${getServer().username}" ) Error.API_BAD_RESPONSE -> Log.e( "API Controller", - "Bad response on session request, user ${server.username}" + "Bad response on session request, user ${getServer().username}" ) } } @@ -182,12 +181,12 @@ class ApiController private constructor(context: Context) { when (openedSessionRequest.code) { Error.API_TIMEOUT -> Log.e( "API Controller", - "Timeout opening session, user ${server.username}" + "Timeout opening session, user ${getServer().username}" ) Error.API_BAD_RESPONSE -> Log.e( "API Controller", - "Bad response on session open, user ${server.username}" + "Bad response on session open, user ${getServer().username}" ) } } @@ -350,11 +349,16 @@ class ApiController private constructor(context: Context) { return result is Result.Success } + fun getServer() = UserController.getInstance(context).getServer() + fun getFaviconServiceRequest(url: String): Pair = - Pair(serviceApi.getFaviconUrl(url), server) + Pair(serviceApi.getFaviconUrl(url), getServer()) fun getAvatarServiceRequest(): Pair = - Pair(serviceApi.getAvatarUrl(), server) + Pair(serviceApi.getAvatarUrl(), getServer()) + + fun getAvatarServiceUrl(server: Server) = serviceApi.getAvatarUrl(server) + companion object { private var instance: ApiController? = null diff --git a/app/src/main/java/com/hegocre/nextcloudpasswords/api/FoldersApi.kt b/app/src/main/java/com/hegocre/nextcloudpasswords/api/FoldersApi.kt index 5568100b..a5306950 100644 --- a/app/src/main/java/com/hegocre/nextcloudpasswords/api/FoldersApi.kt +++ b/app/src/main/java/com/hegocre/nextcloudpasswords/api/FoldersApi.kt @@ -1,10 +1,12 @@ package com.hegocre.nextcloudpasswords.api +import android.content.Context import com.hegocre.nextcloudpasswords.BuildConfig import com.hegocre.nextcloudpasswords.data.folder.DeletedFolder import com.hegocre.nextcloudpasswords.data.folder.Folder import com.hegocre.nextcloudpasswords.data.folder.NewFolder import com.hegocre.nextcloudpasswords.data.folder.UpdatedFolder +import com.hegocre.nextcloudpasswords.data.user.UserController import com.hegocre.nextcloudpasswords.utils.Error import com.hegocre.nextcloudpasswords.utils.OkHttpRequest import com.hegocre.nextcloudpasswords.utils.Result @@ -13,15 +15,16 @@ import kotlinx.coroutines.withContext import kotlinx.serialization.json.Json import java.net.SocketTimeoutException import javax.net.ssl.SSLHandshakeException +import kotlin.context /** * Class with methods used to interact with the * [Folder API](https://git.mdns.eu/nextcloud/passwords/-/wikis/Developers/Api/Folder-Api). * This is a Singleton class and will have only one instance. * - * @property server The [Server] where the requests will be made. + * @property context The [Context] where the requests will be made. */ -class FoldersApi private constructor(private var server: Server) { +class FoldersApi private constructor(private var context: Context) { /** * Sends a request to the api to list all the user passwords. If the user uses CSE, a @@ -36,10 +39,10 @@ class FoldersApi private constructor(private var server: Server) { return try { val apiResponse = withContext(Dispatchers.IO) { OkHttpRequest.getInstance().get( - sUrl = server.url + LIST_URL, + sUrl = getServer().url + LIST_URL, sessionCode = sessionCode, - username = server.username, - password = server.password + username = getServer().username, + password = getServer().password ) } @@ -88,12 +91,12 @@ class FoldersApi private constructor(private var server: Server) { return try { val apiResponse = withContext(Dispatchers.IO) { OkHttpRequest.getInstance().post( - sUrl = server.url + CREATE_URL, + sUrl = getServer().url + CREATE_URL, sessionCode = sessionCode, body = Json.encodeToString(newFolder), mediaType = OkHttpRequest.JSON, - username = server.username, - password = server.password + username = getServer().username, + password = getServer().password ) } @@ -140,12 +143,12 @@ class FoldersApi private constructor(private var server: Server) { return try { val apiResponse = withContext(Dispatchers.IO) { OkHttpRequest.getInstance().patch( - sUrl = server.url + UPDATE_URL, + sUrl = getServer().url + UPDATE_URL, sessionCode = sessionCode, body = Json.encodeToString(updatedFolder), mediaType = OkHttpRequest.JSON, - username = server.username, - password = server.password + username = getServer().username, + password = getServer().password ) } @@ -191,12 +194,12 @@ class FoldersApi private constructor(private var server: Server) { return try { val apiResponse = withContext(Dispatchers.IO) { OkHttpRequest.getInstance().delete( - sUrl = server.url + DELETE_URL, + sUrl = getServer().url + DELETE_URL, sessionCode = sessionCode, body = Json.encodeToString(deletedFolder), mediaType = OkHttpRequest.JSON, - username = server.username, - password = server.password + username = getServer().username, + password = getServer().password ) } @@ -228,6 +231,8 @@ class FoldersApi private constructor(private var server: Server) { } } + fun getServer() = UserController.getInstance(context).getServer() + companion object { private const val LIST_URL = "/index.php/apps/passwords/api/1.0/folder/list" private const val CREATE_URL = "/index.php/apps/passwords/api/1.0/folder/create" @@ -240,15 +245,15 @@ class FoldersApi private constructor(private var server: Server) { /** * Get the instance of the [FoldersApi], and create it if null. * - * @param server The [Server] where the requests will be made. + * @param context The [Context] where the requests will be made. * @return The instance of the api. */ - fun getInstance(server: Server): FoldersApi { + fun getInstance(context: Context): FoldersApi { synchronized(this) { var tempInstance = instance if (tempInstance == null) { - tempInstance = FoldersApi(server) + tempInstance = FoldersApi(context) instance = tempInstance } diff --git a/app/src/main/java/com/hegocre/nextcloudpasswords/api/PasswordsApi.kt b/app/src/main/java/com/hegocre/nextcloudpasswords/api/PasswordsApi.kt index 3f97301b..95dc3279 100644 --- a/app/src/main/java/com/hegocre/nextcloudpasswords/api/PasswordsApi.kt +++ b/app/src/main/java/com/hegocre/nextcloudpasswords/api/PasswordsApi.kt @@ -1,10 +1,12 @@ package com.hegocre.nextcloudpasswords.api +import android.content.Context import com.hegocre.nextcloudpasswords.BuildConfig import com.hegocre.nextcloudpasswords.data.password.DeletedPassword import com.hegocre.nextcloudpasswords.data.password.NewPassword import com.hegocre.nextcloudpasswords.data.password.Password import com.hegocre.nextcloudpasswords.data.password.UpdatedPassword +import com.hegocre.nextcloudpasswords.data.user.UserController import com.hegocre.nextcloudpasswords.utils.Error import com.hegocre.nextcloudpasswords.utils.OkHttpRequest import com.hegocre.nextcloudpasswords.utils.Result @@ -19,9 +21,9 @@ import javax.net.ssl.SSLHandshakeException * [Password API](https://git.mdns.eu/nextcloud/passwords/-/wikis/Developers/Api/Password-Api). * This is a Singleton class and will have only one instance. * - * @param server The [Server] where the requests will be made. + * @param context The [Context] where the requests will be made. */ -class PasswordsApi private constructor(private var server: Server) { +class PasswordsApi private constructor(private var context: Context) { /** * Sends a request to the api to list all the user passwords. If the user uses CSE, a @@ -36,10 +38,10 @@ class PasswordsApi private constructor(private var server: Server) { return try { val apiResponse = withContext(Dispatchers.IO) { OkHttpRequest.getInstance().get( - sUrl = server.url + LIST_URL, + sUrl = getServer().url + LIST_URL, sessionCode = sessionCode, - username = server.username, - password = server.password + username = getServer().username, + password = getServer().password ) } @@ -89,12 +91,12 @@ class PasswordsApi private constructor(private var server: Server) { return try { val apiResponse = withContext(Dispatchers.IO) { OkHttpRequest.getInstance().post( - sUrl = server.url + CREATE_URL, + sUrl = getServer().url + CREATE_URL, sessionCode = sessionCode, body = Json.encodeToString(newPassword), mediaType = OkHttpRequest.JSON, - username = server.username, - password = server.password + username = getServer().username, + password = getServer().password ) } @@ -141,12 +143,12 @@ class PasswordsApi private constructor(private var server: Server) { return try { val apiResponse = withContext(Dispatchers.IO) { OkHttpRequest.getInstance().patch( - sUrl = server.url + UPDATE_URL, + sUrl = getServer().url + UPDATE_URL, sessionCode = sessionCode, body = Json.encodeToString(updatedPassword), mediaType = OkHttpRequest.JSON, - username = server.username, - password = server.password + username = getServer().username, + password = getServer().password ) } @@ -192,12 +194,12 @@ class PasswordsApi private constructor(private var server: Server) { return try { val apiResponse = withContext(Dispatchers.IO) { OkHttpRequest.getInstance().delete( - sUrl = server.url + DELETE_URL, + sUrl = getServer().url + DELETE_URL, sessionCode = sessionCode, body = Json.encodeToString(deletedPassword), mediaType = OkHttpRequest.JSON, - username = server.username, - password = server.password + username = getServer().username, + password = getServer().password ) } @@ -229,6 +231,8 @@ class PasswordsApi private constructor(private var server: Server) { } } + fun getServer() = UserController.getInstance(context).getServer() + companion object { private const val LIST_URL = "/index.php/apps/passwords/api/1.0/password/list" private const val CREATE_URL = "/index.php/apps/passwords/api/1.0/password/create" @@ -240,15 +244,15 @@ class PasswordsApi private constructor(private var server: Server) { /** * Get the instance of the [PasswordsApi], and create it if null. * - * @param server The [Server] where the requests will be made. + * @param context The [Context] where the requests will be made. * @return The instance of the api. */ - fun getInstance(server: Server): PasswordsApi { + fun getInstance(context: Context): PasswordsApi { synchronized(this) { var tempInstance = instance if (tempInstance == null) { - tempInstance = PasswordsApi(server) + tempInstance = PasswordsApi(context) instance = tempInstance } diff --git a/app/src/main/java/com/hegocre/nextcloudpasswords/api/Server.kt b/app/src/main/java/com/hegocre/nextcloudpasswords/api/Server.kt index 143ae422..8ea7b200 100644 --- a/app/src/main/java/com/hegocre/nextcloudpasswords/api/Server.kt +++ b/app/src/main/java/com/hegocre/nextcloudpasswords/api/Server.kt @@ -1,5 +1,7 @@ package com.hegocre.nextcloudpasswords.api +import kotlinx.serialization.Serializable + /** * A data class representing an authenticated server where requests can be made. The credentials * can be obtained using the @@ -9,8 +11,23 @@ package com.hegocre.nextcloudpasswords.api * @property username The username used to authenticate on the server. * @property password The password used to authenticate on the server. This is usually an app password. */ +@Serializable data class Server( val url: String, val username: String, val password: String -) +) { + private var loggedIn: Boolean = false + + fun isLoggedIn(): Boolean { + return loggedIn + } + + fun logIn() { + this.loggedIn = true + } + + fun logOut() { + this.loggedIn = false + } +} diff --git a/app/src/main/java/com/hegocre/nextcloudpasswords/api/ServiceApi.kt b/app/src/main/java/com/hegocre/nextcloudpasswords/api/ServiceApi.kt index e9f13aa8..65b70b45 100644 --- a/app/src/main/java/com/hegocre/nextcloudpasswords/api/ServiceApi.kt +++ b/app/src/main/java/com/hegocre/nextcloudpasswords/api/ServiceApi.kt @@ -1,9 +1,11 @@ package com.hegocre.nextcloudpasswords.api +import android.content.Context import android.util.Log import com.hegocre.nextcloudpasswords.BuildConfig import com.hegocre.nextcloudpasswords.data.password.GeneratedPassword import com.hegocre.nextcloudpasswords.data.password.RequestedPassword +import com.hegocre.nextcloudpasswords.data.user.UserController import com.hegocre.nextcloudpasswords.utils.Error import com.hegocre.nextcloudpasswords.utils.OkHttpRequest import com.hegocre.nextcloudpasswords.utils.Result @@ -20,9 +22,9 @@ import javax.net.ssl.SSLHandshakeException * [Service API](https://git.mdns.eu/nextcloud/passwords/-/wikis/Developers/Api/Service-Api). * This is a Singleton class and will have only one instance. * - * @param server The [Server] where the requests will be made. + * @param context The [Context] where the requests will be made. */ -class ServiceApi private constructor(private val server: Server) { +class ServiceApi private constructor(private val context: Context) { /** * Sends a request to the api to obtain a generated password using user settings. @@ -42,10 +44,10 @@ class ServiceApi private constructor(private val server: Server) { val apiResponse = withContext(Dispatchers.IO) { OkHttpRequest.getInstance().post( - sUrl = server.url + PASSWORD_URL, + sUrl = getServer().url + PASSWORD_URL, sessionCode = sessionCode, - username = server.username, - password = server.password, + username = getServer().username, + password = getServer().password, body = requestBody, mediaType = OkHttpRequest.JSON ) @@ -84,7 +86,7 @@ class ServiceApi private constructor(private val server: Server) { } fun getFaviconUrl(url: String): String = - server.url + String.format( + getServer().url + String.format( Locale.getDefault(), FAVICON_URL, URLEncoder.encode(url, "utf-8"), @@ -92,12 +94,16 @@ class ServiceApi private constructor(private val server: Server) { ) fun getAvatarUrl(): String = + getAvatarUrl(getServer()) + + fun getAvatarUrl(server: Server): String = server.url + String.format( Locale.getDefault(), AVATAR_URL, URLEncoder.encode(server.username, "utf-8"), 256 ) + fun getServer() = UserController.getInstance(context).getServer() companion object { private const val FAVICON_URL = "/index.php/apps/passwords/api/1.0/service/favicon/%s/%d" @@ -109,15 +115,15 @@ class ServiceApi private constructor(private val server: Server) { /** * Get the instance of the [ServiceApi], and create it if null. * - * @param server The [Server] where the requests will be made. + * @param context The [Context] where the requests will be made. * @return The instance of the api. */ - fun getInstance(server: Server): ServiceApi { + fun getInstance(context: Context): ServiceApi { synchronized(this) { var tempInstance = instance if (tempInstance == null) { - tempInstance = ServiceApi(server) + tempInstance = ServiceApi(context) instance = tempInstance } diff --git a/app/src/main/java/com/hegocre/nextcloudpasswords/api/SessionApi.kt b/app/src/main/java/com/hegocre/nextcloudpasswords/api/SessionApi.kt index 857ed577..c44bfc89 100644 --- a/app/src/main/java/com/hegocre/nextcloudpasswords/api/SessionApi.kt +++ b/app/src/main/java/com/hegocre/nextcloudpasswords/api/SessionApi.kt @@ -1,9 +1,11 @@ package com.hegocre.nextcloudpasswords.api +import android.content.Context import com.hegocre.nextcloudpasswords.BuildConfig import com.hegocre.nextcloudpasswords.api.encryption.PWDv1Challenge import com.hegocre.nextcloudpasswords.api.exceptions.ClientDeauthorizedException import com.hegocre.nextcloudpasswords.api.exceptions.PWDv1ChallengeMasterKeyInvalidException +import com.hegocre.nextcloudpasswords.data.user.UserController import com.hegocre.nextcloudpasswords.utils.Error import com.hegocre.nextcloudpasswords.utils.OkHttpRequest import com.hegocre.nextcloudpasswords.utils.Result @@ -18,9 +20,9 @@ import javax.net.ssl.SSLHandshakeException * [Session API](https://git.mdns.eu/nextcloud/passwords/-/wikis/Developers/Api/Session-Api). * This is a Singleton class and will have only one instance. * - * @param server The [Server] where the requests will be made. + * @param context The [Context] where the requests will be made. */ -class SessionApi private constructor(private var server: Server) { +class SessionApi private constructor(private var context: Context) { /** * Sends a request to the api to open a session. If the user uses client-side encryption, @@ -33,9 +35,9 @@ class SessionApi private constructor(private var server: Server) { val apiResponse = try { withContext(Dispatchers.IO) { OkHttpRequest.getInstance().get( - sUrl = server.url + REQUEST_URL, - username = server.username, - password = server.password + sUrl = getServer().url + REQUEST_URL, + username = getServer().username, + password = getServer().password ) } } catch (e: SSLHandshakeException) { @@ -92,11 +94,11 @@ class SessionApi private constructor(private var server: Server) { return try { val apiResponse = withContext(Dispatchers.IO) { OkHttpRequest.getInstance().post( - sUrl = server.url + OPEN_URL, + sUrl = getServer().url + OPEN_URL, body = jsonChallenge, mediaType = OkHttpRequest.JSON, - username = server.username, - password = server.password + username = getServer().username, + password = getServer().password ) } @@ -148,10 +150,10 @@ class SessionApi private constructor(private var server: Server) { return try { val apiResponse = withContext(Dispatchers.IO) { OkHttpRequest.getInstance().get( - sUrl = server.url + KEEPALIVE_URL, + sUrl = getServer().url + KEEPALIVE_URL, sessionCode = sessionCode, - username = server.username, - password = server.password + username = getServer().username, + password = getServer().password ) } @@ -180,10 +182,10 @@ class SessionApi private constructor(private var server: Server) { return try { val apiResponse = withContext(Dispatchers.IO) { OkHttpRequest.getInstance().get( - sUrl = server.url + CLOSE_URL, + sUrl = getServer().url + CLOSE_URL, sessionCode = sessionCode, - username = server.username, - password = server.password + username = getServer().username, + password = getServer().password ) } @@ -201,6 +203,8 @@ class SessionApi private constructor(private var server: Server) { } } + fun getServer() = UserController.getInstance(context).getServer() + companion object { private const val REQUEST_URL = "/index.php/apps/passwords/api/1.0/session/request" private const val OPEN_URL = "/index.php/apps/passwords/api/1.0/session/open" @@ -212,15 +216,15 @@ class SessionApi private constructor(private var server: Server) { /** * Get the instance of the [SessionApi], and create it if null. * - * @param server The [Server] where the requests will be made. + * @param context The [Context] where the requests will be made. * @return The instance of the api. */ - fun getInstance(server: Server): SessionApi { + fun getInstance(context: Context): SessionApi { synchronized(this) { var tempInstance = instance if (tempInstance == null) { - tempInstance = SessionApi(server) + tempInstance = SessionApi(context) instance = tempInstance } diff --git a/app/src/main/java/com/hegocre/nextcloudpasswords/api/SettingsApi.kt b/app/src/main/java/com/hegocre/nextcloudpasswords/api/SettingsApi.kt index ead38aa2..622cf76b 100644 --- a/app/src/main/java/com/hegocre/nextcloudpasswords/api/SettingsApi.kt +++ b/app/src/main/java/com/hegocre/nextcloudpasswords/api/SettingsApi.kt @@ -1,7 +1,9 @@ package com.hegocre.nextcloudpasswords.api +import android.content.Context import com.hegocre.nextcloudpasswords.BuildConfig import com.hegocre.nextcloudpasswords.data.serversettings.ServerSettings +import com.hegocre.nextcloudpasswords.data.user.UserController import com.hegocre.nextcloudpasswords.utils.Error import com.hegocre.nextcloudpasswords.utils.OkHttpRequest import com.hegocre.nextcloudpasswords.utils.Result @@ -11,7 +13,7 @@ import kotlinx.serialization.json.Json import java.net.SocketTimeoutException import javax.net.ssl.SSLHandshakeException -class SettingsApi private constructor(private val server: Server) { +class SettingsApi private constructor(private val context: Context) { /** * Sends a request to the api to obtain required user settings. No session is required to send this request. @@ -22,11 +24,11 @@ class SettingsApi private constructor(private val server: Server) { return try { val apiResponse = withContext(Dispatchers.IO) { OkHttpRequest.getInstance().post( - sUrl = server.url + GET_URL, + sUrl = getServer().url + GET_URL, body = ServerSettings.getRequestBody(), mediaType = OkHttpRequest.JSON, - username = server.username, - password = server.password, + username = getServer().username, + password = getServer().password, ) } @@ -62,6 +64,8 @@ class SettingsApi private constructor(private val server: Server) { } + fun getServer() = UserController.getInstance(context).getServer() + companion object { private const val GET_URL = "/index.php/apps/passwords/api/1.0/settings/get" @@ -70,15 +74,15 @@ class SettingsApi private constructor(private val server: Server) { /** * Get the instance of the [ServiceApi], and create it if null. * - * @param server The [Server] where the requests will be made. + * @param context The [Context] where the requests will be made. * @return The instance of the api. */ - fun getInstance(server: Server): SettingsApi { + fun getInstance(context: Context): SettingsApi { synchronized(this) { var tempInstance = instance if (tempInstance == null) { - tempInstance = SettingsApi(server) + tempInstance = SettingsApi(context) instance = tempInstance } diff --git a/app/src/main/java/com/hegocre/nextcloudpasswords/data/user/UserController.kt b/app/src/main/java/com/hegocre/nextcloudpasswords/data/user/UserController.kt index cf300e97..ef76c557 100644 --- a/app/src/main/java/com/hegocre/nextcloudpasswords/data/user/UserController.kt +++ b/app/src/main/java/com/hegocre/nextcloudpasswords/data/user/UserController.kt @@ -4,8 +4,13 @@ import android.content.Context import com.hegocre.nextcloudpasswords.api.Server import com.hegocre.nextcloudpasswords.databases.AppDatabase import com.hegocre.nextcloudpasswords.utils.PreferencesManager +import dev.spght.encryptedprefs.EncryptedSharedPreferences +import dev.spght.encryptedprefs.MasterKey +import dev.spght.encryptedprefs.MasterKeys import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import kotlinx.serialization.json.Json +import androidx.core.content.edit /** * Class used to manage to log in and log out, as well as providing the current server to the API @@ -18,8 +23,17 @@ class UserController private constructor(context: Context) { private val passwordDatabase = AppDatabase.getInstance(context) private val folderDatabase = AppDatabase.getInstance(context) + private val servers = mutableSetOf() + + fun init() { + val propertyVal = _preferencesManager.getServers() + if (propertyVal != null) { + this.servers.addAll(unmarshal(propertyVal)) + } + } + val isLoggedIn: Boolean - get() = _preferencesManager.getLoggedInServer() != null + get() = getServers().find { it.isLoggedIn() } != null /** * Method to store the server URl and credentials on the storage. @@ -28,11 +42,35 @@ class UserController private constructor(context: Context) { * @param username The username used to authenticate on the server. * @param password The password used to authenticate on the server. */ - fun logIn(server: String, username: String, password: String) { - with(_preferencesManager) { - setLoggedInServer(server) - setLoggedInUser(username) - setLoggedInPassword(password) + suspend fun logIn(server: String, username: String, password: String) { + try { + val currentServer = getServer() + currentServer.logOut() + } catch(e: UserException) { + // user not logged in + } + val newServer = Server(server, username, password) + this.servers.add(newServer) + setActiveServer(newServer) + } + + private fun updateServers(servers: Set) { + _preferencesManager.setServers(marshal(servers)) + } + + fun getServers() : Set { + return this.servers; + } + + fun removeServer(server: Server) { + this.servers.remove(server) + updateServers(this.servers) + } + + suspend fun clearAllDB() { + withContext(Dispatchers.IO) { + passwordDatabase.passwordDao.deleteDatabase() + folderDatabase.folderDao.deleteDatabase() } } @@ -42,11 +80,9 @@ class UserController private constructor(context: Context) { * */ suspend fun logOut() { - withContext(Dispatchers.IO) { - passwordDatabase.passwordDao.deleteDatabase() - folderDatabase.folderDao.deleteDatabase() - } + clearAllDB() _preferencesManager.clear() + this.servers.clear() } /** @@ -57,12 +93,36 @@ class UserController private constructor(context: Context) { */ @Throws(UserException::class) fun getServer(): Server { - return with(_preferencesManager) { - val url = getLoggedInServer() ?: throw UserException("Not logged in") - val username = getLoggedInUser() ?: throw UserException("Not logged in") - val password = getLoggedInPassword() ?: throw UserException("Not logged in") - Server(url, username, password) + val loggedInServer = servers.find { it.isLoggedIn() } + // Check if a logged-in server was found + if (loggedInServer != null) { + return loggedInServer + } else { + // If no server has loggedIn = true, or if the servers list itself might be empty + // and you consider that an exceptional case for this method. + throw UserException("No logged-in server found.") + } + } + + suspend fun setActiveServer(serverToActivate: Server) { + // Deactivate all other servers + servers.forEach { + if (it != serverToActivate) { + it.logOut() + } } + clearAllDB() + // Activate the selected server + serverToActivate.logIn() + updateServers(servers) + } + + fun marshal(configs: Set): String { + return Json.encodeToString(configs) + } + + fun unmarshal(propertyVal: String): Set { + return Json.decodeFromString(propertyVal) } companion object { @@ -80,6 +140,7 @@ class UserController private constructor(context: Context) { if (tempInstance == null) { tempInstance = UserController(context) + tempInstance.init() instance = tempInstance } @@ -87,4 +148,4 @@ class UserController private constructor(context: Context) { } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/hegocre/nextcloudpasswords/services/keepalive/KeepAliveWorker.kt b/app/src/main/java/com/hegocre/nextcloudpasswords/services/keepalive/KeepAliveWorker.kt index 5331f7e4..b268bf14 100644 --- a/app/src/main/java/com/hegocre/nextcloudpasswords/services/keepalive/KeepAliveWorker.kt +++ b/app/src/main/java/com/hegocre/nextcloudpasswords/services/keepalive/KeepAliveWorker.kt @@ -17,7 +17,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import java.util.concurrent.TimeUnit -class KeepAliveWorker(context: Context, private val params: WorkerParameters) : +class KeepAliveWorker(private val context: Context, private val params: WorkerParameters) : CoroutineWorker(context, params) { override suspend fun doWork(): Result { val apiController = ApiController.getInstance(applicationContext) @@ -26,7 +26,7 @@ class KeepAliveWorker(context: Context, private val params: WorkerParameters) : } val server = UserController.getInstance(applicationContext).getServer() - val sessionApi = SessionApi.getInstance(server) + val sessionApi = SessionApi.getInstance(context) val sessionCode = params.inputData.getString(SESSION_CODE_KEY) ?: return Result.failure() val keepAliveDelay = params.inputData.getLong(KEEPALIVE_DELAY_KEY, -1L) diff --git a/app/src/main/java/com/hegocre/nextcloudpasswords/ui/activities/AccountsActivity.kt b/app/src/main/java/com/hegocre/nextcloudpasswords/ui/activities/AccountsActivity.kt new file mode 100644 index 00000000..873afbfb --- /dev/null +++ b/app/src/main/java/com/hegocre/nextcloudpasswords/ui/activities/AccountsActivity.kt @@ -0,0 +1,32 @@ +package com.hegocre.nextcloudpasswords.ui.activities + +import android.os.Bundle +import android.widget.Toast +import androidx.activity.compose.setContent +import androidx.core.view.WindowCompat +import androidx.fragment.app.FragmentActivity +import androidx.lifecycle.lifecycleScope +import com.hegocre.nextcloudpasswords.BuildConfig +import com.hegocre.nextcloudpasswords.ui.components.NCPAccountsScreen +import com.hegocre.nextcloudpasswords.ui.components.NCPAppLockWrapper +import com.hegocre.nextcloudpasswords.ui.viewmodels.PasswordsViewModel +import com.hegocre.nextcloudpasswords.utils.LogHelper +import com.hegocre.nextcloudpasswords.utils.copyToClipboard + +class AccountsActivity : FragmentActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + WindowCompat.setDecorFitsSystemWindows(window, false) + + setContent { + NCPAppLockWrapper { + NCPAccountsScreen( + onBackPressed = this::finish, + lifecycleScope + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hegocre/nextcloudpasswords/ui/activities/WebLoginActivity.kt b/app/src/main/java/com/hegocre/nextcloudpasswords/ui/activities/WebLoginActivity.kt index cfa4738f..3bf41089 100644 --- a/app/src/main/java/com/hegocre/nextcloudpasswords/ui/activities/WebLoginActivity.kt +++ b/app/src/main/java/com/hegocre/nextcloudpasswords/ui/activities/WebLoginActivity.kt @@ -7,9 +7,12 @@ import android.webkit.WebStorage import android.webkit.WebView import androidx.activity.ComponentActivity import androidx.activity.compose.setContent +import androidx.activity.result.launch import androidx.core.view.WindowCompat +import androidx.lifecycle.lifecycleScope import com.hegocre.nextcloudpasswords.data.user.UserController import com.hegocre.nextcloudpasswords.ui.components.NCPWebLoginScreen +import kotlinx.coroutines.launch import java.net.URLDecoder import java.nio.charset.StandardCharsets @@ -46,13 +49,17 @@ class WebLoginActivity : ComponentActivity() { val intent = Intent() if (user != null && password != null && server != null) { - UserController.getInstance(this).logIn( - server, - user, - password - ) + val userController = UserController.getInstance(this) + lifecycleScope.launch { + userController.logIn( + server, + user, + password + ) + } intent.putExtra("loggedIn", true) setResult(RESULT_OK, intent) + } else { intent.putExtra("loggedIn", false) setResult(RESULT_CANCELED, intent) diff --git a/app/src/main/java/com/hegocre/nextcloudpasswords/ui/components/NCPAccounts.kt b/app/src/main/java/com/hegocre/nextcloudpasswords/ui/components/NCPAccounts.kt new file mode 100644 index 00000000..5e94fade --- /dev/null +++ b/app/src/main/java/com/hegocre/nextcloudpasswords/ui/components/NCPAccounts.kt @@ -0,0 +1,280 @@ +package com.hegocre.nextcloudpasswords.ui.components + +import android.content.Intent +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.scaleIn +import androidx.compose.animation.scaleOut +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.Image +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +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.layout.statusBars +import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.outlined.AccountCircle +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.LifecycleCoroutineScope +import androidx.lifecycle.viewmodel.compose.viewModel +import com.hegocre.nextcloudpasswords.R +import com.hegocre.nextcloudpasswords.api.Server +import com.hegocre.nextcloudpasswords.data.user.UserController +import com.hegocre.nextcloudpasswords.ui.NCPScreen +import com.hegocre.nextcloudpasswords.ui.activities.LoginActivity +import com.hegocre.nextcloudpasswords.ui.theme.ContentAlpha +import com.hegocre.nextcloudpasswords.ui.theme.NextcloudPasswordsTheme +import com.hegocre.nextcloudpasswords.ui.viewmodels.PasswordsViewModel +import kotlinx.coroutines.launch + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) +@Composable +fun NCPAccountsScreen( + onBackPressed: () -> Unit, + lifecycleScope: LifecycleCoroutineScope? = null, + passwordsViewModel: PasswordsViewModel = viewModel(), + onLogoLongPressed: (() -> Unit)? = null +) { + val context = LocalContext.current + var servers by remember { mutableStateOf>(emptySet()) } + var isLoading by remember { mutableStateOf(true) } + var errorMessage by remember { mutableStateOf(null) } + var serverForContextMenu by remember { mutableStateOf(null) } + + fun loadServers() { + isLoading = true + errorMessage = null + try { + servers = UserController.getInstance(context).getServers() + } catch (e: Exception) { + errorMessage = "Failed to load accounts: ${e.localizedMessage}" + } finally { + isLoading = false + } + } + + LaunchedEffect(key1 = Unit) { + loadServers() + } + + NextcloudPasswordsTheme { + Scaffold( + topBar = { + TopAppBar( + title = { + Text(text = stringResource(id = R.string.screen_manage_accounts)) + }, + navigationIcon = { + IconButton(onClick = onBackPressed) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = stringResource(id = R.string.navigation_back) + ) + } + }, + windowInsets = WindowInsets.statusBars + ) + }, + floatingActionButton = { + AnimatedVisibility( + visible = true, + enter = scaleIn(), + exit = scaleOut(), + ) { + FloatingActionButton( + onClick = { + val intent = Intent(context, LoginActivity::class.java) + context.startActivity(intent) + }, + ) { + Icon( + imageVector = Icons.Filled.Add, + contentDescription = stringResource(id = R.string.action_create_element) + ) + } + } + }, + contentWindowInsets = WindowInsets.systemBars + ) { paddingValues -> + Column( + modifier = Modifier + .padding(paddingValues) + .fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = if (isLoading || errorMessage != null || servers.isEmpty()) Arrangement.Center else Arrangement.Top + ) { + if (isLoading) { + CircularProgressIndicator() + } else if (errorMessage != null) { + Text( + text = errorMessage!!, + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.padding(16.dp) + ) + } else if (servers.isNotEmpty()) { + Column( + modifier = Modifier + .padding(vertical = 8.dp) + .verticalScroll(rememberScrollState()) + .fillMaxWidth(), + horizontalAlignment = Alignment.Start + ) { + servers.forEach { server -> + Box(modifier = Modifier.fillMaxWidth()) { + Row( + modifier = Modifier + .fillMaxWidth() + .combinedClickable( + onClick = { + lifecycleScope?.launch { + val userController = + UserController.getInstance(context) + userController.setActiveServer(server) + loadServers() + passwordsViewModel.sync() + } + }, + onLongClick = { + serverForContextMenu = server + } + ) + .padding(vertical = 12.dp, horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Box( + modifier = Modifier + .padding(all = 8.dp) + .padding(end = 12.dp) + ) { + Image( + painter = passwordsViewModel.getPainterForAvatar( + server + ), + contentDescription = "", + modifier = Modifier + .clip(CircleShape) + .size(40.dp) + ) + } + + Column { + Text( + text = server.username, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Start + ) + + CompositionLocalProvider( + LocalContentColor provides LocalContentColor.current.copy( + alpha = ContentAlpha.medium + ) + ) { + Text( + text = server.url, + style = MaterialTheme.typography.bodyMedium, + fontSize = 14.sp + ) + } + } + if (server.isLoggedIn()) { + Icon( + imageVector = Icons.Filled.Check, + contentDescription = stringResource(R.string.logged_in_status), + tint = Color.Green, + modifier = Modifier.padding(start = 8.dp) + ) + } + } + + DropdownMenu( + expanded = serverForContextMenu == server, + onDismissRequest = { serverForContextMenu = null } + ) { + DropdownMenuItem( + enabled = !server.isLoggedIn(), + text = { + Text( + stringResource( + R.string.delete_account_entry, + server.username + ) + ) + }, + onClick = { + serverForContextMenu = null + try { + UserController.getInstance(context) + .removeServer(server) + loadServers() + } catch (e: Exception) { + errorMessage = + "Failed to delete ${server.username}: ${e.localizedMessage}" + } + } + ) + } + } + } + Spacer(modifier = Modifier.height(8.dp)) + } + } else { + Text( + text = stringResource(R.string.no_account_logged_in), + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.padding(16.dp) + ) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/hegocre/nextcloudpasswords/ui/components/NCPApp.kt b/app/src/main/java/com/hegocre/nextcloudpasswords/ui/components/NCPApp.kt index 7f9994d6..8cf9ce6b 100644 --- a/app/src/main/java/com/hegocre/nextcloudpasswords/ui/components/NCPApp.kt +++ b/app/src/main/java/com/hegocre/nextcloudpasswords/ui/components/NCPApp.kt @@ -96,10 +96,6 @@ fun NextcloudPasswordsApp( } val (searchQuery, setSearchQuery) = rememberSaveable { mutableStateOf(defaultSearchQuery) } - val server = remember { - passwordsViewModel.server - } - NextcloudPasswordsTheme { val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior( rememberTopAppBarState() @@ -112,8 +108,7 @@ fun NextcloudPasswordsApp( topBar = { if (currentScreen != NCPScreen.PasswordEdit && currentScreen != NCPScreen.FolderEdit) { NCPSearchTopBar( - username = server.username, - serverAddress = server.url, + passwordsViewModel = passwordsViewModel, title = when (currentScreen) { NCPScreen.Passwords, NCPScreen.Favorites -> stringResource(currentScreen.title) NCPScreen.Folders -> { diff --git a/app/src/main/java/com/hegocre/nextcloudpasswords/ui/components/NCPTopBar.kt b/app/src/main/java/com/hegocre/nextcloudpasswords/ui/components/NCPTopBar.kt index 9e61cb64..99a2b701 100644 --- a/app/src/main/java/com/hegocre/nextcloudpasswords/ui/components/NCPTopBar.kt +++ b/app/src/main/java/com/hegocre/nextcloudpasswords/ui/components/NCPTopBar.kt @@ -29,6 +29,7 @@ import androidx.compose.material.icons.automirrored.outlined.Logout import androidx.compose.material.icons.filled.Clear import androidx.compose.material.icons.outlined.AccountCircle import androidx.compose.material.icons.outlined.Info +import androidx.compose.material.icons.outlined.ManageAccounts import androidx.compose.material.icons.outlined.Search import androidx.compose.material.icons.outlined.Settings import androidx.compose.material3.BasicAlertDialog @@ -78,6 +79,7 @@ import androidx.compose.ui.window.DialogProperties import com.hegocre.nextcloudpasswords.R import com.hegocre.nextcloudpasswords.ui.theme.ContentAlpha import com.hegocre.nextcloudpasswords.ui.theme.NextcloudPasswordsTheme +import com.hegocre.nextcloudpasswords.ui.viewmodels.PasswordsViewModel import kotlinx.coroutines.job object AppBarDefaults { @@ -87,8 +89,7 @@ object AppBarDefaults { @OptIn(ExperimentalMaterial3Api::class) @Composable fun NCPSearchTopBar( - username: String, - serverAddress: String, + passwordsViewModel: PasswordsViewModel? = null, modifier: Modifier = Modifier, title: String = stringResource(R.string.app_name), scrollBehavior: TopAppBarScrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior( @@ -97,7 +98,7 @@ fun NCPSearchTopBar( userAvatar: @Composable (Dp) -> Unit = { size -> Image( imageVector = Icons.Outlined.AccountCircle, - contentDescription = username, + contentDescription = passwordsViewModel?.server?.username ?: "", modifier = Modifier.size(size) ) }, @@ -121,8 +122,7 @@ fun NCPSearchTopBar( ) } else { TitleAppBar( - username = username, - serverAddress = serverAddress, + passwordsViewModel = passwordsViewModel, title = title, onSearchClick = onSearchClick, onLogoutClick = onLogoutClick, @@ -138,8 +138,7 @@ fun NCPSearchTopBar( @OptIn(ExperimentalMaterial3Api::class) @Composable fun TitleAppBar( - username: String, - serverAddress: String, + passwordsViewModel: PasswordsViewModel? = null, title: String, onSearchClick: () -> Unit, onLogoutClick: () -> Unit, @@ -170,8 +169,8 @@ fun TitleAppBar( } PopupAppMenu( - username = username, - serverAddress = serverAddress, + username = passwordsViewModel?.server?.username ?: "", + serverAddress = passwordsViewModel?.server?.url ?: "", menuExpanded = menuExpanded, userAvatar = userAvatar, onDismissRequest = { menuExpanded = false }, @@ -377,6 +376,31 @@ fun PopupAppMenu( } ) + DropdownMenuItem( + onClick = { + val intent = + Intent("com.hegocre.nextcloudpasswords.action.accounts") + .setPackage(context.packageName) + context.startActivity(intent) + onDismissRequest() + }, + text = { + Text( + text = stringResource(id = R.string.screen_manage_accounts), + modifier = Modifier.padding(end = 16.dp) + ) + }, + leadingIcon = { + Icon( + imageVector = Icons.Outlined.ManageAccounts, + contentDescription = stringResource(id = R.string.screen_manage_accounts), + modifier = Modifier + .padding(end = 8.dp) + .padding(start = 16.dp) + ) + } + ) + DropdownMenuItem( onClick = { val intent = Intent("com.hegocre.nextcloudpasswords.action.about") @@ -464,7 +488,7 @@ fun PopupAppMenu( @Composable fun TopBarPreview() { NextcloudPasswordsTheme { - NCPSearchTopBar("", "") + NCPSearchTopBar() } } diff --git a/app/src/main/java/com/hegocre/nextcloudpasswords/ui/viewmodels/PasswordsViewModel.kt b/app/src/main/java/com/hegocre/nextcloudpasswords/ui/viewmodels/PasswordsViewModel.kt index 50fc6e85..20256b53 100644 --- a/app/src/main/java/com/hegocre/nextcloudpasswords/ui/viewmodels/PasswordsViewModel.kt +++ b/app/src/main/java/com/hegocre/nextcloudpasswords/ui/viewmodels/PasswordsViewModel.kt @@ -21,6 +21,7 @@ import coil.compose.rememberAsyncImagePainter import coil.request.ImageRequest import com.hegocre.nextcloudpasswords.R import com.hegocre.nextcloudpasswords.api.ApiController +import com.hegocre.nextcloudpasswords.api.Server import com.hegocre.nextcloudpasswords.api.encryption.CSEv1Keychain import com.hegocre.nextcloudpasswords.api.exceptions.ClientDeauthorizedException import com.hegocre.nextcloudpasswords.api.exceptions.PWDv1ChallengeMasterKeyInvalidException @@ -318,6 +319,13 @@ class PasswordsViewModel(application: Application) : AndroidViewModel(applicatio val context = LocalContext.current val (requestUrl, server) = apiController.getAvatarServiceRequest() + return getPainterForAvatar(server) + } + + @Composable + fun getPainterForAvatar(server: Server): Painter { + val context = LocalContext.current + val requestUrl = apiController.getAvatarServiceUrl(server) return rememberAsyncImagePainter( model = ImageRequest.Builder(context).apply { data(requestUrl) diff --git a/app/src/main/java/com/hegocre/nextcloudpasswords/utils/PreferencesManager.kt b/app/src/main/java/com/hegocre/nextcloudpasswords/utils/PreferencesManager.kt index 60972e56..7f17639a 100644 --- a/app/src/main/java/com/hegocre/nextcloudpasswords/utils/PreferencesManager.kt +++ b/app/src/main/java/com/hegocre/nextcloudpasswords/utils/PreferencesManager.kt @@ -74,22 +74,13 @@ class PreferencesManager private constructor(context: Context) { fun setAppLockPasscode(value: String?): Boolean = _encryptedSharedPrefs.edit().putString("APP_LOCK_PASSCODE", value).commit() - fun getLoggedInServer(): String? = _encryptedSharedPrefs.getString("LOGGED_IN_SERVER", null) - fun setLoggedInServer(value: String?): Boolean = - _encryptedSharedPrefs.edit().putString("LOGGED_IN_SERVER", value).commit() - - fun getLoggedInUser(): String? = _encryptedSharedPrefs.getString("LOGGED_IN_USER", null) - fun setLoggedInUser(value: String?): Boolean = - _encryptedSharedPrefs.edit().putString("LOGGED_IN_USER", value).commit() - - fun getLoggedInPassword(): String? = _encryptedSharedPrefs.getString("LOGGED_IN_PASSWORD", null) - fun setLoggedInPassword(value: String?): Boolean = - _encryptedSharedPrefs.edit().putString("LOGGED_IN_PASSWORD", value).commit() - fun getMasterPassword(): String? = _encryptedSharedPrefs.getString("MASTER_KEY", null) fun setMasterPassword(value: String?): Boolean = _encryptedSharedPrefs.edit().putString("MASTER_KEY", value).commit() + fun getServers(): String? = _encryptedSharedPrefs.getString("SERVERS", null) + fun setServers(servers: String): Boolean = _encryptedSharedPrefs.edit().putString("SERVERS", servers).commit() + fun getCSEv1Keychain(): String? = _encryptedSharedPrefs.getString("CSE_V1_KEYCHAIN", null) fun setCSEv1Keychain(value: String?): Boolean = _encryptedSharedPrefs.edit().putString("CSE_V1_KEYCHAIN", value).commit() diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0c5ab6ce..6b95abfa 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -130,4 +130,10 @@ Oldest first Newest first Order content by + Manage Accounts + Avatar + Unknown user + No account logged in + Delete account + Logged In \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 44fadca5..eeb53117 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,7 +6,7 @@ # http://www.gradle.org/docs/current/userguide/build_environment.html # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. -org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 -XX:+UseParallelGC +org.gradle.jvmargs=-Xmx4096m -Dfile.encoding=UTF-8 -XX:+UseParallelGC # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects