浏览代码

adding arbitrary storage implementation, initial steps towards new util classes

Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
Andy Scherzinger 2 年之前
父节点
当前提交
05db482d06
共有 27 个文件被更改,包括 944 次插入90 次删除
  1. 3 0
      app/build.gradle
  2. 19 25
      app/src/androidTest/java/com/nextcloud/talk/activities/MainActivityTest.kt
  3. 1 1
      app/src/androidTest/java/com/nextcloud/talk/ui/LoginIT.java
  4. 8 7
      app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt
  5. 9 3
      app/src/main/java/com/nextcloud/talk/controllers/SettingsController.kt
  6. 8 1
      app/src/main/java/com/nextcloud/talk/dagger/modules/DatabaseModule.java
  7. 15 0
      app/src/main/java/com/nextcloud/talk/dagger/modules/RepositoryModule.kt
  8. 62 0
      app/src/main/java/com/nextcloud/talk/data/source/local/Migrations.kt
  9. 37 13
      app/src/main/java/com/nextcloud/talk/data/source/local/TalkDatabase.kt
  10. 47 0
      app/src/main/java/com/nextcloud/talk/data/source/local/converters/ExternalSignalingServerConverter.kt
  11. 44 0
      app/src/main/java/com/nextcloud/talk/data/storage/ArbitraryStoragesDao.kt
  12. 29 0
      app/src/main/java/com/nextcloud/talk/data/storage/ArbitraryStoragesRepository.kt
  13. 42 0
      app/src/main/java/com/nextcloud/talk/data/storage/ArbitraryStoragesRepositoryImpl.kt
  14. 38 0
      app/src/main/java/com/nextcloud/talk/data/storage/model/ArbitraryStorageNgEntity.kt
  15. 27 10
      app/src/main/java/com/nextcloud/talk/data/user/UsersDao.kt
  16. 49 0
      app/src/main/java/com/nextcloud/talk/data/user/UsersRepository.kt
  17. 119 0
      app/src/main/java/com/nextcloud/talk/data/user/UsersRepositoryImpl.kt
  18. 11 9
      app/src/main/java/com/nextcloud/talk/data/user/model/User.kt
  19. 17 14
      app/src/main/java/com/nextcloud/talk/data/user/model/UserNgEntity.kt
  20. 2 0
      app/src/main/java/com/nextcloud/talk/models/ExternalSignalingServer.kt
  21. 57 0
      app/src/main/java/com/nextcloud/talk/models/database/CapabilitiesUtil.java
  22. 1 0
      app/src/main/java/com/nextcloud/talk/models/database/User.java
  23. 204 0
      app/src/main/java/com/nextcloud/talk/users/UserManager.kt
  24. 33 0
      app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java
  25. 30 0
      app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.java
  26. 27 0
      app/src/main/java/com/nextcloud/talk/utils/database/user/CurrentUserProviderNew.kt
  27. 5 7
      app/src/main/java/com/nextcloud/talk/utils/preferences/preferencestorage/DatabaseStorageModule.java

+ 3 - 0
app/build.gradle

