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