diff --git a/app/build.gradle b/app/build.gradle index 5b906e092..7524d347b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -33,6 +33,7 @@ apply plugin: 'kotlin-android-extensions' apply plugin: 'com.github.spotbugs' apply plugin: 'io.gitlab.arturbosch.detekt' apply plugin: "org.jlleitschuh.gradle.ktlint" +apply plugin: 'kotlinx-serialization' android { compileSdkVersion 31 @@ -80,6 +81,12 @@ android { disable 'VectorPath' } + javaCompileOptions { + annotationProcessorOptions { + arguments += ["room.schemaLocation": "$projectDir/schemas".toString()] + } + } + testInstrumentationRunnerArgument "TEST_SERVER_URL", "${NC_TEST_SERVER_BASEURL}" testInstrumentationRunnerArgument "TEST_SERVER_USERNAME", "${NC_TEST_SERVER_USERNAME}" testInstrumentationRunnerArgument "TEST_SERVER_PASSWORD", "${NC_TEST_SERVER_PASSWORD}" @@ -153,10 +160,12 @@ 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" retrofit2Version = "2.9.0" + roomVersion = "2.2.5" workVersion = "2.7.1" markwonVersion = "4.6.2" espressoVersion = "3.4.0" @@ -179,6 +188,9 @@ dependencies { detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:1.20.0") implementation fileTree(include: ['*'], dir: 'libs') + + implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.3" + implementation 'androidx.appcompat:appcompat:1.4.2' implementation 'com.google.android.material:material:1.6.1' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' @@ -203,6 +215,7 @@ dependencies { implementation "androidx.exifinterface:exifinterface:1.3.3" implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' + implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0' implementation 'androidx.biometric:biometric:1.1.0' @@ -236,6 +249,14 @@ dependencies { implementation 'io.requery:requery-android:1.6.1' implementation 'net.zetetic:android-database-sqlcipher:4.5.1' kapt 'io.requery:requery-processor:1.6.1' + + implementation "androidx.room:room-runtime:${roomVersion}" + implementation "androidx.room:room-rxjava2:${roomVersion}" + kapt "androidx.room:room-compiler:${roomVersion}" + 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}" diff --git a/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/8.json b/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/8.json new file mode 100644 index 000000000..68394c1dc --- /dev/null +++ b/app/schemas/com.nextcloud.talk.data.source.local.TalkDatabase/8.json @@ -0,0 +1,138 @@ +{ + "formatVersion": 1, + "database": { + "version": 8, + "identityHash": "055a9d64f28216e2981bea2fb6cc4b28", + "entities": [ + { + "tableName": "User", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT 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)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "baseUrl", + "columnName": "baseUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "pushConfigurationState", + "columnName": "pushConfigurationState", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "capabilities", + "columnName": "capabilities", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "clientCertificate", + "columnName": "clientCertificate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "externalSignalingServer", + "columnName": "externalSignalingServer", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "current", + "columnName": "current", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scheduledForDeletion", + "columnName": "scheduledForDeletion", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ArbitraryStorage", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountIdentifier` INTEGER NOT NULL, `key` TEXT, `object` TEXT, `value` TEXT, PRIMARY KEY(`accountIdentifier`))", + "fields": [ + { + "fieldPath": "accountIdentifier", + "columnName": "accountIdentifier", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "storageObject", + "columnName": "object", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "accountIdentifier" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '055a9d64f28216e2981bea2fb6cc4b28')" + ] + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/nextcloud/talk/activities/MainActivityTest.kt b/app/src/androidTest/java/com/nextcloud/talk/activities/MainActivityTest.kt index 3b785c6ce..cd34ac884 100644 --- a/app/src/androidTest/java/com/nextcloud/talk/activities/MainActivityTest.kt +++ b/app/src/androidTest/java/com/nextcloud/talk/activities/MainActivityTest.kt @@ -1,11 +1,8 @@ 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 org.junit.Assert.assertTrue +import com.nextcloud.talk.users.UserManager +import org.junit.Assert.assertNotNull import org.junit.Rule import org.junit.Test @@ -20,40 +17,25 @@ class MainActivityTest { @Test 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") } - ) - try { - Thread.sleep(2000) - } catch (e: InterruptedException) { - e.printStackTrace() - } + val user = sut.userManager.createOrUpdateUser( + "test", + UserManager.UserAttributes( + null, + serverUrl = "http://server/nc", + currentUser = true, + userId = "test", + token = "test", + displayName = "Test Name", + pushConfigurationState = null, + capabilities = null, + certificateAlias = null, + externalSignalingServer = null + ) + ).blockingGet() + + assertNotNull("Error creating user", user) sut.runOnUiThread { sut.resetConversationsList() } - - assertTrue(sut.userUtils.getIfUserWithUsernameAndServer("test", "http://server/nc")) - - try { - } catch (e: InterruptedException) { - e.printStackTrace() - } } } diff --git a/app/src/androidTest/java/com/nextcloud/talk/ui/LoginIT.java b/app/src/androidTest/java/com/nextcloud/talk/ui/LoginIT.java index e807f3775..d97af0392 100644 --- a/app/src/androidTest/java/com/nextcloud/talk/ui/LoginIT.java +++ b/app/src/androidTest/java/com/nextcloud/talk/ui/LoginIT.java @@ -29,8 +29,6 @@ import android.os.Bundle; import com.nextcloud.talk.R; import com.nextcloud.talk.activities.MainActivity; -import junit.framework.AssertionFailedError; - import org.junit.Test; import java.util.Objects; @@ -52,7 +50,6 @@ import static androidx.test.espresso.matcher.ViewMatchers.withText; import static androidx.test.espresso.web.sugar.Web.onWebView; import static androidx.test.espresso.web.webdriver.DriverAtoms.findElement; import static androidx.test.espresso.web.webdriver.DriverAtoms.webClick; -import static org.hamcrest.Matchers.not; import static org.junit.Assert.assertEquals; @@ -143,10 +140,8 @@ 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.userManager.getCurrentUser().blockingGet()).getUserId()); }); - } - - -} \ No newline at end of file +} diff --git a/app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt b/app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt index 7c59c31d3..eb3210799 100644 --- a/app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/activities/MainActivity.kt @@ -46,9 +46,11 @@ 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.model.User import com.nextcloud.talk.databinding.ActivityMainBinding import com.nextcloud.talk.models.database.UserEntity import com.nextcloud.talk.models.json.conversations.RoomOverall +import com.nextcloud.talk.users.UserManager import com.nextcloud.talk.utils.ApiUtils import com.nextcloud.talk.utils.ConductorRemapping.remapChatController import com.nextcloud.talk.utils.SecurityUtils @@ -57,8 +59,8 @@ import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ACTIVE_CONVERSATION import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_ID import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_USER_ENTITY -import com.nextcloud.talk.utils.database.user.UserUtils import io.reactivex.Observer +import io.reactivex.SingleObserver import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.Disposable import io.reactivex.schedulers.Schedulers @@ -72,9 +74,6 @@ import javax.inject.Inject class MainActivity : BaseActivity(), ActionBarProvider { lateinit var binding: ActivityMainBinding - @Inject - lateinit var userUtils: UserUtils - @Inject lateinit var dataStore: ReactiveEntityStore @@ -84,6 +83,9 @@ class MainActivity : BaseActivity(), ActionBarProvider { @Inject lateinit var ncApi: NcApi + @Inject + lateinit var userManager: UserManager + private var router: Router? = null @Suppress("Detekt.TooGenericExceptionCaught") @@ -114,11 +116,31 @@ class MainActivity : BaseActivity(), ActionBarProvider { onNewIntent(intent) } else if (!router!!.hasRootController()) { if (hasDb) { - if (userUtils.anyUserExists()) { - setDefaultRootController() - } else { - launchLoginScreen() + if (!appPreferences.isDbRoomMigrated) { + appPreferences.isDbRoomMigrated = true } + + userManager.users.subscribe(object : SingleObserver> { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onSuccess(users: List) { + if (users.isNotEmpty()) { + runOnUiThread { + setDefaultRootController() + } + } else { + runOnUiThread { + launchLoginScreen() + } + } + } + + override fun onError(e: Throwable) { + Log.e(TAG, "Error loading existing users", e) + } + }) } else { launchLoginScreen() } @@ -178,9 +200,23 @@ class MainActivity : BaseActivity(), ActionBarProvider { } fun resetConversationsList() { - if (userUtils.anyUserExists()) { - setDefaultRootController() - } + userManager.users.subscribe(object : SingleObserver> { + override fun onSubscribe(d: Disposable) { + // unused atm + } + + override fun onSuccess(users: List) { + if (users.isNotEmpty()) { + runOnUiThread { + setDefaultRootController() + } + } + } + + override fun onError(e: Throwable) { + Log.e(TAG, "Error loading existing users", e) + } + }) } fun openSettings() { @@ -218,7 +254,8 @@ 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 (userManager.currentUser.blockingGet()?.baseUrl?.endsWith(baseUrl) == true) { startConversation(user) } else { Snackbar.make( @@ -234,14 +271,16 @@ class MainActivity : BaseActivity(), ActionBarProvider { private fun startConversation(userId: String) { val roomType = "1" - val currentUser = userUtils.currentUser ?: return + + val currentUser = userManager.currentUser.blockingGet() val apiVersion = ApiUtils.getConversationApiVersion(currentUser, intArrayOf(ApiUtils.APIv4, 1)) - val credentials = ApiUtils.getCredentials(currentUser.username, currentUser.token) + val credentials = ApiUtils.getCredentials(currentUser?.username, currentUser?.token) val retrofitBucket = ApiUtils.getRetrofitBucketForCreateRoom( - apiVersion, currentUser.baseUrl, roomType, + apiVersion, currentUser?.baseUrl, roomType, null, userId, null ) + ncApi.createRoom( credentials, retrofitBucket.url, retrofitBucket.queryMap @@ -252,6 +291,7 @@ class MainActivity : BaseActivity(), ActionBarProvider { override fun onSubscribe(d: Disposable) { // unused atm } + override fun onNext(roomOverall: RoomOverall) { val bundle = Bundle() bundle.putParcelable(KEY_USER_ENTITY, currentUser) @@ -263,7 +303,7 @@ class MainActivity : BaseActivity(), ActionBarProvider { credentials, ApiUtils.getUrlForRoom( apiVersion, - currentUser.baseUrl, + currentUser?.baseUrl, roomOverall.ocs!!.data!!.token ) ) @@ -273,13 +313,14 @@ class MainActivity : BaseActivity(), ActionBarProvider { override fun onSubscribe(d: Disposable) { // unused atm } + override fun onNext(roomOverall: RoomOverall) { bundle.putParcelable( KEY_ACTIVE_CONVERSATION, Parcels.wrap(roomOverall.ocs!!.data) ) remapChatController( - router!!, currentUser.id, + router!!, currentUser!!.id!!, roomOverall.ocs!!.data!!.token!!, bundle, true ) } @@ -287,6 +328,7 @@ class MainActivity : BaseActivity(), ActionBarProvider { override fun onError(e: Throwable) { // unused atm } + override fun onComplete() { // unused atm } @@ -296,6 +338,7 @@ class MainActivity : BaseActivity(), ActionBarProvider { override fun onError(e: Throwable) { // unused atm } + override fun onComplete() { // unused atm } @@ -351,6 +394,6 @@ class MainActivity : BaseActivity(), ActionBarProvider { } companion object { - private val TAG = "MainActivity" + private const val TAG = "MainActivity" } } diff --git a/app/src/main/java/com/nextcloud/talk/adapters/items/ConversationItem.java b/app/src/main/java/com/nextcloud/talk/adapters/items/ConversationItem.java index fcb706309..9de5a060c 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/items/ConversationItem.java +++ b/app/src/main/java/com/nextcloud/talk/adapters/items/ConversationItem.java @@ -351,7 +351,6 @@ public class ConversationItem extends AbstractFlexibleItem + * + * 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 . + */ + +package com.nextcloud.talk.arbitrarystorage + +import com.nextcloud.talk.data.storage.ArbitraryStoragesRepository +import com.nextcloud.talk.data.storage.model.ArbitraryStorage +import io.reactivex.Maybe + +class ArbitraryStorageManager(private val arbitraryStoragesRepository: ArbitraryStoragesRepository) { + fun storeStorageSetting(accountIdentifier: Long, key: String?, value: String?, objectString: String?) { + arbitraryStoragesRepository.saveArbitraryStorage(ArbitraryStorage(accountIdentifier, key, objectString, value)) + } + + fun getStorageSetting(accountIdentifier: Long, key: String, objectString: String): Maybe { + return arbitraryStoragesRepository.getStorageSetting(accountIdentifier, key, objectString) + } + + suspend fun deleteAllEntriesForAccountIdentifier(accountIdentifier: Long) { + return arbitraryStoragesRepository.deleteArbitraryStorage(accountIdentifier) + } +} diff --git a/app/src/main/java/com/nextcloud/talk/controllers/ConversationsListController.java b/app/src/main/java/com/nextcloud/talk/controllers/ConversationsListController.java index a340b9e47..e20f393bc 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/ConversationsListController.java +++ b/app/src/main/java/com/nextcloud/talk/controllers/ConversationsListController.java @@ -884,7 +884,9 @@ public class ConversationsListController extends BaseController implements Flexi public void onDestroy() { super.onDestroy(); dispose(null); - searchViewDisposable.dispose(); + if (searchViewDisposable != null && !searchViewDisposable.isDisposed()) { + searchViewDisposable.dispose(); + } } public void onQueryTextChange(final String newText) { diff --git a/app/src/main/java/com/nextcloud/talk/controllers/SettingsController.kt b/app/src/main/java/com/nextcloud/talk/controllers/SettingsController.kt index 9c05a7231..185ad0b4b 100644 --- a/app/src/main/java/com/nextcloud/talk/controllers/SettingsController.kt +++ b/app/src/main/java/com/nextcloud/talk/controllers/SettingsController.kt @@ -69,6 +69,7 @@ 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.user.model.User import com.nextcloud.talk.databinding.ControllerSettingsBinding import com.nextcloud.talk.jobs.AccountRemovalWorker import com.nextcloud.talk.jobs.ContactAddressBookWorker @@ -86,6 +87,8 @@ import com.nextcloud.talk.utils.NotificationUtils.getCallRingtoneUri import com.nextcloud.talk.utils.NotificationUtils.getMessageRingtoneUri import com.nextcloud.talk.utils.SecurityUtils import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ARE_CALL_SOUNDS +import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew +import com.nextcloud.talk.utils.database.user.CurrentUserProviderNew import com.nextcloud.talk.utils.database.user.UserUtils import com.nextcloud.talk.utils.preferences.MagicUserInputModule import com.nextcloud.talk.utils.singletons.ApplicationWideMessageHolder @@ -100,7 +103,6 @@ import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.RequestBody import java.net.URI import java.net.URISyntaxException -import java.util.ArrayList import java.util.Arrays import java.util.Locale import javax.inject.Inject @@ -115,8 +117,11 @@ class SettingsController : NewBaseController(R.layout.controller_settings) { @Inject lateinit var userUtils: UserUtils + @Inject + lateinit var currentUserProvider: CurrentUserProviderNew + private var saveStateHandler: LovelySaveStateHandler? = null - private var currentUser: UserEntity? = null + private var currentUser: User? = null private var credentials: String? = null private var proxyTypeChangeListener: OnPreferenceValueChangedListener? = null private var proxyCredentialsChangeListener: OnPreferenceValueChangedListener? = null @@ -134,7 +139,7 @@ class SettingsController : NewBaseController(R.layout.controller_settings) { resources!!.getString(R.string.nc_settings) private fun getCurrentUser() { - currentUser = userUtils.currentUser + currentUser = currentUserProvider.currentUser.blockingGet() credentials = ApiUtils.getCredentials(currentUser!!.username, currentUser!!.token) } @@ -144,6 +149,7 @@ class SettingsController : NewBaseController(R.layout.controller_settings) { sharedApplication!!.componentApplication.inject(this) ViewCompat.setTransitionName((binding.avatarImage), "userAvatar.transitionTag") + getCurrentUser() if (saveStateHandler == null) { @@ -184,7 +190,7 @@ class SettingsController : NewBaseController(R.layout.controller_settings) { } private fun setupPhoneBookIntegration() { - if (CapabilitiesUtil.isPhoneBookIntegrationAvailable(userUtils.currentUser)) { + if (CapabilitiesUtilNew.isPhoneBookIntegrationAvailable(currentUser!!)) { binding.settingsPhoneBookIntegration.visibility = View.VISIBLE } else { binding.settingsPhoneBookIntegration.visibility = View.GONE @@ -424,7 +430,7 @@ class SettingsController : NewBaseController(R.layout.controller_settings) { } private fun removeCurrentAccount() { - val otherUserExists = userUtils.scheduleUserForDeletionWithId(currentUser!!.id) + val otherUserExists = userUtils.scheduleUserForDeletionWithId(currentUser!!.id!!) val accountRemovalWork = OneTimeWorkRequest.Builder(AccountRemovalWorker::class.java).build() WorkManager.getInstance().enqueue(accountRemovalWork) if (otherUserExists && view != null) { @@ -456,7 +462,6 @@ class SettingsController : NewBaseController(R.layout.controller_settings) { super.onAttach(view) actionBar?.show() dispose(null) - getCurrentUser() binding.settingsVersion.setOnClickListener { sendLogs() @@ -639,7 +644,7 @@ class SettingsController : NewBaseController(R.layout.controller_settings) { private fun setupServerAgeWarning() { when { - CapabilitiesUtil.isServerEOL(currentUser) -> { + CapabilitiesUtilNew.isServerEOL(currentUser!!) -> { binding.serverAgeWarningText.setTextColor(ContextCompat.getColor((context)!!, R.color.nc_darkRed)) binding.serverAgeWarningText.setText(R.string.nc_settings_server_eol) binding.serverAgeWarningIcon.setColorFilter( @@ -647,7 +652,7 @@ class SettingsController : NewBaseController(R.layout.controller_settings) { PorterDuff.Mode.SRC_IN ) } - CapabilitiesUtil.isServerAlmostEOL(currentUser) -> { + CapabilitiesUtilNew.isServerAlmostEOL(currentUser!!) -> { binding.serverAgeWarningText.setTextColor( ContextCompat.getColor((context)!!, R.color.nc_darkYellow) ) @@ -679,7 +684,7 @@ class SettingsController : NewBaseController(R.layout.controller_settings) { if (CapabilitiesUtil.isReadStatusAvailable(userUtils.currentUser)) { (binding.settingsReadPrivacy.findViewById(R.id.mp_checkable) as Checkable).isChecked = - !CapabilitiesUtil.isReadStatusPrivate(currentUser) + !CapabilitiesUtilNew.isReadStatusPrivate(currentUser!!) } else { binding.settingsReadPrivacy.visibility = View.GONE } diff --git a/app/src/main/java/com/nextcloud/talk/dagger/modules/DatabaseModule.java b/app/src/main/java/com/nextcloud/talk/dagger/modules/DatabaseModule.java index 87be3413f..8f2a91ef1 100644 --- a/app/src/main/java/com/nextcloud/talk/dagger/modules/DatabaseModule.java +++ b/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; @@ -35,6 +36,7 @@ import io.requery.reactivex.ReactiveSupport; import io.requery.sql.Configuration; import io.requery.sql.EntityDataStore; import net.orange_box.storebox.StoreBox; +import net.sqlcipher.database.SQLiteDatabase; import javax.inject.Singleton; @@ -44,7 +46,13 @@ public class DatabaseModule { @Provides @Singleton - public SqlCipherDatabaseSource provideSqlCipherDatabaseSource(@NonNull final Context context) { + public SqlCipherDatabaseSource provideSqlCipherDatabaseSource( + @NonNull final Context context, + final AppPreferences appPreferences) { + int version = DB_VERSION; + if (appPreferences.getIsDbRoomMigrated()) { + version++; + } return new SqlCipherDatabaseSource( context, Models.DEFAULT, @@ -56,7 +64,14 @@ public class DatabaseModule { .trim() + ".sqlite", context.getString(R.string.nc_talk_database_encryption_key), - DB_VERSION); + version) { + @Override + public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { + if (newVersion < 7) { + super.onDowngrade(db, oldVersion, newVersion); + } + } + }; } @Provides @@ -70,8 +85,14 @@ public class DatabaseModule { @Provides @Singleton public AppPreferences providePreferences(@NonNull final Context poContext) { - AppPreferences preferences = StoreBox.create(poContext, AppPreferences.class); + AppPreferences preferences = StoreBox.create(poContext, AppPreferences.class); preferences.removeLinkPreviews(); return preferences; } + + @Provides + @Singleton + public TalkDatabase provideTalkDatabase(@NonNull final Context context) { + return TalkDatabase.getInstance(context); + } } diff --git a/app/src/main/java/com/nextcloud/talk/dagger/modules/RepositoryModule.kt b/app/src/main/java/com/nextcloud/talk/dagger/modules/RepositoryModule.kt index 267ab6b41..bb8a5f255 100644 --- a/app/src/main/java/com/nextcloud/talk/dagger/modules/RepositoryModule.kt +++ b/app/src/main/java/com/nextcloud/talk/dagger/modules/RepositoryModule.kt @@ -2,6 +2,8 @@ * Nextcloud Talk application * * @author Álvaro Brey + * @author Andy Scherzinger + * Copyright (C) 2022 Andy Scherzinger * Copyright (C) 2022 Álvaro Brey * Copyright (C) 2022 Nextcloud GmbH * @@ -22,6 +24,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 +57,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()) + } } diff --git a/app/src/main/java/com/nextcloud/talk/data/source/local/Migrations.kt b/app/src/main/java/com/nextcloud/talk/data/source/local/Migrations.kt new file mode 100644 index 000000000..4d675f0f5 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/data/source/local/Migrations.kt @@ -0,0 +1,95 @@ +/* + * Nextcloud Talk application + * + * @author Andy Scherzinger + * Copyright (C) 2022 Andy Scherzinger + * + * 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 . + */ + +package com.nextcloud.talk.data.source.local + +import android.util.Log +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +@Suppress("MagicNumber") +object Migrations { + val MIGRATION_6_8 = object : Migration(6, 8) { + override fun migrate(database: SupportSQLiteDatabase) { + Log.i("Migrations", "Migrating 6 to 8") + migrateToRoom(database) + } + } + + val MIGRATION_7_8 = object : Migration(7, 8) { + override fun migrate(database: SupportSQLiteDatabase) { + Log.i("Migrations", "Migrating 7 to 8") + migrateToRoom(database) + } + } + + fun migrateToRoom(database: SupportSQLiteDatabase) { + 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") + } +} diff --git a/app/src/main/java/com/nextcloud/talk/data/source/local/TalkDatabase.kt b/app/src/main/java/com/nextcloud/talk/data/source/local/TalkDatabase.kt new file mode 100644 index 000000000..bc422c7fa --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/data/source/local/TalkDatabase.kt @@ -0,0 +1,99 @@ +/* + * Nextcloud Talk application + * + * @author Mario Danic + * Copyright (C) 2017-2020 Mario Danic + * + * 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 . + */ + +package com.nextcloud.talk.data.source.local + +import android.content.Context +import androidx.room.Database +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.ArbitraryStorageEntity +import com.nextcloud.talk.data.user.UsersDao +import com.nextcloud.talk.data.user.model.UserEntity +import net.sqlcipher.database.SQLiteDatabase +import net.sqlcipher.database.SupportFactory +import java.util.Locale + +@Database( + entities = [UserEntity::class, ArbitraryStorageEntity::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 { + + @Volatile + private var INSTANCE: TalkDatabase? = null + + @JvmStatic + fun getInstance(context: Context): TalkDatabase = + INSTANCE ?: synchronized(this) { + INSTANCE ?: build(context).also { INSTANCE = it } + } + + 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_6_8, 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() + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/data/source/local/converters/CapabilitiesConverter.kt b/app/src/main/java/com/nextcloud/talk/data/source/local/converters/CapabilitiesConverter.kt new file mode 100644 index 000000000..8993711fc --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/data/source/local/converters/CapabilitiesConverter.kt @@ -0,0 +1,45 @@ +/* + * Nextcloud Talk application + * + * @author Mario Danic + * Copyright (C) 2017-2020 Mario Danic + * + * 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 . + */ + +package com.nextcloud.talk.data.source.local.converters + +import androidx.room.TypeConverter +import com.bluelinelabs.logansquare.LoganSquare +import com.nextcloud.talk.models.json.capabilities.Capabilities + +class CapabilitiesConverter { + @TypeConverter + fun fromCapabilitiesToString(capabilities: Capabilities?): String { + return if (capabilities == null) { + "" + } else { + LoganSquare.serialize(capabilities) + } + } + + @TypeConverter + fun fromStringToCapabilities(value: String): Capabilities? { + return if (value.isBlank()) { + null + } else { + return LoganSquare.parse(value, Capabilities::class.java) + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/data/source/local/converters/ExternalSignalingServerConverter.kt b/app/src/main/java/com/nextcloud/talk/data/source/local/converters/ExternalSignalingServerConverter.kt new file mode 100644 index 000000000..c5a611053 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/data/source/local/converters/ExternalSignalingServerConverter.kt @@ -0,0 +1,46 @@ +/* + * Nextcloud Talk application + * + * @author Andy Scherzinger + * Copyright (C) 2022 Andy Scherzinger + * + * 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 . + */ + +package com.nextcloud.talk.data.source.local.converters + +import androidx.room.TypeConverter +import com.bluelinelabs.logansquare.LoganSquare +import com.nextcloud.talk.models.ExternalSignalingServer + +class ExternalSignalingServerConverter { + + @TypeConverter + fun fromExternalSignalingServerToString(externalSignalingServer: ExternalSignalingServer?): String { + return if (externalSignalingServer == null) { + "" + } else { + LoganSquare.serialize(externalSignalingServer) + } + } + + @TypeConverter + fun fromStringToExternalSignalingServer(value: String): ExternalSignalingServer? { + return if (value.isBlank()) { + null + } else { + return LoganSquare.parse(value, ExternalSignalingServer::class.java) + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/data/source/local/converters/HashMapHashMapConverter.kt b/app/src/main/java/com/nextcloud/talk/data/source/local/converters/HashMapHashMapConverter.kt new file mode 100644 index 000000000..a40220a82 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/data/source/local/converters/HashMapHashMapConverter.kt @@ -0,0 +1,44 @@ +/* + * Nextcloud Talk application + * + * @author Mario Danic + * Copyright (C) 2017-2020 Mario Danic + * + * 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 . + */ + +package com.nextcloud.talk.data.source.local.converters + +import androidx.room.TypeConverter +import com.bluelinelabs.logansquare.LoganSquare + +class HashMapHashMapConverter { + @TypeConverter + fun fromDoubleHashMapToString(map: HashMap>?): String? { + return if (map == null) { + LoganSquare.serialize(hashMapOf>()) + } else { + return LoganSquare.serialize(map) + } + } + + @TypeConverter + fun fromStringToDoubleHashMap(value: String?): HashMap>? { + if (value.isNullOrEmpty()) { + return hashMapOf() + } + + return LoganSquare.parseMap(value, HashMap::class.java) as HashMap>? + } +} diff --git a/app/src/main/java/com/nextcloud/talk/data/source/local/converters/PushConfigurationConverter.kt b/app/src/main/java/com/nextcloud/talk/data/source/local/converters/PushConfigurationConverter.kt new file mode 100644 index 000000000..2e4738bcc --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/data/source/local/converters/PushConfigurationConverter.kt @@ -0,0 +1,48 @@ +/* + * Nextcloud Talk application + * + * @author Andy Scherzinger + * @author Mario Danic + * Copyright (C) 2022 Andy Scherzinger + * Copyright (C) 2017-2020 Mario Danic + * + * 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 . + */ + +package com.nextcloud.talk.data.source.local.converters + +import androidx.room.TypeConverter +import com.bluelinelabs.logansquare.LoganSquare +import com.nextcloud.talk.models.json.push.PushConfigurationState + +class PushConfigurationConverter { + + @TypeConverter + fun fromPushConfigurationToString(pushConfiguration: PushConfigurationState?): String { + return if (pushConfiguration == null) { + "" + } else { + LoganSquare.serialize(pushConfiguration) + } + } + + @TypeConverter + fun fromStringToPushConfiguration(value: String?): PushConfigurationState? { + return if (value.isNullOrBlank()) { + null + } else { + return LoganSquare.parse(value, PushConfigurationState::class.java) + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/data/source/local/converters/SignalingSettingsConverter.kt b/app/src/main/java/com/nextcloud/talk/data/source/local/converters/SignalingSettingsConverter.kt new file mode 100644 index 000000000..5664fbbfc --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/data/source/local/converters/SignalingSettingsConverter.kt @@ -0,0 +1,48 @@ +/* + * Nextcloud Talk application + * + * @author Andy Scherzinger + * @author Mario Danic + * Copyright (C) 2022 Andy Scherzinger + * Copyright (C) 2017-2020 Mario Danic + * + * 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 . + */ + +package com.nextcloud.talk.data.source.local.converters + +import androidx.room.TypeConverter +import com.bluelinelabs.logansquare.LoganSquare +import com.nextcloud.talk.models.json.signaling.settings.SignalingSettings + +class SignalingSettingsConverter { + + @TypeConverter + fun fromSignalingSettingsToString(signalingSettings: SignalingSettings?): String { + return if (signalingSettings == null) { + "" + } else { + LoganSquare.serialize(signalingSettings) + } + } + + @TypeConverter + fun fromStringToSignalingSettings(value: String): SignalingSettings? { + return if (value.isBlank()) { + null + } else { + return LoganSquare.parse(value, SignalingSettings::class.java) + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/data/storage/ArbitraryStorageMapper.kt b/app/src/main/java/com/nextcloud/talk/data/storage/ArbitraryStorageMapper.kt new file mode 100644 index 000000000..cd99dda9b --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/data/storage/ArbitraryStorageMapper.kt @@ -0,0 +1,46 @@ +/* + * Nextcloud Talk application + * + * @author Andy Scherzinger + * Copyright (C) 2022 Andy Scherzinger + * + * model 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. + * + * model 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 model program. If not, see . + */ + +package com.nextcloud.talk.data.storage + +import com.nextcloud.talk.data.storage.model.ArbitraryStorage +import com.nextcloud.talk.data.storage.model.ArbitraryStorageEntity + +object ArbitraryStorageMapper { + fun toModel(entity: ArbitraryStorageEntity?): ArbitraryStorage? { + return entity?.let { + ArbitraryStorage( + it.accountIdentifier, + it.key, + it.storageObject, + it.value + ) + } + } + + fun toEntity(model: ArbitraryStorage): ArbitraryStorageEntity { + return ArbitraryStorageEntity( + accountIdentifier = model.accountIdentifier, + key = model.key, + storageObject = model.storageObject, + value = model.value + ) + } +} diff --git a/app/src/main/java/com/nextcloud/talk/data/storage/ArbitraryStoragesDao.kt b/app/src/main/java/com/nextcloud/talk/data/storage/ArbitraryStoragesDao.kt new file mode 100644 index 000000000..0bcbe2d7e --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/data/storage/ArbitraryStoragesDao.kt @@ -0,0 +1,49 @@ +/* + * Nextcloud Talk application + * + * @author Andy Scherzinger + * Copyright (C) 2022 Andy Scherzinger + * + * 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 . + */ + +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.ArbitraryStorageEntity +import io.reactivex.Maybe + +@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 + ): Maybe + + @Query("DELETE FROM ArbitraryStorage WHERE accountIdentifier = :accountIdentifier") + abstract fun deleteArbitraryStorage(accountIdentifier: Long) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + abstract fun saveArbitraryStorage(arbitraryStorage: ArbitraryStorageEntity): Long +} diff --git a/app/src/main/java/com/nextcloud/talk/data/storage/ArbitraryStoragesRepository.kt b/app/src/main/java/com/nextcloud/talk/data/storage/ArbitraryStoragesRepository.kt new file mode 100644 index 000000000..e97080967 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/data/storage/ArbitraryStoragesRepository.kt @@ -0,0 +1,30 @@ +/* + * Nextcloud Talk application + * + * @author Andy Scherzinger + * Copyright (C) 2022 Andy Scherzinger + * + * 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 . + */ + +package com.nextcloud.talk.data.storage + +import com.nextcloud.talk.data.storage.model.ArbitraryStorage +import io.reactivex.Maybe + +interface ArbitraryStoragesRepository { + fun getStorageSetting(accountIdentifier: Long, key: String, objectString: String): Maybe + fun deleteArbitraryStorage(accountIdentifier: Long) + fun saveArbitraryStorage(arbitraryStorage: ArbitraryStorage): Long +} diff --git a/app/src/main/java/com/nextcloud/talk/data/storage/ArbitraryStoragesRepositoryImpl.kt b/app/src/main/java/com/nextcloud/talk/data/storage/ArbitraryStoragesRepositoryImpl.kt new file mode 100644 index 000000000..264b29756 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/data/storage/ArbitraryStoragesRepositoryImpl.kt @@ -0,0 +1,45 @@ +/* + * Nextcloud Talk application + * + * @author Andy Scherzinger + * Copyright (C) 2022 Andy Scherzinger + * + * 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 . + */ + +package com.nextcloud.talk.data.storage + +import com.nextcloud.talk.data.storage.model.ArbitraryStorage +import io.reactivex.Maybe + +class ArbitraryStoragesRepositoryImpl(private val arbitraryStoragesDao: ArbitraryStoragesDao) : + ArbitraryStoragesRepository { + override fun getStorageSetting( + accountIdentifier: Long, + key: String, + objectString: String + ): Maybe { + return arbitraryStoragesDao + .getStorageSetting(accountIdentifier, key, objectString) + .map { ArbitraryStorageMapper.toModel(it) } + } + + override fun deleteArbitraryStorage(accountIdentifier: Long) { + arbitraryStoragesDao.deleteArbitraryStorage(accountIdentifier) + } + + override fun saveArbitraryStorage(arbitraryStorage: ArbitraryStorage): Long { + return arbitraryStoragesDao.saveArbitraryStorage(ArbitraryStorageMapper.toEntity(arbitraryStorage)) + } +} diff --git a/app/src/main/java/com/nextcloud/talk/data/storage/model/ArbitraryStorage.kt b/app/src/main/java/com/nextcloud/talk/data/storage/model/ArbitraryStorage.kt new file mode 100644 index 000000000..90353bc76 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/data/storage/model/ArbitraryStorage.kt @@ -0,0 +1,32 @@ +/* + * Nextcloud Talk application + * + * @author Andy Scherzinger + * Copyright (C) 2022 Andy Scherzinger + * + * 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 . + */ + +package com.nextcloud.talk.data.storage.model + +import android.os.Parcelable +import kotlinx.android.parcel.Parcelize + +@Parcelize +data class ArbitraryStorage( + var accountIdentifier: Long = 0, + var key: String? = null, + var storageObject: String? = null, + var value: String? = null +) : Parcelable diff --git a/app/src/main/java/com/nextcloud/talk/data/storage/model/ArbitraryStorageEntity.kt b/app/src/main/java/com/nextcloud/talk/data/storage/model/ArbitraryStorageEntity.kt new file mode 100644 index 000000000..a76d3e684 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/data/storage/model/ArbitraryStorageEntity.kt @@ -0,0 +1,36 @@ +/* + * Nextcloud Talk application + * + * @author Andy Scherzinger + * Copyright (C) 2022 Andy Scherzinger + * + * 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 . + */ + +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 + +@Parcelize +@Entity(tableName = "ArbitraryStorage") +data class ArbitraryStorageEntity( + @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 diff --git a/app/src/main/java/com/nextcloud/talk/data/user/UserMapper.kt b/app/src/main/java/com/nextcloud/talk/data/user/UserMapper.kt new file mode 100644 index 000000000..f6a88ac77 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/data/user/UserMapper.kt @@ -0,0 +1,69 @@ +/* + * Nextcloud Talk application + * + * @author Andy Scherzinger + * Copyright (C) 2022 Andy Scherzinger + * + * model 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. + * + * model 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 model program. If not, see . + */ + +package com.nextcloud.talk.data.user + +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.data.user.model.UserEntity + +object UserMapper { + fun toModel(entities: List?): List { + return entities?.map { user: UserEntity? -> + toModel(user)!! + } ?: emptyList() + } + + fun toModel(entity: UserEntity?): User? { + return entity?.let { + User( + entity.id, + entity.userId, + entity.username, + entity.baseUrl, + entity.token, + entity.displayName, + entity.pushConfigurationState, + entity.capabilities, + entity.clientCertificate, + entity.externalSignalingServer, + entity.current, + entity.scheduledForDeletion + ) + } + } + + fun toEntity(model: User): UserEntity { + val userEntity = when (val id = model.id) { + null -> UserEntity(userId = model.userId, username = model.username, baseUrl = model.baseUrl) + else -> UserEntity(id, model.userId, model.username, model.baseUrl) + } + userEntity.apply { + token = model.token + displayName = model.displayName + pushConfigurationState = model.pushConfigurationState + capabilities = model.capabilities + clientCertificate = model.clientCertificate + externalSignalingServer = model.externalSignalingServer + current = model.current + scheduledForDeletion = model.scheduledForDeletion + } + return userEntity + } +} diff --git a/app/src/main/java/com/nextcloud/talk/data/user/UsersDao.kt b/app/src/main/java/com/nextcloud/talk/data/user/UsersDao.kt new file mode 100644 index 000000000..e1826f5ce --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/data/user/UsersDao.kt @@ -0,0 +1,126 @@ +/* + * Nextcloud Talk application + * + * @author Mario Danic + * @author Andy Scherzinger + * Copyright (C) 2022 Andy Scherzinger + * Copyright (C) 2017-2020 Mario Danic + * + * 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 . + */ + +package com.nextcloud.talk.data.user + +import android.util.Log +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Transaction +import androidx.room.Update +import com.nextcloud.talk.data.user.model.UserEntity +import io.reactivex.Maybe +import io.reactivex.Single +import java.lang.Boolean.FALSE +import java.lang.Boolean.TRUE + +@Dao +@Suppress("TooManyFunctions") +abstract class UsersDao { + // get active user + @Query("SELECT * FROM User where current = 1") + abstract fun getActiveUser(): Maybe + + @Query("SELECT * FROM User where current = 1") + abstract fun getActiveUserSynchronously(): UserEntity? + + @Query("DELETE FROM User WHERE id = :id") + abstract fun deleteUserWithId(id: Long) + + @Update + abstract fun updateUser(user: UserEntity): Int + + @Insert(onConflict = OnConflictStrategy.REPLACE) + abstract fun saveUser(user: UserEntity): Long + + @Insert(onConflict = OnConflictStrategy.REPLACE) + abstract fun saveUsers(vararg users: UserEntity): List + + // get all users not scheduled for deletion + @Query("SELECT * FROM User where scheduledForDeletion != 1") + abstract fun getUsers(): Single> + + @Query("SELECT * FROM User where id = :id") + abstract fun getUserWithId(id: Long): Maybe + + @Query("SELECT * FROM User where id = :id AND scheduledForDeletion != 1") + abstract fun getUserWithIdNotScheduledForDeletion(id: Long): Maybe + + @Query("SELECT * FROM User where userId = :userId") + abstract fun getUserWithUserId(userId: String): Maybe + + @Query("SELECT * FROM User where userId != :userId") + abstract fun getUsersWithoutUserId(userId: Long): Single> + + @Query("SELECT * FROM User where scheduledForDeletion = 1") + abstract fun getUsersScheduledForDeletion(): Single> + + @Query("SELECT * FROM User where scheduledForDeletion = 0") + abstract fun getUsersNotScheduledForDeletion(): Single> + + @Query("SELECT * FROM User WHERE username = :username AND baseUrl = :server") + abstract fun getUserWithUsernameAndServer(username: String, server: String): Maybe + + @Transaction + @Suppress("Detekt.TooGenericExceptionCaught") // blockingGet() only throws RuntimeExceptions per rx docs + open fun setUserAsActiveWithId(id: Long): Boolean { + return try { + getUsers().blockingGet().forEach { user -> + user.current = user.id == id + updateUser(user) + } + true + } catch (e: RuntimeException) { + Log.e(TAG, "Error setting user active", e) + false + } + } + + @Transaction + open fun markUserForDeletion(id: Long): Boolean { + getUserWithId(id).blockingGet()?.let { user -> + user.current = FALSE + updateUser(user) + } + + return setAnyUserAsActive() + } + + @Transaction + open fun setAnyUserAsActive(): Boolean { + val users = getUsers().blockingGet() + + val result = users.firstOrNull()?.let { user -> + user.current = TRUE + updateUser(user) + TRUE + } ?: FALSE + + return result + } + + companion object { + const val TAG = "UsersDao" + } +} diff --git a/app/src/main/java/com/nextcloud/talk/data/user/UsersRepository.kt b/app/src/main/java/com/nextcloud/talk/data/user/UsersRepository.kt new file mode 100644 index 000000000..afe903527 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/data/user/UsersRepository.kt @@ -0,0 +1,46 @@ +/* + * Nextcloud Talk application + * + * @author Mario Danic + * @author Andy Scherzinger + * Copyright (C) 2022 Andy Scherzinger + * Copyright (C) 2017-2020 Mario Danic + * + * 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 . + */ + +package com.nextcloud.talk.data.user + +import com.nextcloud.talk.data.user.model.User +import io.reactivex.Maybe +import io.reactivex.Single + +@Suppress("TooManyFunctions") +interface UsersRepository { + fun getActiveUser(): Maybe + fun getUsers(): Single> + fun getUserWithId(id: Long): Maybe + fun getUserWithIdNotScheduledForDeletion(id: Long): Maybe + fun getUserWithUserId(userId: String): Maybe + fun getUsersWithoutUserId(userId: Long): Single> + fun getUsersScheduledForDeletion(): Single> + fun getUsersNotScheduledForDeletion(): Single> + fun getUserWithUsernameAndServer(username: String, server: String): Maybe + fun updateUser(user: User): Int + fun insertUser(user: User): Long + fun setUserAsActiveWithId(id: Long): Single + fun deleteUserWithId(id: Long) + fun setAnyUserAsActive(): Boolean + fun markUserForDeletion(id: Long): Boolean +} diff --git a/app/src/main/java/com/nextcloud/talk/data/user/UsersRepositoryImpl.kt b/app/src/main/java/com/nextcloud/talk/data/user/UsersRepositoryImpl.kt new file mode 100644 index 000000000..07957cda2 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/data/user/UsersRepositoryImpl.kt @@ -0,0 +1,91 @@ +/* + * Nextcloud Talk application + * + * @author Mario Danic + * @author Andy Scherzinger + * Copyright (C) 2022 Andy Scherzinger + * Copyright (C) 2017-2020 Mario Danic + * + * 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 . + */ + +package com.nextcloud.talk.data.user + +import com.nextcloud.talk.data.user.model.User +import io.reactivex.Maybe +import io.reactivex.Single + +@Suppress("TooManyFunctions") +class UsersRepositoryImpl(private val usersDao: UsersDao) : UsersRepository { + + override fun getActiveUser(): Maybe { + return usersDao.getActiveUser().map { UserMapper.toModel(it) } + } + + override fun getUsers(): Single> { + return usersDao.getUsers().map { UserMapper.toModel(it) } + } + + override fun getUserWithId(id: Long): Maybe { + return usersDao.getUserWithId(id).map { UserMapper.toModel(it) } + } + + override fun getUserWithIdNotScheduledForDeletion(id: Long): Maybe { + return usersDao.getUserWithIdNotScheduledForDeletion(id).map { UserMapper.toModel(it) } + } + + override fun getUserWithUserId(userId: String): Maybe { + return usersDao.getUserWithUserId(userId).map { UserMapper.toModel(it) } + } + + override fun getUsersWithoutUserId(userId: Long): Single> { + return usersDao.getUsersWithoutUserId(userId).map { UserMapper.toModel(it) } + } + + override fun getUsersScheduledForDeletion(): Single> { + return usersDao.getUsersScheduledForDeletion().map { UserMapper.toModel(it) } + } + + override fun getUsersNotScheduledForDeletion(): Single> { + return usersDao.getUsersNotScheduledForDeletion().map { UserMapper.toModel(it) } + } + + override fun getUserWithUsernameAndServer(username: String, server: String): Maybe { + return usersDao.getUserWithUsernameAndServer(username, server).map { UserMapper.toModel(it) } + } + + override fun updateUser(user: User): Int { + return usersDao.updateUser(UserMapper.toEntity(user)) + } + + override fun insertUser(user: User): Long { + return usersDao.saveUser(UserMapper.toEntity(user)) + } + + override fun setUserAsActiveWithId(id: Long): Single { + return Single.just(usersDao.setUserAsActiveWithId(id)) + } + + override fun deleteUserWithId(id: Long) { + usersDao.deleteUserWithId(id) + } + + override fun setAnyUserAsActive(): Boolean { + return usersDao.setAnyUserAsActive() + } + + override fun markUserForDeletion(id: Long): Boolean { + return usersDao.markUserForDeletion(id) + } +} diff --git a/app/src/main/java/com/nextcloud/talk/data/user/model/User.kt b/app/src/main/java/com/nextcloud/talk/data/user/model/User.kt new file mode 100644 index 000000000..551a4a2e2 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/data/user/model/User.kt @@ -0,0 +1,72 @@ +/* + * Nextcloud Talk application + * + * @author Mario Danic + * Copyright (C) 2017-2020 Mario Danic + * + * 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 . + */ +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.utils.ApiUtils +import kotlinx.android.parcel.Parcelize +import java.lang.Boolean.FALSE + +@Parcelize +data class User( + var id: Long? = null, + var userId: String? = null, + var username: String? = null, + var baseUrl: String? = null, + var token: String? = null, + var displayName: String? = null, + var pushConfigurationState: PushConfigurationState? = null, + var capabilities: Capabilities? = null, + var clientCertificate: String? = null, + var externalSignalingServer: ExternalSignalingServer? = null, + var current: Boolean = FALSE, + var scheduledForDeletion: Boolean = FALSE, +) : Parcelable { + + fun getMaxMessageLength(): Int { + return capabilities?.spreedCapability?.config?.get("chat")?.get("max-length")?.toInt() + ?: DEFAULT_CHAT_MESSAGE_LENGTH + } + + fun getAttachmentsConfig(key: String): Any? { + return capabilities?.spreedCapability?.config?.get("attachments")?.get(key) + } + + fun canUserCreateGroupConversations(): Boolean { + val canCreateValue = capabilities?.spreedCapability?.config?.get("conversations")?.get("can-create") + canCreateValue?.let { + return it.toBoolean() + } + return true + } + + fun getCredentials(): String = ApiUtils.getCredentials(username, token) + + fun hasSpreedFeatureCapability(capabilityName: String): Boolean { + return capabilities?.spreedCapability?.features?.contains(capabilityName) ?: false + } + + companion object { + const val DEFAULT_CHAT_MESSAGE_LENGTH: Int = 1000 + } +} diff --git a/app/src/main/java/com/nextcloud/talk/data/user/model/UserEntity.kt b/app/src/main/java/com/nextcloud/talk/data/user/model/UserEntity.kt new file mode 100644 index 000000000..555bdebcd --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/data/user/model/UserEntity.kt @@ -0,0 +1,50 @@ +/* + * Nextcloud Talk application + * + * @author Andy Scherzinger + * @author Mario Danic + * Copyright (C) 2022 Andy Scherzinger + * Copyright (C) 2017-2020 Mario Danic + * + * 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 . + */ + +package com.nextcloud.talk.data.user.model + +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 kotlinx.android.parcel.Parcelize +import java.lang.Boolean.FALSE + +@Parcelize +@Entity(tableName = "User") +data class UserEntity( + @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") var id: Long = 0, + @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 = "displayName") var displayName: String? = null, + @ColumnInfo(name = "pushConfigurationState") var pushConfigurationState: PushConfigurationState? = null, + @ColumnInfo(name = "capabilities") var capabilities: Capabilities? = null, + @ColumnInfo(name = "clientCertificate") var clientCertificate: String? = null, + @ColumnInfo(name = "externalSignalingServer") var externalSignalingServer: ExternalSignalingServer? = null, + @ColumnInfo(name = "current") var current: Boolean = FALSE, + @ColumnInfo(name = "scheduledForDeletion") var scheduledForDeletion: Boolean = FALSE, +) : Parcelable diff --git a/app/src/main/java/com/nextcloud/talk/models/database/ArbitraryStorage.java b/app/src/main/java/com/nextcloud/talk/models/database/ArbitraryStorage.java index 4927c2986..897f20217 100644 --- a/app/src/main/java/com/nextcloud/talk/models/database/ArbitraryStorage.java +++ b/app/src/main/java/com/nextcloud/talk/models/database/ArbitraryStorage.java @@ -27,6 +27,10 @@ import io.requery.Persistable; import java.io.Serializable; +/** + * Legacy arbitrary storage entity, please migrate to {@link com.nextcloud.talk.data.storage.model.ArbitraryStorage}. + */ +@Deprecated @Entity public interface ArbitraryStorage extends Parcelable, Persistable, Serializable { @Key diff --git a/app/src/main/java/com/nextcloud/talk/models/database/CapabilitiesUtil.java b/app/src/main/java/com/nextcloud/talk/models/database/CapabilitiesUtil.java index 447ffc339..ee5cc67a9 100644 --- a/app/src/main/java/com/nextcloud/talk/models/database/CapabilitiesUtil.java +++ b/app/src/main/java/com/nextcloud/talk/models/database/CapabilitiesUtil.java @@ -33,6 +33,10 @@ import java.util.Map; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +/** + * Deprecated, please migrate to {@link com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew}. + */ +@Deprecated public abstract class CapabilitiesUtil { private static final String TAG = CapabilitiesUtil.class.getSimpleName(); @@ -123,6 +127,7 @@ public abstract class CapabilitiesUtil { return 1000; } + @Deprecated public static boolean isPhoneBookIntegrationAvailable(@Nullable UserEntity user) { if (user != null && user.getCapabilities() != null) { try { @@ -156,6 +161,7 @@ public abstract class CapabilitiesUtil { return false; } + @Deprecated public static boolean isReadStatusPrivate(@Nullable UserEntity user) { if (user != null && user.getCapabilities() != null) { try { @@ -280,6 +286,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); } diff --git a/app/src/main/java/com/nextcloud/talk/models/database/User.java b/app/src/main/java/com/nextcloud/talk/models/database/User.java index 47e9fac7e..e92274498 100644 --- a/app/src/main/java/com/nextcloud/talk/models/database/User.java +++ b/app/src/main/java/com/nextcloud/talk/models/database/User.java @@ -30,6 +30,10 @@ import io.requery.Generated; import io.requery.Key; import io.requery.Persistable; +/** + * Legacy user entity, please migrate to {@link com.nextcloud.talk.data.user.model.User}. + */ +@Deprecated @Entity public interface User extends Parcelable, Persistable, Serializable { String TAG = "UserEntity"; diff --git a/app/src/main/java/com/nextcloud/talk/models/json/capabilities/NotificationsCapability.kt b/app/src/main/java/com/nextcloud/talk/models/json/capabilities/NotificationsCapability.kt index 6ea1ccfe5..ac8d207f4 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/capabilities/NotificationsCapability.kt +++ b/app/src/main/java/com/nextcloud/talk/models/json/capabilities/NotificationsCapability.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 NotificationsCapability( @JsonField(name = ["ocs-endpoints"]) var features: List? diff --git a/app/src/main/java/com/nextcloud/talk/models/json/capabilities/ProvisioningCapability.kt b/app/src/main/java/com/nextcloud/talk/models/json/capabilities/ProvisioningCapability.kt index cb809cdf5..2ff775777 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/capabilities/ProvisioningCapability.kt +++ b/app/src/main/java/com/nextcloud/talk/models/json/capabilities/ProvisioningCapability.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 ProvisioningCapability( @JsonField(name = ["AccountPropertyScopesVersion"]) var accountPropertyScopesVersion: Int? diff --git a/app/src/main/java/com/nextcloud/talk/models/json/capabilities/SpreedCapability.kt b/app/src/main/java/com/nextcloud/talk/models/json/capabilities/SpreedCapability.kt index cc0665ec5..658301ee7 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/capabilities/SpreedCapability.kt +++ b/app/src/main/java/com/nextcloud/talk/models/json/capabilities/SpreedCapability.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 SpreedCapability( @JsonField(name = ["features"]) var features: List?, diff --git a/app/src/main/java/com/nextcloud/talk/models/json/capabilities/ThemingCapability.kt b/app/src/main/java/com/nextcloud/talk/models/json/capabilities/ThemingCapability.kt index 61b3344d5..6dfdcae7b 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/capabilities/ThemingCapability.kt +++ b/app/src/main/java/com/nextcloud/talk/models/json/capabilities/ThemingCapability.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 ThemingCapability( @JsonField(name = ["name"]) var name: String?, diff --git a/app/src/main/java/com/nextcloud/talk/models/json/capabilities/UserStatusCapability.kt b/app/src/main/java/com/nextcloud/talk/models/json/capabilities/UserStatusCapability.kt index eda9cb399..2ca1edf2f 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/capabilities/UserStatusCapability.kt +++ b/app/src/main/java/com/nextcloud/talk/models/json/capabilities/UserStatusCapability.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 UserStatusCapability( @JsonField(name = ["enabled"]) var enabled: Boolean, diff --git a/app/src/main/java/com/nextcloud/talk/models/json/signaling/settings/IceServer.kt b/app/src/main/java/com/nextcloud/talk/models/json/signaling/settings/IceServer.kt index 85a4efbc9..90d57ff79 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/signaling/settings/IceServer.kt +++ b/app/src/main/java/com/nextcloud/talk/models/json/signaling/settings/IceServer.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 IceServer( @Deprecated("") @JsonField(name = ["url"]) diff --git a/app/src/main/java/com/nextcloud/talk/models/json/signaling/settings/Settings.kt b/app/src/main/java/com/nextcloud/talk/models/json/signaling/settings/SignalingSettings.kt similarity index 94% rename from app/src/main/java/com/nextcloud/talk/models/json/signaling/settings/Settings.kt rename to app/src/main/java/com/nextcloud/talk/models/json/signaling/settings/SignalingSettings.kt index f04b3095c..4b1a1cd6b 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/signaling/settings/Settings.kt +++ b/app/src/main/java/com/nextcloud/talk/models/json/signaling/settings/SignalingSettings.kt @@ -25,10 +25,12 @@ 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 -data class Settings( +@Serializable +data class SignalingSettings( @JsonField(name = ["stunservers"]) var stunServers: List? = null, @JsonField(name = ["turnservers"]) diff --git a/app/src/main/java/com/nextcloud/talk/models/json/signaling/settings/SignalingSettingsOcs.kt b/app/src/main/java/com/nextcloud/talk/models/json/signaling/settings/SignalingSettingsOcs.kt index 473b8a57c..2b3358155 100644 --- a/app/src/main/java/com/nextcloud/talk/models/json/signaling/settings/SignalingSettingsOcs.kt +++ b/app/src/main/java/com/nextcloud/talk/models/json/signaling/settings/SignalingSettingsOcs.kt @@ -33,7 +33,7 @@ data class SignalingSettingsOcs( @JsonField(name = ["meta"]) var meta: GenericMeta?, @JsonField(name = ["data"]) - var settings: Settings? = null + var settings: SignalingSettings? = null ) : Parcelable { // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' constructor() : this(null, null) diff --git a/app/src/main/java/com/nextcloud/talk/users/UserManager.kt b/app/src/main/java/com/nextcloud/talk/users/UserManager.kt new file mode 100644 index 000000000..467742af9 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/users/UserManager.kt @@ -0,0 +1,226 @@ +/* + * Nextcloud Talk application + * + * @author Mario Danic + * @author Andy Scherzinger + * Copyright (C) 2022 Andy Scherzinger + * Copyright (C) 2017 Mario Danic + * + * 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 . + */ +package com.nextcloud.talk.users + +import android.text.TextUtils +import com.bluelinelabs.logansquare.LoganSquare +import com.nextcloud.talk.data.user.UsersRepository +import com.nextcloud.talk.data.user.model.User +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 +import io.reactivex.Maybe +import io.reactivex.Single +import java.lang.Boolean.TRUE + +@Suppress("TooManyFunctions") +class UserManager internal constructor(private val userRepository: UsersRepository) : CurrentUserProviderNew { + val users: Single> + get() = userRepository.getUsers() + + val usersScheduledForDeletion: Single> + get() = userRepository.getUsersScheduledForDeletion() + + private fun setAnyUserAndSetAsActive(): Single { + val results = userRepository.getUsersNotScheduledForDeletion() + + return results.map { users -> + users + .firstOrNull() + ?.apply { + current = true + }.also { user -> + userRepository.updateUser(user!!) + } + } + } + + override val currentUser: Maybe + get() { + return userRepository.getActiveUser() + } + + fun deleteUser(internalId: Long) { + userRepository.deleteUserWithId(internalId) + } + + fun deleteUserWithId(internalId: Long) { + userRepository.deleteUserWithId(internalId) + } + + fun getUserById(userId: String): Maybe { + return userRepository.getUserWithUserId(userId) + } + + fun getUserWithId(id: Long): Maybe { + return userRepository.getUserWithId(id) + } + + fun disableAllUsersWithoutId(userId: Long): Single { + val results = userRepository.getUsersWithoutUserId(userId) + + return results.map { users -> + var count = 0 + if (users.isNotEmpty()) { + for (entity in users) { + entity.current = false + userRepository.updateUser(entity) + count++ + } + } + count + } + } + + fun checkIfUserIsScheduledForDeletion(username: String, server: String): Maybe { + return userRepository.getUserWithUsernameAndServer(username, server).map { it.scheduledForDeletion } + } + + fun getUserWithInternalId(id: Long): Maybe { + return userRepository.getUserWithIdNotScheduledForDeletion(id) + } + + fun getIfUserWithUsernameAndServer(username: String, server: String): Maybe { + return userRepository.getUserWithUsernameAndServer(username, server).map { TRUE } + } + + fun scheduleUserForDeletionWithId(id: Long): Single { + return userRepository.getUserWithId(id).map { user -> + user.scheduledForDeletion = true + user.current = false + userRepository.updateUser(user) + } + .toSingle() + .flatMap { + setAnyUserAndSetAsActive() + }.map { TRUE } + } + + fun createOrUpdateUser( + username: String?, + userAttributes: UserAttributes, + ): Maybe { + + val userMaybe: Maybe = if (userAttributes.id != null) { + userRepository.getUserWithId(userAttributes.id) + } else if (username != null && userAttributes.serverUrl != null) { + userRepository.getUserWithUsernameAndServer(username, userAttributes.serverUrl) + } else { + Maybe.empty() + } + + return userMaybe + .map { user: User? -> + when (user) { + null -> createUser( + username, + userAttributes + ) + else -> { + updateUserData( + user, + userAttributes + ) + user + } + } + } + .switchIfEmpty(Maybe.just(createUser(username, userAttributes))) + .map { user -> + userRepository.insertUser(user) + } + .flatMap { id -> + userRepository.getUserWithId(id) + } + } + + fun getUserWithUsernameAndServer(username: String, server: String): Maybe { + return userRepository.getUserWithUsernameAndServer(username, server) + } + + private fun updateUserData(user: User, userAttributes: UserAttributes) { + user.userId = userAttributes.userId + user.token = userAttributes.token + user.displayName = userAttributes.displayName + if (userAttributes.pushConfigurationState != null) { + user.pushConfigurationState = LoganSquare + .parse(userAttributes.pushConfigurationState, PushConfigurationState::class.java) + } + if (userAttributes.capabilities != null) { + user.capabilities = LoganSquare + .parse(userAttributes.capabilities, Capabilities::class.java) + } + user.clientCertificate = userAttributes.certificateAlias + if (userAttributes.externalSignalingServer != null) { + user.externalSignalingServer = LoganSquare + .parse(userAttributes.externalSignalingServer, ExternalSignalingServer::class.java) + } + user.current = userAttributes.currentUser == true + } + + private fun createUser(username: String?, userAttributes: UserAttributes): User { + val user = User() + user.baseUrl = userAttributes.serverUrl + user.username = username + user.token = userAttributes.token + if (!TextUtils.isEmpty(userAttributes.displayName)) { + user.displayName = userAttributes.displayName + } + if (userAttributes.pushConfigurationState != null) { + user.pushConfigurationState = LoganSquare + .parse(userAttributes.pushConfigurationState, PushConfigurationState::class.java) + } + if (!TextUtils.isEmpty(userAttributes.userId)) { + user.userId = userAttributes.userId + } + if (!TextUtils.isEmpty(userAttributes.capabilities)) { + user.capabilities = LoganSquare.parse(userAttributes.capabilities, Capabilities::class.java) + } + if (!TextUtils.isEmpty(userAttributes.certificateAlias)) { + user.clientCertificate = userAttributes.certificateAlias + } + if (!TextUtils.isEmpty(userAttributes.externalSignalingServer)) { + user.externalSignalingServer = LoganSquare + .parse(userAttributes.externalSignalingServer, ExternalSignalingServer::class.java) + } + user.current = true + return user + } + + companion object { + const val TAG = "UserManager" + } + + data class UserAttributes( + val id: Long?, + val serverUrl: String?, + val currentUser: Boolean?, + val userId: String?, + val token: String?, + val displayName: String?, + val pushConfigurationState: String?, + val capabilities: String?, + val certificateAlias: String?, + val externalSignalingServer: String? + ) +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java b/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java index 9ca98882d..a493dc785 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/ApiUtils.java +++ b/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.User; import com.nextcloud.talk.models.RetrofitBucket; import com.nextcloud.talk.models.database.CapabilitiesUtil; import com.nextcloud.talk.models.database.UserEntity; @@ -123,7 +124,7 @@ public class ApiUtils { return getConversationApiVersion(capabilities, versions); } - public static int getConversationApiVersion(UserEntity user, int[] versions) throws NoSupportedApiException { + public static int getConversationApiVersion(User user, int[] versions) throws NoSupportedApiException { boolean hasApiV4 = false; for (int version : versions) { hasApiV4 |= version == APIv4; @@ -135,18 +136,18 @@ public class ApiUtils { } for (int version : versions) { - if (CapabilitiesUtil.hasSpreedFeatureCapability(user, "conversation-v" + version)) { + if (user.hasSpreedFeatureCapability("conversation-v" + version)) { return version; } // Fallback for old API versions if ((version == APIv1 || version == APIv2)) { - if (CapabilitiesUtil.hasSpreedFeatureCapability(user, "conversation-v2")) { + if (user.hasSpreedFeatureCapability("conversation-v2")) { return version; } if (version == APIv1 && - CapabilitiesUtil.hasSpreedFeatureCapability(user, "mention-flag") && - !CapabilitiesUtil.hasSpreedFeatureCapability(user, "conversation-v4")) { + user.hasSpreedFeatureCapability("mention-flag") && + !user.hasSpreedFeatureCapability("conversation-v4")) { return version; } } @@ -154,6 +155,11 @@ public class ApiUtils { throw new NoSupportedApiException(); } + @Deprecated + public static int getConversationApiVersion(UserEntity user, int[] versions) throws NoSupportedApiException { + return getConversationApiVersion(LegacyUserEntityMapper.toModel(user), versions); + } + public static int getSignalingApiVersion(UserEntity user, int[] versions) throws NoSupportedApiException { for (int version : versions) { if (CapabilitiesUtil.hasSpreedFeatureCapability(user, "signaling-v" + version)) { diff --git a/app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.java b/app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.java index 526b09c04..ed52c1f44 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/DisplayUtils.java +++ b/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.User; import com.nextcloud.talk.events.UserMentionClickEvent; import com.nextcloud.talk.models.database.UserEntity; import com.nextcloud.talk.utils.text.Spans; @@ -92,6 +93,7 @@ import java.text.DateFormat; import java.util.Date; import java.util.HashMap; import java.util.Map; +import java.util.Objects; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -489,9 +491,13 @@ public class DisplayUtils { * @param color the color * @return true if primaryColor is lighter than MAX_LIGHTNESS */ + @SuppressWarnings("correctness") public static boolean lightTheme(int color) { float[] hsl = colorToHSL(color); + // spotbugs dislikes fixed index access + // which is enforced by having such an + // array from Android-API itself return hsl[INDEX_LUMINATION] >= MAX_LIGHTNESS; } @@ -565,7 +571,12 @@ public class DisplayUtils { } } + @Deprecated public static void loadAvatarImage(UserEntity user, SimpleDraweeView avatarImageView, boolean deleteCache) { + loadAvatarImage(Objects.requireNonNull(LegacyUserEntityMapper.toModel(user)), avatarImageView, deleteCache); + } + + public static void loadAvatarImage(User user, SimpleDraweeView avatarImageView, boolean deleteCache) { String avatarId; if (!TextUtils.isEmpty(user.getUserId())) { avatarId = user.getUserId(); diff --git a/app/src/main/java/com/nextcloud/talk/utils/LegacyUserEntityMapper.kt b/app/src/main/java/com/nextcloud/talk/utils/LegacyUserEntityMapper.kt new file mode 100644 index 000000000..c265f0cf9 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/LegacyUserEntityMapper.kt @@ -0,0 +1,56 @@ +/* + * Nextcloud Talk application + * + * @author Andy Scherzinger + * Copyright (C) 2022 Andy Scherzinger + * + * model 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. + * + * model 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 model program. If not, see . + */ + +package com.nextcloud.talk.utils + +import com.bluelinelabs.logansquare.LoganSquare +import com.nextcloud.talk.data.user.model.User +import com.nextcloud.talk.models.ExternalSignalingServer +import com.nextcloud.talk.models.database.UserEntity +import com.nextcloud.talk.models.json.capabilities.Capabilities +import com.nextcloud.talk.models.json.push.PushConfigurationState + +object LegacyUserEntityMapper { + fun toModel(entities: List?): List { + return entities?.map { user: UserEntity? -> + toModel(user)!! + } ?: emptyList() + } + + @JvmStatic + fun toModel(entity: UserEntity?): User? { + return entity?.let { + User( + entity.id, + entity.userId, + entity.username, + entity.baseUrl, + entity.token, + entity.displayName, + entity.pushConfigurationState?.let { LoganSquare.parse(it, PushConfigurationState::class.java) }, + entity.capabilities?.let { LoganSquare.parse(it, Capabilities::class.java) }, + entity.clientCertificate, + entity.externalSignalingServer?.let { LoganSquare.parse(it, ExternalSignalingServer::class.java) }, + entity.current, + entity.scheduledForDeletion + ) + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/database/arbitrarystorage/ArbitraryStorageModule.java b/app/src/main/java/com/nextcloud/talk/utils/database/arbitrarystorage/ArbitraryStorageModule.java index b54c358b4..ded6889c6 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/database/arbitrarystorage/ArbitraryStorageModule.java +++ b/app/src/main/java/com/nextcloud/talk/utils/database/arbitrarystorage/ArbitraryStorageModule.java @@ -21,7 +21,10 @@ package com.nextcloud.talk.utils.database.arbitrarystorage; import autodagger.AutoInjector; import com.nextcloud.talk.application.NextcloudTalkApplication; +import com.nextcloud.talk.arbitrarystorage.ArbitraryStorageManager; import com.nextcloud.talk.dagger.modules.DatabaseModule; +import com.nextcloud.talk.data.storage.ArbitraryStoragesRepository; + import dagger.Module; import dagger.Provides; import io.requery.Persistable; @@ -41,4 +44,9 @@ public class ArbitraryStorageModule { public ArbitraryStorageUtils provideArbitraryStorageUtils(ReactiveEntityStore dataStore) { return new ArbitraryStorageUtils(dataStore); } + + @Provides + public ArbitraryStorageManager provideArbitraryStorageManager(ArbitraryStoragesRepository repository) { + return new ArbitraryStorageManager(repository); + } } diff --git a/app/src/main/java/com/nextcloud/talk/utils/database/arbitrarystorage/ArbitraryStorageUtils.java b/app/src/main/java/com/nextcloud/talk/utils/database/arbitrarystorage/ArbitraryStorageUtils.java index d9db2d559..92abd42ec 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/database/arbitrarystorage/ArbitraryStorageUtils.java +++ b/app/src/main/java/com/nextcloud/talk/utils/database/arbitrarystorage/ArbitraryStorageUtils.java @@ -20,8 +20,10 @@ package com.nextcloud.talk.utils.database.arbitrarystorage; import androidx.annotation.Nullable; + import com.nextcloud.talk.models.database.ArbitraryStorage; import com.nextcloud.talk.models.database.ArbitraryStorageEntity; + import io.reactivex.Observable; import io.reactivex.schedulers.Schedulers; import io.requery.Persistable; @@ -29,6 +31,10 @@ import io.requery.query.Result; import io.requery.reactivex.ReactiveEntityStore; import io.requery.reactivex.ReactiveScalar; +/** + * @deprecated use {@link com.nextcloud.talk.arbitrarystorage.ArbitraryStorageManager} instead. + */ +@Deprecated public class ArbitraryStorageUtils { private ReactiveEntityStore dataStore; @@ -45,16 +51,16 @@ public class ArbitraryStorageUtils { arbitraryStorageEntity.setObject(object); dataStore.upsert(arbitraryStorageEntity) - .toObservable() - .subscribeOn(Schedulers.io()) - .subscribe(); + .toObservable() + .subscribeOn(Schedulers.io()) + .subscribe(); } public ArbitraryStorageEntity getStorageSetting(long accountIdentifier, String key, @Nullable String object) { Result findStorageQueryResult = dataStore.select(ArbitraryStorage.class) - .where(ArbitraryStorageEntity.ACCOUNT_IDENTIFIER.eq(accountIdentifier) - .and(ArbitraryStorageEntity.KEY.eq(key)).and(ArbitraryStorageEntity.OBJECT.eq(object))) - .limit(1).get(); + .where(ArbitraryStorageEntity.ACCOUNT_IDENTIFIER.eq(accountIdentifier) + .and(ArbitraryStorageEntity.KEY.eq(key)).and(ArbitraryStorageEntity.OBJECT.eq(object))) + .limit(1).get(); return (ArbitraryStorageEntity) findStorageQueryResult.firstOrNull(); } @@ -63,6 +69,6 @@ public class ArbitraryStorageUtils { ReactiveScalar deleteResult = dataStore.delete(ArbitraryStorage.class).where(ArbitraryStorageEntity.ACCOUNT_IDENTIFIER.eq(accountIdentifier)).get(); return deleteResult.single().toObservable() - .subscribeOn(Schedulers.io()); + .subscribeOn(Schedulers.io()); } } diff --git a/app/src/main/java/com/nextcloud/talk/utils/database/user/CapabilitiesUtilNew.kt b/app/src/main/java/com/nextcloud/talk/utils/database/user/CapabilitiesUtilNew.kt new file mode 100644 index 000000000..175753bb3 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/database/user/CapabilitiesUtilNew.kt @@ -0,0 +1,154 @@ +/* + * Nextcloud Talk application + * + * @author Andy Scherzinger + * @author Mario Danic + * Copyright (C) 2021 Andy Scherzinger (info@andy-scherzinger.de) + * Copyright (C) 2017-2018 Mario Danic + * + * 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 . + */ +package com.nextcloud.talk.utils.database.user + +import com.nextcloud.talk.data.user.model.User + +@Suppress("TooManyFunctions") +object CapabilitiesUtilNew { + fun hasNotificationsCapability(user: User, capabilityName: String): Boolean { + return user.capabilities?.spreedCapability?.features?.contains(capabilityName) == true + } + + fun hasExternalCapability(user: User, capabilityName: String?): Boolean { + if (user.capabilities?.externalCapability?.containsKey("v1") == true) { + return user.capabilities!!.externalCapability!!["v1"]?.contains(capabilityName!!) == true + } + return false + } + + fun isServerEOL(user: User): Boolean { + // Capability is available since Talk 4 => Nextcloud 14 => Autmn 2018 + return !hasSpreedFeatureCapability(user, "no-ping") + } + + fun isServerAlmostEOL(user: User): Boolean { + // Capability is available since Talk 8 => Nextcloud 18 => January 2020 + return !hasSpreedFeatureCapability(user, "chat-replies") + } + + fun canSetChatReadMarker(user: User): Boolean { + return hasSpreedFeatureCapability(user, "chat-read-marker") + } + + fun hasSpreedFeatureCapability(user: User, capabilityName: String): Boolean { + if (user.capabilities?.spreedCapability?.features != null) { + return user.capabilities!!.spreedCapability!!.features!!.contains(capabilityName) + } + return false + } + + fun getMessageMaxLength(user: User): Int { + val capabilities = user.capabilities!! + if (user.capabilities?.spreedCapability?.config?.containsKey("chat") == true) { + val chatConfigHashMap = user.capabilities!!.spreedCapability!!.config!!["chat"] + if (chatConfigHashMap?.containsKey("max-length") == true) { + val chatSize = chatConfigHashMap["max-length"]!!.toInt() + return if (chatSize > 0) { + chatSize + } else { + DEFAULT_CHAT_SIZE + } + } + } + + return DEFAULT_CHAT_SIZE + } + + fun isPhoneBookIntegrationAvailable(user: User): Boolean { + return user.capabilities?.spreedCapability?.features?.contains("phonebook-search") == true + } + + fun isReadStatusAvailable(user: User): Boolean { + if (user.capabilities?.spreedCapability?.config?.containsKey("chat") == true) { + val map: Map? = user.capabilities!!.spreedCapability!!.config!!["chat"] + return map != null && map.containsKey("read-privacy") + } + return false + } + + fun isReadStatusPrivate(user: User): Boolean { + if (user.capabilities?.spreedCapability?.config?.containsKey("chat") == true) { + val map = user.capabilities!!.spreedCapability!!.config!!["chat"] + if (map?.containsKey("read-privacy") == true) { + return map["read-privacy"]!!.toInt() == 1 + } + } + + return false + } + + fun isUserStatusAvailable(user: User): Boolean { + return user.capabilities?.userStatusCapability?.enabled == true && + user.capabilities?.userStatusCapability?.supportsEmoji == true + } + + fun getAttachmentFolder(user: User): String? { + if (user.capabilities?.spreedCapability?.config?.containsKey("attachments") == true) { + val map = user.capabilities!!.spreedCapability!!.config!!["attachments"] + if (map?.containsKey("folder") == true) { + return map["folder"] + } + } + return "/Talk" + } + + fun getServerName(user: User): String? { + if (user.capabilities?.themingCapability != null) { + return user.capabilities!!.themingCapability!!.name + } + return "" + } + + // TODO later avatar can also be checked via user fields, for now it is in Talk capability + fun isAvatarEndpointAvailable(user: User): Boolean { + return user.capabilities?.spreedCapability?.features?.contains("temp-user-avatar-api") == true + } + + fun canEditScopes(user: User): Boolean { + return user.capabilities?.provisioningCapability?.accountPropertyScopesVersion != null && + user.capabilities!!.provisioningCapability!!.accountPropertyScopesVersion!! > 1 + } + + fun isAbleToCall(user: User): Boolean { + if (user.capabilities != null) { + val capabilities = user.capabilities + return if ( + capabilities?.spreedCapability?.config?.containsKey("call") == true && + capabilities.spreedCapability!!.config!!["call"] != null && + capabilities.spreedCapability!!.config!!["call"]!!.containsKey("enabled") + ) { + java.lang.Boolean.parseBoolean(capabilities.spreedCapability!!.config!!["call"]!!["enabled"]) + } else { + // older nextcloud versions without the capability can't disable the calls + true + } + } + return false + } + + fun isUnifiedSearchAvailable(user: User): Boolean { + return hasSpreedFeatureCapability(user, "unified-search") + } + + const val DEFAULT_CHAT_SIZE = 1000 +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/database/user/CurrentUserProviderNew.kt b/app/src/main/java/com/nextcloud/talk/utils/database/user/CurrentUserProviderNew.kt new file mode 100644 index 000000000..b1b457ce5 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/database/user/CurrentUserProviderNew.kt @@ -0,0 +1,28 @@ +/* + * 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 . + */ +package com.nextcloud.talk.utils.database.user + +import com.nextcloud.talk.data.user.model.User +import io.reactivex.Maybe + +interface CurrentUserProviderNew { + val currentUser: Maybe +} diff --git a/app/src/main/java/com/nextcloud/talk/utils/database/user/UserModule.kt b/app/src/main/java/com/nextcloud/talk/utils/database/user/UserModule.kt index aad73490a..d29eedb56 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/database/user/UserModule.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/database/user/UserModule.kt @@ -20,6 +20,8 @@ package com.nextcloud.talk.utils.database.user import com.nextcloud.talk.dagger.modules.DatabaseModule +import com.nextcloud.talk.data.user.UsersRepository +import com.nextcloud.talk.users.UserManager import dagger.Binds import dagger.Module import dagger.Provides @@ -32,10 +34,18 @@ abstract class UserModule { @Binds abstract fun bindCurrentUserProvider(userUtils: UserUtils): CurrentUserProvider + @Binds + abstract fun bindCurrentUserProviderNew(userManager: UserManager): CurrentUserProviderNew + companion object { @Provides fun provideUserUtils(dataStore: ReactiveEntityStore?): UserUtils { return UserUtils(dataStore) } + + @Provides + fun provideUserManager(userRepository: UsersRepository): UserManager { + return UserManager(userRepository) + } } } diff --git a/app/src/main/java/com/nextcloud/talk/utils/database/user/UserUtils.java b/app/src/main/java/com/nextcloud/talk/utils/database/user/UserUtils.java index 4dad177fb..dc97b6ea0 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/database/user/UserUtils.java +++ b/app/src/main/java/com/nextcloud/talk/utils/database/user/UserUtils.java @@ -35,6 +35,10 @@ import io.requery.Persistable; import io.requery.query.Result; import io.requery.reactivex.ReactiveEntityStore; +/** + * @deprecated use {@link com.nextcloud.talk.users.UserManager} instead. + */ +@Deprecated public class UserUtils implements CurrentUserProvider { private ReactiveEntityStore dataStore; @@ -44,24 +48,24 @@ public class UserUtils implements CurrentUserProvider { public boolean anyUserExists() { return (dataStore.count(User.class).where(UserEntity.SCHEDULED_FOR_DELETION.notEqual(Boolean.TRUE)) - .limit(1).get().value() > 0); + .limit(1).get().value() > 0); } public boolean hasMultipleUsers() { return (dataStore.count(User.class).where(UserEntity.SCHEDULED_FOR_DELETION.notEqual(Boolean.TRUE)) - .get().value() > 1); + .get().value() > 1); } public List getUsers() { Result findUsersQueryResult = dataStore.select(User.class).where - (UserEntity.SCHEDULED_FOR_DELETION.notEqual(Boolean.TRUE)).get(); + (UserEntity.SCHEDULED_FOR_DELETION.notEqual(Boolean.TRUE)).get(); return findUsersQueryResult.toList(); } public List getUsersScheduledForDeletion() { Result findUsersQueryResult = dataStore.select(User.class) - .where(UserEntity.SCHEDULED_FOR_DELETION.eq(Boolean.TRUE)).get(); + .where(UserEntity.SCHEDULED_FOR_DELETION.eq(Boolean.TRUE)).get(); return findUsersQueryResult.toList(); } @@ -69,8 +73,8 @@ public class UserUtils implements CurrentUserProvider { public UserEntity getAnyUserAndSetAsActive() { Result findUserQueryResult = dataStore.select(User.class) - .where(UserEntity.SCHEDULED_FOR_DELETION.notEqual(Boolean.TRUE)) - .limit(1).get(); + .where(UserEntity.SCHEDULED_FOR_DELETION.notEqual(Boolean.TRUE)) + .limit(1).get(); UserEntity userEntity; if ((userEntity = (UserEntity) findUserQueryResult.firstOrNull()) != null) { @@ -83,10 +87,11 @@ public class UserUtils implements CurrentUserProvider { } @Override - public @Nullable UserEntity getCurrentUser() { + public @Nullable + UserEntity getCurrentUser() { Result findUserQueryResult = dataStore.select(User.class).where(UserEntity.CURRENT.eq(Boolean.TRUE) - .and(UserEntity.SCHEDULED_FOR_DELETION.notEqual(Boolean.TRUE))) - .limit(1).get(); + .and(UserEntity.SCHEDULED_FOR_DELETION.notEqual(Boolean.TRUE))) + .limit(1).get(); return (UserEntity) findUserQueryResult.firstOrNull(); } @@ -97,8 +102,8 @@ public class UserUtils implements CurrentUserProvider { UserEntity user = (UserEntity) findUserQueryResult.firstOrNull(); return dataStore.delete(user) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()); + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()); } @@ -108,20 +113,20 @@ public class UserUtils implements CurrentUserProvider { UserEntity user = (UserEntity) findUserQueryResult.firstOrNull(); return dataStore.delete(user) - .subscribeOn(Schedulers.io()); + .subscribeOn(Schedulers.io()); } public UserEntity getUserById(String id) { Result findUserQueryResult = dataStore.select(User.class).where(UserEntity.USER_ID.eq(id)) - .limit(1).get(); + .limit(1).get(); return (UserEntity) findUserQueryResult.firstOrNull(); } public UserEntity getUserWithId(long id) { Result findUserQueryResult = dataStore.select(User.class).where(UserEntity.ID.eq(id)) - .limit(1).get(); + .limit(1).get(); return (UserEntity) findUserQueryResult.firstOrNull(); } @@ -139,8 +144,8 @@ public class UserUtils implements CurrentUserProvider { public boolean checkIfUserIsScheduledForDeletion(String username, String server) { Result findUserQueryResult = dataStore.select(User.class).where(UserEntity.USERNAME.eq(username)) - .and(UserEntity.BASE_URL.eq(server)) - .limit(1).get(); + .and(UserEntity.BASE_URL.eq(server)) + .limit(1).get(); UserEntity userEntity; if ((userEntity = (UserEntity) findUserQueryResult.firstOrNull()) != null) { @@ -152,23 +157,23 @@ public class UserUtils implements CurrentUserProvider { public UserEntity getUserWithInternalId(long internalId) { Result findUserQueryResult = dataStore.select(User.class).where(UserEntity.ID.eq(internalId) - .and(UserEntity.SCHEDULED_FOR_DELETION.notEqual(Boolean.TRUE))) - .limit(1).get(); + .and(UserEntity.SCHEDULED_FOR_DELETION.notEqual(Boolean.TRUE))) + .limit(1).get(); return (UserEntity) findUserQueryResult.firstOrNull(); } public boolean getIfUserWithUsernameAndServer(String username, String server) { Result findUserQueryResult = dataStore.select(User.class).where(UserEntity.USERNAME.eq(username) - .and(UserEntity.BASE_URL.eq(server))) - .limit(1).get(); + .and(UserEntity.BASE_URL.eq(server))) + .limit(1).get(); return findUserQueryResult.firstOrNull() != null; } public boolean scheduleUserForDeletionWithId(long id) { Result findUserQueryResult = dataStore.select(User.class).where(UserEntity.ID.eq(id)) - .limit(1).get(); + .limit(1).get(); UserEntity userEntity; if ((userEntity = (UserEntity) findUserQueryResult.firstOrNull()) != null) { @@ -193,7 +198,7 @@ public class UserUtils implements CurrentUserProvider { Result findUserQueryResult; if (internalId == null) { findUserQueryResult = dataStore.select(User.class).where(UserEntity.USERNAME.eq(username). - and(UserEntity.BASE_URL.eq(serverUrl))).limit(1).get(); + and(UserEntity.BASE_URL.eq(serverUrl))).limit(1).get(); } else { findUserQueryResult = dataStore.select(User.class).where(UserEntity.ID.eq(internalId)).get(); } @@ -242,7 +247,7 @@ public class UserUtils implements CurrentUserProvider { } if ((displayName != null && user.getDisplayName() == null) || (displayName != null && user.getDisplayName() - != null && !displayName.equals(user.getDisplayName()))) { + != null && !displayName.equals(user.getDisplayName()))) { user.setDisplayName(displayName); } diff --git a/app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferences.java b/app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferences.java index b542fddd6..96b53631d 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferences.java +++ b/app/src/main/java/com/nextcloud/talk/utils/preferences/AppPreferences.java @@ -284,10 +284,17 @@ public interface AppPreferences { @KeyByString("db_cypher_v4_upgrade") @DefaultValue(R.bool.value_true) - boolean getIsDbCypherToUpgrade(); + boolean isDbCypherToUpgrade(); @KeyByString("db_cypher_v4_upgrade") - void setIsDbCypherToUpgrade(boolean value); + void setDbCypherToUpgrade(boolean value); + + @KeyByString("db_room_migrated") + @DefaultValue(R.bool.value_false) + boolean getIsDbRoomMigrated(); + + @KeyByString("db_room_migrated") + void setIsDbRoomMigrated(boolean value); @KeyByResource(R.string.nc_settings_phone_book_integration_key) @RegisterChangeListenerMethod diff --git a/app/src/main/java/com/nextcloud/talk/utils/preferences/preferencestorage/DatabaseStorageModule.java b/app/src/main/java/com/nextcloud/talk/utils/preferences/preferencestorage/DatabaseStorageModule.java index 3ec3b2eb8..3e4601089 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/preferences/preferencestorage/DatabaseStorageModule.java +++ b/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"; diff --git a/build.gradle b/build.gradle index d45a09421..dfc0d62f2 100644 --- a/build.gradle +++ b/build.gradle @@ -36,6 +36,7 @@ buildscript { dependencies { classpath 'com.android.tools.build:gradle:7.2.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}" + classpath "org.jetbrains.kotlin:kotlin-serialization:${kotlinVersion}" classpath 'gradle.plugin.com.github.spotbugs.snom:spotbugs-gradle-plugin:4.7.5' classpath "io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.20.0" classpath "org.jlleitschuh.gradle:ktlint-gradle:10.3.0" diff --git a/scripts/analysis/findbugs-results.txt b/scripts/analysis/findbugs-results.txt index 2efea5198..09df92759 100644 --- a/scripts/analysis/findbugs-results.txt +++ b/scripts/analysis/findbugs-results.txt @@ -1 +1 @@ -167 \ No newline at end of file +166 \ No newline at end of file diff --git a/spotbugs-filter.xml b/spotbugs-filter.xml index 002977d77..f04773e2e 100644 --- a/spotbugs-filter.xml +++ b/spotbugs-filter.xml @@ -34,6 +34,16 @@ + + + + + + + + + +