@@ -154,6 +154,7 @@ ext {
     butterknifeVersion = "10.2.3"
     coilKtVersion = "2.1.0"
     daggerVersion = "2.42"
+    lifecycleVersion = '2.2.0'
     okhttpVersion = "4.10.0"
     materialDialogsVersion = "3.3.0"
     parcelerVersion = "1.1.13"
@@ -246,6 +247,8 @@ dependencies {
     kapt "androidx.room:room-compiler:${roomVersion}" // For Kotlin use kapt instead of annotationProcessor
     implementation "androidx.room:room-ktx:${roomVersion}"
 
+    implementation "androidx.lifecycle:lifecycle-livedata-ktx:${lifecycleVersion}"
+
     implementation "org.parceler:parceler-api:$parcelerVersion"
     implementation 'net.orange-box.storebox:storebox-lib:1.4.0'
     implementation "com.jakewharton:butterknife:${butterknifeVersion}"

+ 19 - 25
app/src/androidTest/java/com/nextcloud/talk/activities/MainActivityTest.kt

@@ -1,10 +1,7 @@
 package com.nextcloud.talk.activities
 
-import android.util.Log
 import androidx.test.espresso.intent.rule.IntentsTestRule
-import com.nextcloud.talk.models.database.UserEntity
-import io.reactivex.android.schedulers.AndroidSchedulers
-import io.reactivex.schedulers.Schedulers
+import com.nextcloud.talk.data.user.model.UserNgEntity
 import org.junit.Assert.assertTrue
 import org.junit.Rule
 import org.junit.Test
@@ -18,28 +15,25 @@ class MainActivityTest {
     )
 
     @Test
-    fun login() {
+    suspend fun login() {
         val sut = activityRule.launchActivity(null)
-        sut.userUtils.createOrUpdateUser(
-            "test",
-            "test",
-            "http://server/nc",
-            "test",
-            null,
-            true,
-            "test",
-            null,
-            null,
-            null,
-            null
-        )
-            .subscribeOn(Schedulers.io())
-            .observeOn(AndroidSchedulers.mainThread())
-            .subscribe(
-                { userEntity: UserEntity? -> Log.i("test", "stored: " + userEntity.toString()) },
-                { throwable: Throwable? -> Log.e("test", "throwable") },
-                { Log.d("test", "complete") }
+
+        sut.usersRepository.insertUser(
+            UserNgEntity(
+                0,
+                "test",
+                "test",
+                "http://server/nc",
+                "test",
+                null,
+                null,
+                null,
+                null,
+                null,
+                false,
+                scheduledForDeletion = false
             )
+        )
 
         try {
             Thread.sleep(2000)
@@ -49,7 +43,7 @@ class MainActivityTest {
 
         sut.runOnUiThread { sut.resetConversationsList() }
 
-        assertTrue(sut.userUtils.getIfUserWithUsernameAndServer("test", "http://server/nc"))
+        assertTrue(sut.usersRepository.getUserWithUsernameAndServer("test", "http://server/nc") != null)
 
         try {
         } catch (e: InterruptedException) {

+ 1 - 1
app/src/androidTest/java/com/nextcloud/talk/ui/LoginIT.java

@@ -143,7 +143,7 @@ public class LoginIT {
         onView(withId(R.id.user_name)).check(matches(withText("User One")));
 
         activityScenario.onActivity(activity -> {
-            assertEquals(loginName, Objects.requireNonNull(activity.userUtils.getCurrentUser()).getUserId());
+            assertEquals(loginName, Objects.requireNonNull(activity.usersRepository.getActiveUser()).getUserId());
         });
 
     }

+ 8 - 7
app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt

@@ -46,6 +46,7 @@ import com.nextcloud.talk.controllers.ServerSelectionController
 import com.nextcloud.talk.controllers.SettingsController
 import com.nextcloud.talk.controllers.WebViewLoginController
 import com.nextcloud.talk.controllers.base.providers.ActionBarProvider
+import com.nextcloud.talk.data.user.UsersRepository
 import com.nextcloud.talk.databinding.ActivityMainBinding
 import com.nextcloud.talk.models.database.UserEntity
 import com.nextcloud.talk.models.json.conversations.RoomOverall
@@ -72,9 +73,6 @@ import javax.inject.Inject
 class MainActivity : BaseActivity(), ActionBarProvider {
     lateinit var binding: ActivityMainBinding
 
-    @Inject
-    lateinit var userUtils: UserUtils
-
     @Inject
     lateinit var dataStore: ReactiveEntityStore<Persistable>
 
@@ -84,6 +82,9 @@ class MainActivity : BaseActivity(), ActionBarProvider {
     @Inject
     lateinit var ncApi: NcApi
 
+    @Inject
+    lateinit var usersRepository: UsersRepository
+
     private var router: Router? = null
 
     @Suppress("Detekt.TooGenericExceptionCaught")
@@ -114,7 +115,7 @@ class MainActivity : BaseActivity(), ActionBarProvider {
             onNewIntent(intent)
         } else if (!router!!.hasRootController()) {
             if (hasDb) {
-                if (userUtils.anyUserExists()) {
+                if (usersRepository.getUsers().isNotEmpty()) {
                     setDefaultRootController()
                 } else {
                     launchLoginScreen()
@@ -178,7 +179,7 @@ class MainActivity : BaseActivity(), ActionBarProvider {
     }
 
     fun resetConversationsList() {
-        if (userUtils.anyUserExists()) {
+        if (usersRepository.getUsers().isNotEmpty()) {
             setDefaultRootController()
         }
     }
@@ -218,7 +219,7 @@ class MainActivity : BaseActivity(), ActionBarProvider {
                 "vnd.android.cursor.item/vnd.com.nextcloud.talk2.chat" -> {
                     val user = userId.substringBeforeLast("@")
                     val baseUrl = userId.substringAfterLast("@")
-                    if (userUtils.currentUser?.baseUrl?.endsWith(baseUrl) == true) {
+                    if (usersRepository.getActiveUser()?.baseUrl?.endsWith(baseUrl) == true) {
                         startConversation(user)
                     } else {
                         Snackbar.make(
@@ -234,7 +235,7 @@ class MainActivity : BaseActivity(), ActionBarProvider {
 
     private fun startConversation(userId: String) {
         val roomType = "1"
-        val currentUser = userUtils.currentUser ?: return
+        val currentUser = usersRepository.getActiveUser() ?: return
 
         val apiVersion = ApiUtils.getConversationApiVersion(currentUser, intArrayOf(ApiUtils.APIv4, 1))
         val credentials = ApiUtils.getCredentials(currentUser.username, currentUser.token)

+ 9 - 3
app/src/main/java/com/nextcloud/talk/controllers/SettingsController.kt

@@ -69,6 +69,9 @@ import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.setAppT
 import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
 import com.nextcloud.talk.controllers.base.NewBaseController
 import com.nextcloud.talk.controllers.util.viewBinding
+import com.nextcloud.talk.data.storage.ArbitraryStoragesRepository
+import com.nextcloud.talk.data.user.UsersRepository
+import com.nextcloud.talk.data.user.model.UserNgEntity
 import com.nextcloud.talk.databinding.ControllerSettingsBinding
 import com.nextcloud.talk.jobs.AccountRemovalWorker
 import com.nextcloud.talk.jobs.ContactAddressBookWorker
@@ -115,8 +118,11 @@ class SettingsController : NewBaseController(R.layout.controller_settings) {
     @Inject
     lateinit var userUtils: UserUtils
 
+    @Inject
+    lateinit var userRepository: UsersRepository
+
     private var saveStateHandler: LovelySaveStateHandler? = null
-    private var currentUser: UserEntity? = null
+    private var currentUser: UserNgEntity? = null
     private var credentials: String? = null
     private var proxyTypeChangeListener: OnPreferenceValueChangedListener<String>? = null
     private var proxyCredentialsChangeListener: OnPreferenceValueChangedListener<Boolean>? = null
@@ -134,7 +140,7 @@ class SettingsController : NewBaseController(R.layout.controller_settings) {
             resources!!.getString(R.string.nc_settings)
 
     private fun getCurrentUser() {
-        currentUser = userUtils.currentUser
+        currentUser = userRepository.getActiveUser()
         credentials = ApiUtils.getCredentials(currentUser!!.username, currentUser!!.token)
     }
 
@@ -184,7 +190,7 @@ class SettingsController : NewBaseController(R.layout.controller_settings) {
     }
 
     private fun setupPhoneBookIntegration() {
-        if (CapabilitiesUtil.isPhoneBookIntegrationAvailable(userUtils.currentUser)) {
+        if (CapabilitiesUtil.isPhoneBookIntegrationAvailable(userRepository.getActiveUser())) {
             binding.settingsPhoneBookIntegration.visibility = View.VISIBLE
         } else {
             binding.settingsPhoneBookIntegration.visibility = View.GONE

+ 8 - 1
app/src/main/java/com/nextcloud/talk/dagger/modules/DatabaseModule.java

@@ -24,6 +24,7 @@ package com.nextcloud.talk.dagger.modules;
 import android.content.Context;
 import androidx.annotation.NonNull;
 import com.nextcloud.talk.R;
+import com.nextcloud.talk.data.source.local.TalkDatabase;
 import com.nextcloud.talk.models.database.Models;
 import com.nextcloud.talk.utils.preferences.AppPreferences;
 import dagger.Module;
@@ -54,7 +55,7 @@ public class DatabaseModule {
                 .toLowerCase()
                 .replace(" ", "_")
                 .trim()
-                + ".sqlite",
+                + ".sqlite_off",
             context.getString(R.string.nc_talk_database_encryption_key),
             DB_VERSION);
     }
@@ -74,4 +75,10 @@ public class DatabaseModule {
         preferences.removeLinkPreviews();
         return preferences;
     }
+
+    @Provides
+    @Singleton
+    public TalkDatabase provideTalkDatabase(@NonNull final Context context) {
+        return TalkDatabase.Companion.getInstance(context);
+    }
 }

+ 15 - 0
app/src/main/java/com/nextcloud/talk/dagger/modules/RepositoryModule.kt

@@ -22,6 +22,11 @@
 package com.nextcloud.talk.dagger.modules
 
 import com.nextcloud.talk.api.NcApi
+import com.nextcloud.talk.data.source.local.TalkDatabase
+import com.nextcloud.talk.data.storage.ArbitraryStoragesRepository
+import com.nextcloud.talk.data.storage.ArbitraryStoragesRepositoryImpl
+import com.nextcloud.talk.data.user.UsersRepository
+import com.nextcloud.talk.data.user.UsersRepositoryImpl
 import com.nextcloud.talk.remotefilebrowser.repositories.RemoteFileBrowserItemsRepository
 import com.nextcloud.talk.remotefilebrowser.repositories.RemoteFileBrowserItemsRepositoryImpl
 import com.nextcloud.talk.repositories.unifiedsearch.UnifiedSearchRepository
@@ -50,4 +55,14 @@ class RepositoryModule {
         RemoteFileBrowserItemsRepository {
         return RemoteFileBrowserItemsRepositoryImpl(okHttpClient, userProvider)
     }
+
+    @Provides
+    fun provideUsersRepository(database : TalkDatabase): UsersRepository {
+        return UsersRepositoryImpl(database.usersDao())
+    }
+
+    @Provides
+    fun provideArbitraryStoragesRepository(database : TalkDatabase): ArbitraryStoragesRepository {
+        return ArbitraryStoragesRepositoryImpl(database.arbitraryStoragesDao())
+    }
 }

+ 62 - 0
app/src/main/java/com/nextcloud/talk/data/source/local/Migrations.kt

@@ -0,0 +1,62 @@
+package com.nextcloud.talk.data.source.local
+
+import androidx.room.migration.Migration
+import androidx.sqlite.db.SupportSQLiteDatabase
+
+object Migrations {
+    val MIGRATION_7_8 = object : Migration(7, 8) {
+        override fun migrate(database: SupportSQLiteDatabase) {
+            // Create the new tables
+            database.execSQL(
+                "CREATE TABLE User_new (" +
+                    "id INTEGER NOT NULL, " +
+                    "userId TEXT, " +
+                    "username TEXT, " +
+                    "baseUrl TEXT, " +
+                    "token TEXT, " +
+                    "displayName TEXT, " +
+                    "pushConfigurationState TEXT, " +
+                    "capabilities TEXT, " +
+                    "clientCertificate TEXT, " +
+                    "externalSignalingServer TEXT, " +
+                    "current INTEGER NOT NULL, " +
+                    "scheduledForDeletion INTEGER NOT NULL, " +
+                    "PRIMARY KEY(id)" +
+                    ")"
+            )
+            database.execSQL(
+                "CREATE TABLE ArbitraryStorage_new (" +
+                    "accountIdentifier INTEGER NOT NULL, " +
+                    "\"key\" TEXT, " +
+                    "object TEXT, " +
+                    "value TEXT, " +
+                    "PRIMARY KEY(accountIdentifier)" +
+                    ")"
+            )
+            // Copy the data
+            database.execSQL(
+                "INSERT INTO User_new (" +
+                    "id, userId, username, baseUrl, token, displayName, pushConfigurationState, capabilities, " +
+                    "clientCertificate, externalSignalingServer, current, scheduledForDeletion) " +
+                    "SELECT " +
+                    "id, userId, username, baseUrl, token, displayName, pushConfigurationState, capabilities, " +
+                    "clientCertificate, externalSignalingServer, current, scheduledForDeletion " +
+                    "FROM User"
+            )
+            database.execSQL(
+                "INSERT INTO ArbitraryStorage_new (" +
+                    "accountIdentifier, \"key\", object, value) " +
+                    "SELECT " +
+                    "accountIdentifier, \"key\", object, value " +
+                    "FROM ArbitraryStorage"
+            )
+            // Remove the old table
+            database.execSQL("DROP TABLE User")
+            database.execSQL("DROP TABLE ArbitraryStorage")
+
+            // Change the table name to the correct one
+            database.execSQL("ALTER TABLE User_new RENAME TO User")
+            database.execSQL("ALTER TABLE ArbitraryStorage_new RENAME TO ArbitraryStorage")
+        }
+    }
+}

+ 37 - 13
app/src/main/java/com/nextcloud/talk/data/source/local/TalkDatabase.kt

@@ -26,31 +26,38 @@ import androidx.room.Room
 import androidx.room.RoomDatabase
 import androidx.room.TypeConverters
 import androidx.sqlite.db.SupportSQLiteDatabase
+import com.nextcloud.talk.R
 import com.nextcloud.talk.data.source.local.converters.CapabilitiesConverter
+import com.nextcloud.talk.data.source.local.converters.ExternalSignalingServerConverter
 import com.nextcloud.talk.data.source.local.converters.HashMapHashMapConverter
 import com.nextcloud.talk.data.source.local.converters.PushConfigurationConverter
 import com.nextcloud.talk.data.source.local.converters.SignalingSettingsConverter
+import com.nextcloud.talk.data.storage.ArbitraryStoragesDao
+import com.nextcloud.talk.data.storage.model.ArbitraryStorageNgEntity
 import com.nextcloud.talk.data.user.UsersDao
 import com.nextcloud.talk.data.user.model.UserNgEntity
+import net.sqlcipher.database.SQLiteDatabase
+import net.sqlcipher.database.SupportFactory
+import java.util.Locale
 
 @Database(
-    entities = [UserNgEntity::class],
-    version = 1,
+    entities = [UserNgEntity::class, ArbitraryStorageNgEntity::class],
+    version = 8,
     exportSchema = true
 )
 @TypeConverters(
     PushConfigurationConverter::class,
     CapabilitiesConverter::class,
+    ExternalSignalingServerConverter::class,
     SignalingSettingsConverter::class,
     HashMapHashMapConverter::class
 )
-
 abstract class TalkDatabase : RoomDatabase() {
 
     abstract fun usersDao(): UsersDao
+    abstract fun arbitraryStoragesDao(): ArbitraryStoragesDao
 
     companion object {
-        private const val DB_NAME = "talk.db"
 
         @Volatile
         private var INSTANCE: TalkDatabase? = null
@@ -60,15 +67,32 @@ abstract class TalkDatabase : RoomDatabase() {
                 INSTANCE ?: build(context).also { INSTANCE = it }
             }
 
-        private fun build(context: Context) =
-            Room.databaseBuilder(context.applicationContext, TalkDatabase::class.java, DB_NAME)
-                .fallbackToDestructiveMigration()
-                .addCallback(object : RoomDatabase.Callback() {
-                    override fun onOpen(db: SupportSQLiteDatabase) {
-                        super.onOpen(db)
-                        db.execSQL("PRAGMA defer_foreign_keys = 1")
-                    }
-                })
+        private fun build(context: Context): TalkDatabase {
+            val passCharArray = context.getString(R.string.nc_talk_database_encryption_key).toCharArray()
+            val passphrase: ByteArray = SQLiteDatabase.getBytes(passCharArray)
+            val factory = SupportFactory(passphrase)
+
+            val dbName = context
+                .resources
+                .getString(R.string.nc_app_product_name)
+                .lowercase(Locale.getDefault())
+                .replace(" ", "_")
+                .trim { it <= ' ' } +
+                ".sqlite"
+
+            return Room
+                .databaseBuilder(context.applicationContext, TalkDatabase::class.java, dbName)
+                .openHelperFactory(factory)
+                .addMigrations(Migrations.MIGRATION_7_8)
+                .allowMainThreadQueries()
+                .addCallback(
+                    object : RoomDatabase.Callback() {
+                        override fun onOpen(db: SupportSQLiteDatabase) {
+                            super.onOpen(db)
+                            db.execSQL("PRAGMA defer_foreign_keys = 1")
+                        }
+                    })
                 .build()
+        }
     }
 }

+ 47 - 0
app/src/main/java/com/nextcloud/talk/data/source/local/converters/ExternalSignalingServerConverter.kt

@@ -0,0 +1,47 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Andy Scherzinger
+ * Copyright (C) 2022 Andy Scherzinger <info@andy-scherzinger.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.data.source.local.converters
+
+import androidx.room.TypeConverter
+import com.nextcloud.talk.models.ExternalSignalingServer
+import com.nextcloud.talk.models.json.signaling.settings.SignalingSettings
+
+class ExternalSignalingServerConverter {
+    val json = JsonConfiguration.customJsonConfiguration
+
+    @TypeConverter
+    fun fromExternalSignalingServerToString(externalSignalingServer: ExternalSignalingServer?): String {
+        return if (externalSignalingServer == null) {
+            ""
+        } else {
+            json.encodeToString(ExternalSignalingServer.serializer(), externalSignalingServer)
+        }
+    }
+
+    @TypeConverter
+    fun fromStringToExternalSignalingServer(value: String): ExternalSignalingServer? {
+        if (value.isBlank()) {
+            return null
+        }
+
+        return json.decodeFromString(ExternalSignalingServer.serializer(), value)
+    }
+}

+ 44 - 0
app/src/main/java/com/nextcloud/talk/data/storage/ArbitraryStoragesDao.kt

@@ -0,0 +1,44 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Andy Scherzinger
+ * Copyright (C) 2022 Andy Scherzinger <infoi@andy-scherzinger.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.data.storage
+
+import androidx.room.Dao
+import androidx.room.Insert
+import androidx.room.OnConflictStrategy
+import androidx.room.Query
+import com.nextcloud.talk.data.storage.model.ArbitraryStorageNgEntity
+
+@Dao
+abstract class ArbitraryStoragesDao {
+    @Query(
+        "SELECT * FROM ArbitraryStorage WHERE " +
+            "accountIdentifier = :accountIdentifier AND " +
+            "\"key\" = :key AND " +
+            "object = :objectString"
+    )
+    abstract fun getStorageSetting(accountIdentifier: Long, key: String, objectString: String): ArbitraryStorageNgEntity
+
+    @Query("DELETE FROM ArbitraryStorage WHERE accountIdentifier = :accountIdentifier")
+    abstract suspend fun deleteArbitraryStorage(accountIdentifier: Long)
+
+    @Insert(onConflict = OnConflictStrategy.REPLACE)
+    abstract fun saveArbitraryStorage(arbitraryStorage: ArbitraryStorageNgEntity): Long
+}

+ 29 - 0
app/src/main/java/com/nextcloud/talk/data/storage/ArbitraryStoragesRepository.kt

@@ -0,0 +1,29 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Andy Scherzinger
+ * Copyright (C) 2022 Andy Scherzinger <info@andy-scherzinger.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.data.storage
+
+import com.nextcloud.talk.data.storage.model.ArbitraryStorageNgEntity
+
+interface ArbitraryStoragesRepository {
+    fun getStorageSetting(accountIdentifier: Long, key: String, objectString: String): ArbitraryStorageNgEntity
+    suspend fun deleteArbitraryStorage(accountIdentifier: Long)
+    fun saveArbitraryStorage(arbitraryStorage: ArbitraryStorageNgEntity): Long
+}

+ 42 - 0
app/src/main/java/com/nextcloud/talk/data/storage/ArbitraryStoragesRepositoryImpl.kt

@@ -0,0 +1,42 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Andy Scherzinger
+ * Copyright (C) 2022 Andy Scherzinger <info@andy-scherzinger.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.data.storage
+
+import com.nextcloud.talk.data.storage.model.ArbitraryStorageNgEntity
+
+class ArbitraryStoragesRepositoryImpl(private val arbitraryStoragesDao: ArbitraryStoragesDao) :
+    ArbitraryStoragesRepository {
+    override fun getStorageSetting(
+        accountIdentifier: Long,
+        key: String,
+        objectString: String
+    ): ArbitraryStorageNgEntity {
+        return arbitraryStoragesDao.getStorageSetting(accountIdentifier, key, objectString)
+    }
+
+    override suspend fun deleteArbitraryStorage(accountIdentifier: Long) {
+        arbitraryStoragesDao.deleteArbitraryStorage(accountIdentifier)
+    }
+
+    override fun saveArbitraryStorage(arbitraryStorage: ArbitraryStorageNgEntity): Long {
+        return arbitraryStoragesDao.saveArbitraryStorage(arbitraryStorage)
+    }
+}

+ 38 - 0
app/src/main/java/com/nextcloud/talk/data/storage/model/ArbitraryStorageNgEntity.kt

@@ -0,0 +1,38 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Andy Scherzinger
+ * Copyright (C) 2022 Andy Scherzinger <infoi@andy-scherzinger.de>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.data.storage.model
+
+import android.os.Parcelable
+import androidx.room.ColumnInfo
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+import kotlinx.android.parcel.Parcelize
+import kotlinx.serialization.Serializable
+
+@Parcelize
+@Serializable
+@Entity(tableName = "ArbitraryStorage")
+data class ArbitraryStorageNgEntity(
+    @PrimaryKey @ColumnInfo(name = "accountIdentifier") var accountIdentifier: Long = 0,
+    @ColumnInfo(name = "key") var key: String? = null,
+    @ColumnInfo(name = "object") var storageObject: String? = null,
+    @ColumnInfo(name = "value") var value: String? = null
+) : Parcelable

+ 27 - 10
app/src/main/java/com/nextcloud/talk/data/user/UsersDao.kt

@@ -2,6 +2,8 @@
  * Nextcloud Talk application
  *
  * @author Mario Danic
+ * @author Andy Scherzinger
+ * Copyright (C) 2022 Andy Scherzinger <info@andy-scherzinger.de>
  * Copyright (C) 2017-2020 Mario Danic <mario@lovelyhq.com>
  *
  * This program is free software: you can redistribute it and/or modify
@@ -34,19 +36,19 @@ import java.lang.Boolean.TRUE
 @Dao
 abstract class UsersDao {
     // get active user
-    @Query("SELECT * FROM users where current = 1")
+    @Query("SELECT * FROM User where current = 1")
     abstract fun getActiveUser(): UserNgEntity?
 
-    @Query("SELECT * FROM users WHERE current = 1")
+    @Query("SELECT * FROM User WHERE current = 1")
     abstract fun getActiveUserLiveData(): LiveData<UserNgEntity?>
 
-    @Query("SELECT * from users ORDER BY current DESC")
+    @Query("SELECT * FROM User ORDER BY current DESC")
     abstract fun getUsersLiveData(): LiveData<List<UserNgEntity>>
 
-    @Query("SELECT * from users WHERE current != 1 ORDER BY current DESC")
+    @Query("SELECT * FROM User WHERE current != 1 ORDER BY current DESC")
     abstract fun getUsersLiveDataWithoutActive(): LiveData<List<UserNgEntity>>
 
-    @Query("DELETE FROM users WHERE id = :id")
+    @Query("DELETE FROM User WHERE id = :id")
     abstract suspend fun deleteUserWithId(id: Long)
 
     @Update
@@ -59,16 +61,31 @@ abstract class UsersDao {
     abstract suspend fun saveUsers(vararg users: UserNgEntity): List<Long>
 
     // get all users not scheduled for deletion
-    @Query("SELECT * FROM users where current != 0")
+    @Query("SELECT * FROM User where current != 0")
     abstract fun getUsers(): List<UserNgEntity>
 
-    @Query("SELECT * FROM users where id = :id")
-    abstract fun getUserWithId(id: Long): UserNgEntity
+    @Query("SELECT * FROM User where id = :id")
+    abstract fun getUserWithId(id: Long): UserNgEntity?
 
-    @Query("SELECT * FROM users where current = 0")
+    @Query("SELECT * FROM User where id = :id")
+    abstract fun getUserWithIdLiveData(id: Long): LiveData<UserNgEntity?>
+
+    @Query("SELECT * FROM User where id = :id AND scheduledForDeletion != 1")
+    abstract fun getUserWithIdNotScheduledForDeletion(id: Long): UserNgEntity?
+
+    @Query("SELECT * FROM User where userId = :userId")
+    abstract fun getUserWithUserId(userId: String): UserNgEntity?
+
+    @Query("SELECT * FROM User where userId != :userId")
+    abstract fun getUsersWithoutUserId(userId: Long): List<UserNgEntity>
+
+    @Query("SELECT * FROM User where current = 0")
     abstract fun getUsersScheduledForDeletion(): List<UserNgEntity>
 
-    @Query("SELECT * FROM users WHERE username = :username AND base_url = :server")
+    @Query("SELECT * FROM User where scheduledForDeletion = 0")
+    abstract fun getUsersNotScheduledForDeletion(): List<UserNgEntity>
+
+    @Query("SELECT * FROM User WHERE username = :username AND baseUrl = :server")
     abstract suspend fun getUserWithUsernameAndServer(username: String, server: String): UserNgEntity?
 
     @Transaction

+ 49 - 0
app/src/main/java/com/nextcloud/talk/data/user/UsersRepository.kt

@@ -0,0 +1,49 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Mario Danic
+ * @author Andy Scherzinger
+ * Copyright (C) 2022 Andy Scherzinger <info@andy-scherzinger.de>
+ * Copyright (C) 2017-2020 Mario Danic <mario@lovelyhq.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.data.user
+
+import androidx.lifecycle.LiveData
+import com.nextcloud.talk.data.user.model.UserNgEntity
+import com.nextcloud.talk.data.user.model.User
+
+interface UsersRepository {
+    fun getActiveUserLiveData(): LiveData<UserNgEntity?>
+    fun getActiveUser(): UserNgEntity?
+    fun getUsers(): List<UserNgEntity>
+    fun getUserWithId(id: Long): UserNgEntity?
+    fun getUserWithIdLiveData(id: Long): LiveData<UserNgEntity?>
+    fun getUserWithIdNotScheduledForDeletion(id: Long): UserNgEntity?
+    fun getUserWithUserId(userId: String): UserNgEntity?
+    fun getUsersWithoutUserId(userId: Long): List<UserNgEntity>
+    fun getUsersLiveData(): LiveData<List<User>>
+    fun getUsersLiveDataWithoutActive(): LiveData<List<User>>
+    fun getUsersScheduledForDeletion(): List<UserNgEntity>
+    fun getUsersNotScheduledForDeletion(): List<UserNgEntity>
+    suspend fun getUserWithUsernameAndServer(username: String, server: String): UserNgEntity?
+    suspend fun updateUser(user: UserNgEntity): Int
+    suspend fun insertUser(user: UserNgEntity): Long
+    suspend fun setUserAsActiveWithId(id: Long): Boolean
+    suspend fun deleteUserWithId(id: Long)
+    suspend fun setAnyUserAsActive(): Boolean
+    suspend fun markUserForDeletion(id: Long): Boolean
+}

+ 119 - 0
app/src/main/java/com/nextcloud/talk/data/user/UsersRepositoryImpl.kt

@@ -0,0 +1,119 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Mario Danic
+ * @author Andy Scherzinger
+ * Copyright (C) 2022 Andy Scherzinger <info@andy-scherzinger.de>
+ * Copyright (C) 2017-2020 Mario Danic <mario@lovelyhq.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.nextcloud.talk.data.user
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.distinctUntilChanged
+import androidx.lifecycle.map
+import com.nextcloud.talk.data.user.model.UserNgEntity
+import com.nextcloud.talk.data.user.model.User
+import com.nextcloud.talk.data.user.model.toUser
+
+class UsersRepositoryImpl(private val usersDao: UsersDao) : UsersRepository {
+    override fun getActiveUserLiveData(): LiveData<UserNgEntity?> {
+        return usersDao.getActiveUserLiveData().distinctUntilChanged()
+    }
+
+    override fun getActiveUser(): UserNgEntity? {
+        return usersDao.getActiveUser()
+    }
+
+    override fun getUsers(): List<UserNgEntity> {
+        return usersDao.getUsers()
+    }
+
+    override fun getUserWithId(id: Long): UserNgEntity? {
+        return usersDao.getUserWithId(id)
+    }
+
+    override fun getUserWithIdLiveData(id: Long): LiveData<UserNgEntity?> {
+        return usersDao.getUserWithIdLiveData(id).distinctUntilChanged()
+    }
+
+    override fun getUserWithIdNotScheduledForDeletion(id: Long): UserNgEntity? {
+        return usersDao.getUserWithIdNotScheduledForDeletion(id)
+    }
+
+    override fun getUserWithUserId(userId: String): UserNgEntity? {
+        return usersDao.getUserWithUserId(userId)
+    }
+
+    override fun getUsersWithoutUserId(userId: Long): List<UserNgEntity> {
+        return usersDao.getUsersWithoutUserId(userId)
+    }
+
+    override fun getUsersLiveData(): LiveData<List<User>> {
+        return usersDao.getUsersLiveData().distinctUntilChanged().map { usersList ->
+            usersList.map {
+                it.toUser()
+            }
+        }
+    }
+
+    override fun getUsersLiveDataWithoutActive(): LiveData<List<User>> {
+        return usersDao.getUsersLiveDataWithoutActive().distinctUntilChanged().map { usersList ->
+            usersList.map {
+                it.toUser()
+            }
+        }
+    }
+
+    override fun getUsersScheduledForDeletion(): List<UserNgEntity> {
+        return usersDao.getUsersScheduledForDeletion()
+    }
+
+    override fun getUsersNotScheduledForDeletion(): List<UserNgEntity> {
+        return usersDao.getUsersNotScheduledForDeletion()
+    }
+
+    override suspend fun getUserWithUsernameAndServer(
+        username: String,
+        server: String
+    ): UserNgEntity? {
+        return usersDao.getUserWithUsernameAndServer(username, server)
+    }
+
+    override suspend fun updateUser(user: UserNgEntity): Int {
+        return usersDao.updateUser(user)
+    }
+
+    override suspend fun insertUser(user: UserNgEntity): Long {
+        return usersDao.saveUser(user)
+    }
+
+    override suspend fun setUserAsActiveWithId(id: Long): Boolean {
+        return usersDao.setUserAsActiveWithId(id)
+    }
+
+    override suspend fun deleteUserWithId(id: Long) {
+        usersDao.deleteUserWithId(id)
+    }
+
+    override suspend fun setAnyUserAsActive(): Boolean {
+        return usersDao.setAnyUserAsActive()
+    }
+
+    override suspend fun markUserForDeletion(id: Long): Boolean {
+        return usersDao.markUserForDeletion(id)
+    }
+}

+ 11 - 9
app/src/main/java/com/nextcloud/talk/data/user/model/User.kt

@@ -20,28 +20,30 @@
 package com.nextcloud.talk.data.user.model
 
 import android.os.Parcelable
+import com.nextcloud.talk.models.ExternalSignalingServer
 import com.nextcloud.talk.models.json.capabilities.Capabilities
 import com.nextcloud.talk.models.json.push.PushConfigurationState
 import com.nextcloud.talk.models.json.signaling.settings.SignalingSettings
 import com.nextcloud.talk.utils.ApiUtils
 import kotlinx.android.parcel.Parcelize
 import kotlinx.serialization.Serializable
+import java.lang.Boolean.FALSE
 
 @Parcelize
 @Serializable
 data class User(
     var id: Long? = null,
-    var userId: String,
-    var username: String,
-    var baseUrl: String,
+    var userId: String? = null,
+    var username: String? = null,
+    var baseUrl: String? = null,
     var token: String? = null,
     var displayName: String? = null,
-    var pushConfiguration: PushConfigurationState? = null,
+    var pushConfigurationState: PushConfigurationState? = null,
     var capabilities: Capabilities? = null,
     var clientCertificate: String? = null,
-    var signalingSettings: SignalingSettings? = null,
-    var current: Boolean = java.lang.Boolean.FALSE,
-    var scheduledForDeletion: Boolean = java.lang.Boolean.FALSE
+    var externalSignalingServer: ExternalSignalingServer? = null,
+    var current: Boolean = FALSE,
+    var scheduledForDeletion: Boolean = FALSE,
 ) : Parcelable
 
 fun User.getMaxMessageLength(): Int {
@@ -76,10 +78,10 @@ fun User.toUserEntity(): UserNgEntity {
 
     userNgEntity!!.token = this.token
     userNgEntity!!.displayName = this.displayName
-    userNgEntity!!.pushConfiguration = this.pushConfiguration
+    userNgEntity!!.pushConfigurationState = this.pushConfigurationState
     userNgEntity!!.capabilities = this.capabilities
     userNgEntity!!.clientCertificate = this.clientCertificate
-    userNgEntity!!.externalSignalingServer = this.signalingSettings
+    userNgEntity!!.externalSignalingServer = this.externalSignalingServer
     userNgEntity!!.current = this.current
     userNgEntity!!.scheduledForDeletion = this.scheduledForDeletion
 

+ 17 - 14
app/src/main/java/com/nextcloud/talk/data/user/model/UserNgEntity.kt

@@ -1,7 +1,9 @@
 /*
  * Nextcloud Talk application
  *
+ * @author Andy Scherzinger
  * @author Mario Danic
+ * Copyright (C) 2022 Andy Scherzinger <infoi@andy-scherzinger.de>
  * Copyright (C) 2017-2020 Mario Danic <mario@lovelyhq.com>
  *
  * This program is free software: you can redistribute it and/or modify
@@ -24,9 +26,9 @@ import android.os.Parcelable
 import androidx.room.ColumnInfo
 import androidx.room.Entity
 import androidx.room.PrimaryKey
+import com.nextcloud.talk.models.ExternalSignalingServer
 import com.nextcloud.talk.models.json.capabilities.Capabilities
 import com.nextcloud.talk.models.json.push.PushConfigurationState
-import com.nextcloud.talk.models.json.signaling.settings.SignalingSettings
 import com.nextcloud.talk.utils.ApiUtils
 import kotlinx.android.parcel.Parcelize
 import kotlinx.serialization.Serializable
@@ -34,25 +36,24 @@ import java.lang.Boolean.FALSE
 
 @Parcelize
 @Serializable
-@Entity(tableName = "users")
+@Entity(tableName = "User")
 data class UserNgEntity(
     @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") var id: Long = 0,
-    @ColumnInfo(name = "user_id") var userId: String,
-    @ColumnInfo(name = "username") var username: String,
-    @ColumnInfo(name = "base_url") var baseUrl: String,
+    @ColumnInfo(name = "userId") var userId: String? = null,
+    @ColumnInfo(name = "username") var username: String? = null,
+    @ColumnInfo(name = "baseUrl") var baseUrl: String? = null,
     @ColumnInfo(name = "token") var token: String? = null,
-    @ColumnInfo(name = "display_name") var displayName: String? = null,
-    @ColumnInfo(name = "push_configuration_state") var pushConfiguration: PushConfigurationState? = null,
+    @ColumnInfo(name = "displayName") var displayName: String? = null,
+    @ColumnInfo(name = "pushConfigurationState") var pushConfigurationState: PushConfigurationState? = null,
     @ColumnInfo(name = "capabilities") var capabilities: Capabilities? = null,
-    @ColumnInfo(name = "client_certificate") var clientCertificate: String? = null,
-    @ColumnInfo(name = "external_signaling_server") var externalSignalingServer: SignalingSettings? = null,
+    @ColumnInfo(name = "clientCertificate") var clientCertificate: String? = null,
+    @ColumnInfo(name = "externalSignalingServer") var externalSignalingServer: ExternalSignalingServer? = null,
     @ColumnInfo(name = "current") var current: Boolean = FALSE,
-    @ColumnInfo(name = "scheduled_for_deletion") var scheduledForDeletion: Boolean = FALSE,
+    @ColumnInfo(name = "scheduledForDeletion") var scheduledForDeletion: Boolean = FALSE,
 ) : Parcelable {
 
     fun hasSpreedFeatureCapability(capabilityName: String): Boolean {
         return capabilities?.spreedCapability?.features?.contains(capabilityName) ?: false
-
     }
 }
 
@@ -65,9 +66,11 @@ fun UserNgEntity.canUserCreateGroupConversations(): Boolean {
 }
 
 fun UserNgEntity.toUser(): User {
-    return User(this.id, this.userId, this.username, this.baseUrl, this.token, this.displayName, this
-        .pushConfiguration, this.capabilities, this.clientCertificate, this.externalSignalingServer, this.current,
-        this.scheduledForDeletion)
+    return User(
+        this.id, this.userId, this.username, this.baseUrl, this.token, this.displayName, this.pushConfigurationState,
+        this.capabilities, this.clientCertificate, this.externalSignalingServer, this.current,
+        this.scheduledForDeletion
+    )
 }
 
 fun UserNgEntity.getCredentials(): String = ApiUtils.getCredentials(username, token)

+ 2 - 0
app/src/main/java/com/nextcloud/talk/models/ExternalSignalingServer.kt

@@ -25,9 +25,11 @@ import android.os.Parcelable
 import com.bluelinelabs.logansquare.annotation.JsonField
 import com.bluelinelabs.logansquare.annotation.JsonObject
 import kotlinx.android.parcel.Parcelize
+import kotlinx.serialization.Serializable
 
 @Parcelize
 @JsonObject
+@Serializable
 data class ExternalSignalingServer(
     @JsonField(name = ["externalSignalingServer"])
     var externalSignalingServer: String? = null,

+ 57 - 0
app/src/main/java/com/nextcloud/talk/models/database/CapabilitiesUtil.java

@@ -24,6 +24,7 @@ package com.nextcloud.talk.models.database;
 import android.util.Log;
 
 import com.bluelinelabs.logansquare.LoganSquare;
+import com.nextcloud.talk.data.user.model.UserNgEntity;
 import com.nextcloud.talk.models.json.capabilities.Capabilities;
 
 import java.io.IOException;
@@ -66,11 +67,25 @@ public abstract class CapabilitiesUtil {
         return false;
     }
 
+    @Deprecated
+    public static boolean isServerEOL(@Nullable UserNgEntity user) {
+        // Capability is available since Talk 4 => Nextcloud 14 => Autmn 2018
+        return !hasSpreedFeatureCapability(user, "no-ping");
+    }
+
+    @Deprecated
+    public static boolean isServerAlmostEOL(@Nullable UserNgEntity user) {
+        // Capability is available since Talk 8 => Nextcloud 18 => January 2020
+        return !hasSpreedFeatureCapability(user, "chat-replies");
+    }
+
+    @Deprecated
     public static boolean isServerEOL(@Nullable UserEntity user) {
         // Capability is available since Talk 4 => Nextcloud 14 => Autmn 2018
         return !hasSpreedFeatureCapability(user, "no-ping");
     }
 
+    @Deprecated
     public static boolean isServerAlmostEOL(@Nullable UserEntity user) {
         // Capability is available since Talk 8 => Nextcloud 18 => January 2020
         return !hasSpreedFeatureCapability(user, "chat-replies");
@@ -80,6 +95,18 @@ public abstract class CapabilitiesUtil {
         return hasSpreedFeatureCapability(user, "chat-read-marker");
     }
 
+    public static boolean hasSpreedFeatureCapability(@Nullable UserNgEntity user, String capabilityName) {
+        if (user != null && user.getCapabilities() != null) {
+            Capabilities capabilities = user.getCapabilities();
+            if (capabilities != null && capabilities.getSpreedCapability() != null &&
+                capabilities.getSpreedCapability().getFeatures() != null) {
+                return capabilities.getSpreedCapability().getFeatures().contains(capabilityName);
+            }
+        }
+        return false;
+    }
+
+    @Deprecated
     public static boolean hasSpreedFeatureCapability(@Nullable UserEntity user, String capabilityName) {
         if (user != null && user.getCapabilities() != null) {
             try {
@@ -123,6 +150,7 @@ public abstract class CapabilitiesUtil {
         return 1000;
     }
 
+    @Deprecated
     public static boolean isPhoneBookIntegrationAvailable(@Nullable UserEntity user) {
         if (user != null && user.getCapabilities() != null) {
             try {
@@ -138,6 +166,17 @@ public abstract class CapabilitiesUtil {
         return false;
     }
 
+    public static boolean isPhoneBookIntegrationAvailable(@Nullable UserNgEntity user) {
+        if (user != null && user.getCapabilities() != null) {
+            Capabilities capabilities = user.getCapabilities();
+            return capabilities != null &&
+                capabilities.getSpreedCapability() != null &&
+                capabilities.getSpreedCapability().getFeatures() != null &&
+                capabilities.getSpreedCapability().getFeatures().contains("phonebook-search");
+        }
+        return false;
+    }
+
     public static boolean isReadStatusAvailable(@Nullable UserEntity user) {
         if (user != null && user.getCapabilities() != null) {
             try {
@@ -156,6 +195,7 @@ public abstract class CapabilitiesUtil {
         return false;
     }
 
+    @Deprecated
     public static boolean isReadStatusPrivate(@Nullable UserEntity user) {
         if (user != null && user.getCapabilities() != null) {
             try {
@@ -176,6 +216,22 @@ public abstract class CapabilitiesUtil {
         return false;
     }
 
+    public static boolean isReadStatusPrivate(@Nullable UserNgEntity user) {
+        if (user != null && user.getCapabilities() != null) {
+            Capabilities capabilities = user.getCapabilities();
+            if (capabilities != null &&
+                capabilities.getSpreedCapability() != null &&
+                capabilities.getSpreedCapability().getConfig() != null &&
+                capabilities.getSpreedCapability().getConfig().containsKey("chat")) {
+                HashMap<String, String> map = capabilities.getSpreedCapability().getConfig().get("chat");
+                if (map != null && map.containsKey("read-privacy")) {
+                    return Integer.parseInt(map.get("read-privacy")) == 1;
+                }
+            }
+        }
+        return false;
+    }
+
     public static boolean isUserStatusAvailable(@Nullable UserEntity user) {
         if (user != null && user.getCapabilities() != null) {
             try {
@@ -280,6 +336,7 @@ public abstract class CapabilitiesUtil {
         return false;
     }
 
+    @Deprecated
     private static Capabilities parseUserCapabilities(@NonNull final UserEntity user) throws IOException {
         return LoganSquare.parse(user.getCapabilities(), Capabilities.class);
     }

+ 1 - 0
app/src/main/java/com/nextcloud/talk/models/database/User.java

@@ -29,6 +29,7 @@ import io.requery.Entity;
 import io.requery.Generated;
 import io.requery.Key;
 import io.requery.Persistable;
+import io.requery.Table;
 
 @Entity
 public interface User extends Parcelable, Persistable, Serializable {

+ 204 - 0
app/src/main/java/com/nextcloud/talk/users/UserManager.kt

@@ -0,0 +1,204 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Mario Danic
+ * @author Andy Scherzinger
+ * Copyright (C) 2022 Andy Scherzinger <info@andy-scherzinger.de>
+ * Copyright (C) 2017 Mario Danic <mario@lovelyhq.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.nextcloud.talk.users
+
+import android.text.TextUtils
+import androidx.lifecycle.LiveData
+import com.bluelinelabs.logansquare.LoganSquare
+import com.nextcloud.talk.data.user.UsersRepository
+import com.nextcloud.talk.data.user.model.UserNgEntity
+import com.nextcloud.talk.models.ExternalSignalingServer
+import com.nextcloud.talk.models.json.capabilities.Capabilities
+import com.nextcloud.talk.models.json.push.PushConfigurationState
+import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew
+
+class UserManager internal constructor(private val userRepository: UsersRepository) : CurrentUserProviderNew {
+    fun anyUserExists(): Boolean {
+        return userRepository.getUsers().isNotEmpty()
+    }
+
+    fun hasMultipleUsers(): Boolean {
+        return userRepository.getUsers().size > 1
+    }
+
+    val users: List<UserNgEntity>
+        get() = userRepository.getUsers()
+
+    val usersScheduledForDeletion: List<UserNgEntity>
+        get() = userRepository.getUsersScheduledForDeletion()
+
+    suspend fun setAnyUserAndSetAsActive(): UserNgEntity? {
+        val results = userRepository.getUsersNotScheduledForDeletion()
+        if (results.isNotEmpty()) {
+            val UserNgEntity = results[0]
+            UserNgEntity.current = true
+            userRepository.updateUser(UserNgEntity)
+            return UserNgEntity
+        }
+        return null
+    }
+
+    override val currentUser: UserNgEntity?
+        get() {
+            return userRepository.getActiveUser()
+        }
+
+    suspend fun deleteUser(internalId: Long) {
+        userRepository.deleteUserWithId(internalId)
+    }
+
+    suspend fun deleteUserWithId(internalId: Long) {
+        userRepository.deleteUserWithId(internalId)
+    }
+
+    fun getUserById(userId: String): UserNgEntity? {
+        return userRepository.getUserWithUserId(userId)
+    }
+
+    fun getUserWithId(id: Long): UserNgEntity? {
+        return userRepository.getUserWithId(id)
+    }
+
+    suspend fun disableAllUsersWithoutId(userId: Long) {
+        val results = userRepository.getUsersWithoutUserId(userId)
+        if (results.isNotEmpty()) {
+            for (entity in results) {
+                entity.current = false
+                userRepository.updateUser(entity)
+            }
+        }
+    }
+
+    suspend fun checkIfUserIsScheduledForDeletion(username: String, server: String): Boolean {
+        val results = userRepository.getUserWithUsernameAndServer(username, server)
+        return results?.scheduledForDeletion ?: false
+    }
+
+    fun getUserWithInternalId(id: Long): UserNgEntity? {
+        return userRepository.getUserWithIdNotScheduledForDeletion(id)
+    }
+
+    suspend fun getIfUserWithUsernameAndServer(username: String, server: String): Boolean {
+        return userRepository.getUserWithUsernameAndServer(username, server) != null
+    }
+
+    suspend fun scheduleUserForDeletionWithId(id: Long): Boolean {
+        val result = userRepository.getUserWithId(id)
+
+        if (result != null) {
+            result.scheduledForDeletion = true
+            result.current = false
+            userRepository.updateUser(result)
+        }
+
+        return setAnyUserAndSetAsActive() != null
+    }
+
+    suspend fun createOrUpdateUser(
+        username: String?, token: String?,
+        serverUrl: String?,
+        displayName: String?,
+        pushConfigurationState: String?,
+        currentUser: Boolean?,
+        userId: String?,
+        internalId: Long?,
+        capabilities: String?,
+        certificateAlias: String?,
+        externalSignalingServer: String?
+    ): LiveData<UserNgEntity?> {
+        var user = if (internalId == null && username != null && serverUrl != null) {
+            userRepository.getUserWithUsernameAndServer(username, serverUrl)
+        } else if (internalId != null) {
+            userRepository.getUserWithId(internalId)
+        } else {
+            null
+        }
+
+        if (user == null) {
+            user = UserNgEntity()
+            user.baseUrl = serverUrl
+            user.username = username
+            user.token = token
+            if (!TextUtils.isEmpty(displayName)) {
+                user.displayName = displayName
+            }
+            if (pushConfigurationState != null) {
+                user.pushConfigurationState = LoganSquare
+                    .parse(pushConfigurationState, PushConfigurationState::class.java)
+            }
+            if (!TextUtils.isEmpty(userId)) {
+                user.userId = userId
+            }
+            if (!TextUtils.isEmpty(capabilities)) {
+                user.capabilities = LoganSquare.parse(capabilities, Capabilities::class.java)
+            }
+            if (!TextUtils.isEmpty(certificateAlias)) {
+                user.clientCertificate = certificateAlias
+            }
+            if (!TextUtils.isEmpty(externalSignalingServer)) {
+                user.externalSignalingServer = LoganSquare
+                    .parse(externalSignalingServer, ExternalSignalingServer::class.java)
+            }
+            user.current = true
+        } else {
+            if (userId != null && (user.userId == null || user.userId != userId)) {
+                user.userId = userId
+            }
+            if (token != null && token != user.token) {
+                user.token = token
+            }
+            if (displayName != null && user.displayName == null || displayName != null && (user.displayName
+                    != null) && displayName != user.displayName
+            ) {
+                user.displayName = displayName
+            }
+            if (pushConfigurationState != null) {
+                val newPushConfigurationState = LoganSquare
+                    .parse(pushConfigurationState, PushConfigurationState::class.java)
+                if (newPushConfigurationState != user.pushConfigurationState) {
+                    user.pushConfigurationState = newPushConfigurationState
+                }
+            }
+            if (capabilities != null) {
+                val newCapabilities = LoganSquare.parse(capabilities, Capabilities::class.java)
+                if (newCapabilities != user.capabilities) {
+                    user.capabilities = newCapabilities
+                }
+            }
+            if (certificateAlias != null && certificateAlias != user.clientCertificate) {
+                user.clientCertificate = certificateAlias
+            }
+            if (externalSignalingServer != null) {
+                val newExternalSignalingServer = LoganSquare
+                    .parse(externalSignalingServer, ExternalSignalingServer::class.java)
+                if (newExternalSignalingServer != user.externalSignalingServer) {
+                    user.externalSignalingServer = newExternalSignalingServer
+                }
+            }
+            if (currentUser != null) {
+                user.current = currentUser
+            }
+        }
+        userRepository.insertUser(user)
+        return userRepository.getUserWithIdLiveData(user.id)
+    }
+}

+ 33 - 0
app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java

@@ -28,6 +28,7 @@ import android.util.Log;
 import com.nextcloud.talk.BuildConfig;
 import com.nextcloud.talk.R;
 import com.nextcloud.talk.application.NextcloudTalkApplication;
+import com.nextcloud.talk.data.user.model.UserNgEntity;
 import com.nextcloud.talk.models.RetrofitBucket;
 import com.nextcloud.talk.models.database.CapabilitiesUtil;
 import com.nextcloud.talk.models.database.UserEntity;
@@ -123,6 +124,38 @@ public class ApiUtils {
         return getConversationApiVersion(capabilities, versions);
     }
 
+    public static int getConversationApiVersion(UserNgEntity user, int[] versions) throws NoSupportedApiException {
+        boolean hasApiV4 = false;
+        for (int version : versions) {
+            hasApiV4 |= version == APIv4;
+        }
+
+        if (!hasApiV4) {
+            Exception e = new Exception("Api call did not try conversation-v4 api");
+            Log.d(TAG, e.getMessage(), e);
+        }
+
+        for (int version : versions) {
+            if (user.hasSpreedFeatureCapability("conversation-v" + version)) {
+                return version;
+            }
+
+            // Fallback for old API versions
+            if ((version == APIv1 || version == APIv2)) {
+                if (user.hasSpreedFeatureCapability("conversation-v2")) {
+                    return version;
+                }
+                if (version == APIv1  &&
+                    user.hasSpreedFeatureCapability("mention-flag") &&
+                    !user.hasSpreedFeatureCapability("conversation-v4")) {
+                    return version;
+                }
+            }
+        }
+        throw new NoSupportedApiException();
+    }
+
+    @Deprecated
     public static int getConversationApiVersion(UserEntity user, int[] versions) throws NoSupportedApiException {
         boolean hasApiV4 = false;
         for (int version : versions) {

+ 30 - 0
app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.java

@@ -79,6 +79,7 @@ import com.facebook.widget.text.span.BetterImageSpan;
 import com.google.android.material.chip.ChipDrawable;
 import com.nextcloud.talk.R;
 import com.nextcloud.talk.application.NextcloudTalkApplication;
+import com.nextcloud.talk.data.user.model.UserNgEntity;
 import com.nextcloud.talk.events.UserMentionClickEvent;
 import com.nextcloud.talk.models.database.UserEntity;
 import com.nextcloud.talk.utils.text.Spans;
@@ -565,6 +566,7 @@ public class DisplayUtils {
         }
     }
 
+    @Deprecated
     public static void loadAvatarImage(UserEntity user, SimpleDraweeView avatarImageView, boolean deleteCache) {
         String avatarId;
         if (!TextUtils.isEmpty(user.getUserId())) {
@@ -593,6 +595,34 @@ public class DisplayUtils {
         avatarImageView.setController(draweeController);
     }
 
+    public static void loadAvatarImage(UserNgEntity user, SimpleDraweeView avatarImageView, boolean deleteCache) {
+        String avatarId;
+        if (!TextUtils.isEmpty(user.getUserId())) {
+            avatarId = user.getUserId();
+        } else {
+            avatarId = user.getUsername();
+        }
+
+        String avatarString = ApiUtils.getUrlForAvatar(user.getBaseUrl(), avatarId, true);
+
+        // clear cache
+        if (deleteCache) {
+            Uri avatarUri = Uri.parse(avatarString);
+
+            ImagePipeline imagePipeline = Fresco.getImagePipeline();
+            imagePipeline.evictFromMemoryCache(avatarUri);
+            imagePipeline.evictFromDiskCache(avatarUri);
+            imagePipeline.evictFromCache(avatarUri);
+        }
+
+        DraweeController draweeController = Fresco.newDraweeControllerBuilder()
+            .setOldController(avatarImageView.getController())
+            .setAutoPlayAnimations(true)
+            .setImageRequest(DisplayUtils.getImageRequestForUrl(avatarString, null))
+            .build();
+        avatarImageView.setController(draweeController);
+    }
+
     public static void loadAvatarPlaceholder(final SimpleDraweeView targetView) {
         final Context context = targetView.getContext();
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {

+ 27 - 0
app/src/main/java/com/nextcloud/talk/utils/database/user/CurrentUserProviderNew.kt

@@ -0,0 +1,27 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Álvaro Brey
+ * Copyright (C) 2022 Álvaro Brey
+ * Copyright (C) 2022 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package com.nextcloud.talk.utils.database.user
+
+import com.nextcloud.talk.data.user.model.UserNgEntity
+
+interface CurrentUserProviderNew {
+    val currentUser: UserNgEntity?
+}

+ 5 - 7
app/src/main/java/com/nextcloud/talk/utils/preferences/preferencestorage/DatabaseStorageModule.java

@@ -26,7 +26,6 @@ import android.os.Bundle;
 import android.text.TextUtils;
 import android.util.Log;
 
-import autodagger.AutoInjector;
 import com.nextcloud.talk.api.NcApi;
 import com.nextcloud.talk.application.NextcloudTalkApplication;
 import com.nextcloud.talk.models.database.ArbitraryStorageEntity;
@@ -35,21 +34,20 @@ import com.nextcloud.talk.models.database.UserEntity;
 import com.nextcloud.talk.models.json.generic.GenericOverall;
 import com.nextcloud.talk.utils.ApiUtils;
 import com.nextcloud.talk.utils.database.arbitrarystorage.ArbitraryStorageUtils;
-import com.nextcloud.talk.utils.database.user.UserUtils;
 import com.yarolegovich.mp.io.StorageModule;
 
 import org.jetbrains.annotations.NotNull;
 
+import java.util.Set;
+
+import javax.inject.Inject;
+
+import autodagger.AutoInjector;
 import io.reactivex.Observer;
 import io.reactivex.android.schedulers.AndroidSchedulers;
 import io.reactivex.disposables.Disposable;
 import io.reactivex.schedulers.Schedulers;
 
-import javax.inject.Inject;
-
-import java.util.Collections;
-import java.util.Set;
-
 @AutoInjector(NextcloudTalkApplication.class)
 public class DatabaseStorageModule implements StorageModule {
     private static final String TAG = "DatabaseStorageModule";