From 4ce54bc21ed9c8a1add24d2565a476c0f5530c49 Mon Sep 17 00:00:00 2001 From: Chris Narkiewicz Date: Sat, 30 Nov 2019 01:51:13 +0000 Subject: [PATCH] Make user model parcelable Signed-off-by: Chris Narkiewicz --- .../client/account/AnonymousUserTest.kt | 53 ++++++++ .../client/account/RegisteredUserTest.kt | 119 ++++++++++++++++++ .../nextcloud/client/account/AnonymousUser.kt | 26 +++- .../client/account/RegisteredUser.kt | 31 ++++- .../com/nextcloud/client/account/Server.kt | 25 +++- .../java/com/nextcloud/client/account/User.kt | 10 +- .../com/nextcloud/client/etm/EtmViewModel.kt | 42 ++++++- .../client/etm/pages/EtmAccountsFragment.kt | 79 ++++++++++++ src/main/res/layout/fragment_etm_accounts.xml | 33 +++++ src/main/res/menu/fragment_etm_accounts.xml | 33 +++++ src/main/res/values/strings.xml | 1 + .../nextcloud/client/etm/TestEtmViewModel.kt | 12 +- 12 files changed, 456 insertions(+), 8 deletions(-) create mode 100644 src/androidTest/java/com/nextcloud/client/account/AnonymousUserTest.kt create mode 100644 src/androidTest/java/com/nextcloud/client/account/RegisteredUserTest.kt create mode 100644 src/main/java/com/nextcloud/client/etm/pages/EtmAccountsFragment.kt create mode 100644 src/main/res/layout/fragment_etm_accounts.xml create mode 100644 src/main/res/menu/fragment_etm_accounts.xml diff --git a/src/androidTest/java/com/nextcloud/client/account/AnonymousUserTest.kt b/src/androidTest/java/com/nextcloud/client/account/AnonymousUserTest.kt new file mode 100644 index 0000000000..eb9f3f49ec --- /dev/null +++ b/src/androidTest/java/com/nextcloud/client/account/AnonymousUserTest.kt @@ -0,0 +1,53 @@ +/* + * Nextcloud Android client application + * + * @author Chris Narkiewicz + * Copyright (C) 2020 Chris Narkiewicz + * Copyright (C) 2020 Nextcloud GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package com.nextcloud.client.account + +import android.os.Parcel +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Assert +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class AnonymousUserTest { + @Test + fun anonymousUserImplementsParcelable() { + // GIVEN + // anonymous user instance + val original = AnonymousUser("test_account") + + // WHEN + // instance is serialized into Parcel + // instance is retrieved from Parcel + val parcel = Parcel.obtain() + parcel.setDataPosition(0) + parcel.writeParcelable(original, 0) + parcel.setDataPosition(0) + val retrieved = parcel.readParcelable(User::class.java.classLoader) + + // THEN + // retrieved instance in distinct + // instances are equal + Assert.assertNotSame(original, retrieved) + Assert.assertTrue(retrieved is AnonymousUser) + Assert.assertEquals(original, retrieved) + } +} diff --git a/src/androidTest/java/com/nextcloud/client/account/RegisteredUserTest.kt b/src/androidTest/java/com/nextcloud/client/account/RegisteredUserTest.kt new file mode 100644 index 0000000000..fc78b5e46c --- /dev/null +++ b/src/androidTest/java/com/nextcloud/client/account/RegisteredUserTest.kt @@ -0,0 +1,119 @@ +/* + * Nextcloud Android client application + * + * @author Chris Narkiewicz + * Copyright (C) 2020 Chris Narkiewicz + * Copyright (C) 2020 Nextcloud GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package com.nextcloud.client.account + +import android.accounts.Account +import android.net.Uri +import android.os.Parcel +import com.owncloud.android.lib.common.OwnCloudAccount +import com.owncloud.android.lib.common.OwnCloudBasicCredentials +import com.owncloud.android.lib.resources.status.OwnCloudVersion +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotSame +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import java.net.URI + +class RegisteredUserTest { + + private companion object { + fun buildTestUser(accountName: String): RegisteredUser { + val uri = Uri.parse("https://nextcloud.localhost.localdomain") + val credentials = OwnCloudBasicCredentials("user", "pass") + val account = Account(accountName, "test-type") + val ownCloudAccount = OwnCloudAccount(uri, credentials) + val server = Server( + uri = URI(uri.toString()), + version = OwnCloudVersion.nextcloud_17 + ) + return RegisteredUser( + account = account, + ownCloudAccount = ownCloudAccount, + server = server + ) + } + } + + private lateinit var user: RegisteredUser + + @Before + fun setUp() { + user = buildTestUser("test@nextcloud.localhost.localdomain") + } + + @Test + fun registeredUserImplementsParcelable() { + // GIVEN + // registered user instance + + // WHEN + // instance is serialized into Parcel + // instance is retrieved from Parcel + val parcel = Parcel.obtain() + parcel.setDataPosition(0) + parcel.writeParcelable(user, 0) + parcel.setDataPosition(0) + val deserialized = parcel.readParcelable(User::class.java.classLoader) + + // THEN + // retrieved instance in distinct + // instances are equal + assertNotSame(user, deserialized) + assertTrue(deserialized is RegisteredUser) + assertEquals(user, deserialized) + } + + @Test + fun accountNamesEquality() { + // GIVEN + // registered user instance with lower-case account name + // registered user instance with mixed-case account name + val user1 = buildTestUser("account_name") + val user2 = buildTestUser("Account_Name") + + // WHEN + // account names are checked for equality + val equal = user1.nameEquals(user2) + + // THEN + // account names are equal + assertTrue(equal) + } + + @Test + fun accountNamesEqualityCheckIsNullSafe() { + // GIVEN + // registered user instance with lower-case account name + // null account + val user1 = buildTestUser("account_name") + val user2: User? = null + + // WHEN + // account names are checked for equality against null + val equal = user1.nameEquals(user2) + + // THEN + // account names are not equal + assertFalse(equal) + } +} diff --git a/src/main/java/com/nextcloud/client/account/AnonymousUser.kt b/src/main/java/com/nextcloud/client/account/AnonymousUser.kt index ec2a284e2c..1682fe518d 100644 --- a/src/main/java/com/nextcloud/client/account/AnonymousUser.kt +++ b/src/main/java/com/nextcloud/client/account/AnonymousUser.kt @@ -23,6 +23,8 @@ package com.nextcloud.client.account import android.accounts.Account import android.content.Context import android.net.Uri +import android.os.Parcel +import android.os.Parcelable import com.owncloud.android.MainApp import com.owncloud.android.R import com.owncloud.android.lib.common.OwnCloudAccount @@ -34,7 +36,7 @@ import java.net.URI * It serves as a semantically correct "empty value", allowing simplification of logic * in various components requiring user data, such as DB queries. */ -internal class AnonymousUser(private val accountType: String) : User { +internal data class AnonymousUser(private val accountType: String) : User, Parcelable { companion object { @JvmStatic @@ -42,9 +44,19 @@ internal class AnonymousUser(private val accountType: String) : User { val type = context.getString(R.string.account_type) return AnonymousUser(type) } + + @JvmField + val CREATOR: Parcelable.Creator = object : Parcelable.Creator { + override fun createFromParcel(source: Parcel): AnonymousUser = AnonymousUser(source) + override fun newArray(size: Int): Array = arrayOfNulls(size) + } } - override val accountName: String = "anonymous" + private constructor(source: Parcel) : this( + source.readString() as String + ) + + override val accountName: String = "anonymous@nohost" override val server = Server(URI.create(""), MainApp.MINIMUM_SUPPORTED_SERVER_VERSION) override val isAnonymous = true @@ -55,4 +67,14 @@ internal class AnonymousUser(private val accountType: String) : User { override fun toOwnCloudAccount(): OwnCloudAccount { return OwnCloudAccount(Uri.EMPTY, OwnCloudBasicCredentials("", "")) } + + override fun nameEquals(user: User?): Boolean { + return user?.accountName.equals(accountName, true) + } + + override fun describeContents() = 0 + + override fun writeToParcel(dest: Parcel, flags: Int) = with(dest) { + writeString(accountType) + } } diff --git a/src/main/java/com/nextcloud/client/account/RegisteredUser.kt b/src/main/java/com/nextcloud/client/account/RegisteredUser.kt index 4ba9565521..8b8ee73cdd 100644 --- a/src/main/java/com/nextcloud/client/account/RegisteredUser.kt +++ b/src/main/java/com/nextcloud/client/account/RegisteredUser.kt @@ -21,16 +21,33 @@ package com.nextcloud.client.account import android.accounts.Account +import android.os.Parcel +import android.os.Parcelable import com.owncloud.android.lib.common.OwnCloudAccount /** * This class represents normal user logged into the Nextcloud server. */ -internal class RegisteredUser( +internal data class RegisteredUser( private val account: Account, private val ownCloudAccount: OwnCloudAccount, override val server: Server ) : User { + + companion object { + @JvmField + val CREATOR: Parcelable.Creator = object : Parcelable.Creator { + override fun createFromParcel(source: Parcel): RegisteredUser = RegisteredUser(source) + override fun newArray(size: Int): Array = arrayOfNulls(size) + } + } + + private constructor(source: Parcel) : this( + source.readParcelable(Account::class.java.classLoader) as Account, + source.readParcelable(OwnCloudAccount::class.java.classLoader) as OwnCloudAccount, + source.readParcelable(Server::class.java.classLoader) as Server + ) + override val isAnonymous = false override val accountName: String get() { @@ -44,4 +61,16 @@ internal class RegisteredUser( override fun toOwnCloudAccount(): OwnCloudAccount { return ownCloudAccount } + + override fun nameEquals(user: User?): Boolean { + return user?.accountName.equals(accountName, true) + } + + override fun describeContents() = 0 + + override fun writeToParcel(dest: Parcel, flags: Int) = with(dest) { + writeParcelable(account, 0) + writeParcelable(ownCloudAccount, 0) + writeParcelable(server, 0) + } } diff --git a/src/main/java/com/nextcloud/client/account/Server.kt b/src/main/java/com/nextcloud/client/account/Server.kt index 0a44dfb4ec..cbce5d8b9c 100644 --- a/src/main/java/com/nextcloud/client/account/Server.kt +++ b/src/main/java/com/nextcloud/client/account/Server.kt @@ -20,6 +20,8 @@ */ package com.nextcloud.client.account +import android.os.Parcel +import android.os.Parcelable import com.owncloud.android.lib.resources.status.OwnCloudVersion import java.net.URI @@ -27,4 +29,25 @@ import java.net.URI * This object provides all information necessary to interact * with backend server. */ -data class Server(val uri: URI, val version: OwnCloudVersion) +data class Server(val uri: URI, val version: OwnCloudVersion) : Parcelable { + + constructor(source: Parcel) : this( + source.readSerializable() as URI, + source.readParcelable(OwnCloudVersion::class.java.classLoader) as OwnCloudVersion + ) + + override fun describeContents() = 0 + + override fun writeToParcel(dest: Parcel, flags: Int) = with(dest) { + writeSerializable(uri) + writeParcelable(version, 0) + } + + companion object { + @JvmField + val CREATOR: Parcelable.Creator = object : Parcelable.Creator { + override fun createFromParcel(source: Parcel): Server = Server(source) + override fun newArray(size: Int): Array = arrayOfNulls(size) + } + } +} diff --git a/src/main/java/com/nextcloud/client/account/User.kt b/src/main/java/com/nextcloud/client/account/User.kt index 0500d8358a..f18f26f263 100644 --- a/src/main/java/com/nextcloud/client/account/User.kt +++ b/src/main/java/com/nextcloud/client/account/User.kt @@ -21,9 +21,10 @@ package com.nextcloud.client.account import android.accounts.Account +import android.os.Parcelable import com.owncloud.android.lib.common.OwnCloudAccount -interface User { +interface User : Parcelable { val accountName: String val server: Server val isAnonymous: Boolean @@ -51,4 +52,11 @@ interface User { */ @Deprecated("Temporary workaround") fun toOwnCloudAccount(): OwnCloudAccount + + /** + * Compare account names, case insensitive. + * + * @return true if account names are same, false otherwise + */ + fun nameEquals(user: User?): Boolean } diff --git a/src/main/java/com/nextcloud/client/etm/EtmViewModel.kt b/src/main/java/com/nextcloud/client/etm/EtmViewModel.kt index e1d96d44c7..60e2b37cad 100644 --- a/src/main/java/com/nextcloud/client/etm/EtmViewModel.kt +++ b/src/main/java/com/nextcloud/client/etm/EtmViewModel.kt @@ -19,23 +19,53 @@ */ package com.nextcloud.client.etm +import android.accounts.Account +import android.accounts.AccountManager import android.content.SharedPreferences +import android.content.res.Resources import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import com.nextcloud.client.etm.pages.EtmAccountsFragment import com.nextcloud.client.etm.pages.EtmPreferencesFragment import com.owncloud.android.R +import com.owncloud.android.lib.common.accounts.AccountUtils import javax.inject.Inject class EtmViewModel @Inject constructor( - private val defaultPreferences: SharedPreferences + private val defaultPreferences: SharedPreferences, + private val platformAccountManager: AccountManager, + private val resources: Resources ) : ViewModel() { + + companion object { + val ACCOUNT_USER_DATA_KEYS = listOf( + // AccountUtils.Constants.KEY_COOKIES, is disabled + AccountUtils.Constants.KEY_DISPLAY_NAME, + AccountUtils.Constants.KEY_OC_ACCOUNT_VERSION, + AccountUtils.Constants.KEY_OC_BASE_URL, + AccountUtils.Constants.KEY_OC_VERSION, + AccountUtils.Constants.KEY_USER_ID + ) + } + + /** + * This data class holds all relevant account information that is + * otherwise kept in separate collections. + */ + data class AccountData(val account: Account, val userData: Map) + val currentPage: LiveData = MutableLiveData() val pages: List = listOf( EtmMenuEntry( iconRes = R.drawable.ic_settings, titleRes = R.string.etm_preferences, pageClass = EtmPreferencesFragment::class + ), + EtmMenuEntry( + iconRes = R.drawable.ic_user, + titleRes = R.string.etm_accounts, + pageClass = EtmAccountsFragment::class ) ) @@ -46,6 +76,16 @@ class EtmViewModel @Inject constructor( .toMap() } + val accounts: List get() { + val accountType = resources.getString(R.string.account_type) + return platformAccountManager.getAccountsByType(accountType).map { account -> + val userData: Map = ACCOUNT_USER_DATA_KEYS.map { key -> + key to platformAccountManager.getUserData(account, key) + }.toMap() + AccountData(account, userData) + } + } + init { (currentPage as MutableLiveData).apply { value = null diff --git a/src/main/java/com/nextcloud/client/etm/pages/EtmAccountsFragment.kt b/src/main/java/com/nextcloud/client/etm/pages/EtmAccountsFragment.kt new file mode 100644 index 0000000000..756d2bf10a --- /dev/null +++ b/src/main/java/com/nextcloud/client/etm/pages/EtmAccountsFragment.kt @@ -0,0 +1,79 @@ +/* + * Nextcloud Android client application + * + * @author Chris Narkiewicz + * Copyright (C) 2019 Chris Narkiewicz + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero 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 Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package com.nextcloud.client.etm.pages + +import android.content.Intent +import android.os.Bundle +import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import com.nextcloud.client.etm.EtmBaseFragment +import com.owncloud.android.R +import kotlinx.android.synthetic.main.fragment_etm_accounts.* +import kotlinx.android.synthetic.main.fragment_etm_preferences.* + +class EtmAccountsFragment : EtmBaseFragment() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setHasOptionsMenu(true) + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.fragment_etm_accounts, container, false) + } + + override fun onResume() { + super.onResume() + val builder = StringBuilder() + vm.accounts.forEach { + builder.append("Account: ${it.account.name}\n") + it.userData.forEach { + builder.append("\t${it.key}: ${it.value}\n") + } + } + etm_accounts_text.text = builder.toString() + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + super.onCreateOptionsMenu(menu, inflater) + inflater.inflate(R.menu.fragment_etm_accounts, menu) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + R.id.etm_accounts_share -> { + onClickedShare(); true + } + else -> super.onOptionsItemSelected(item) + } + } + + private fun onClickedShare() { + val intent = Intent(Intent.ACTION_SEND) + intent.putExtra(Intent.EXTRA_SUBJECT, "Nextcloud accounts information") + intent.putExtra(Intent.EXTRA_TEXT, etm_accounts_text.text) + intent.type = "text/plain" + startActivity(intent) + } +} diff --git a/src/main/res/layout/fragment_etm_accounts.xml b/src/main/res/layout/fragment_etm_accounts.xml new file mode 100644 index 0000000000..c584edd6f1 --- /dev/null +++ b/src/main/res/layout/fragment_etm_accounts.xml @@ -0,0 +1,33 @@ + + + + + + diff --git a/src/main/res/menu/fragment_etm_accounts.xml b/src/main/res/menu/fragment_etm_accounts.xml new file mode 100644 index 0000000000..6dff0e006b --- /dev/null +++ b/src/main/res/menu/fragment_etm_accounts.xml @@ -0,0 +1,33 @@ + + + + + + + diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index c58f592abf..9670ff481e 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -885,6 +885,7 @@ Disable power save check Disabling power save check might result in uploading files when in low battery state! Engineering Test Mode + Accounts Preferences Loading… diff --git a/src/test/java/com/nextcloud/client/etm/TestEtmViewModel.kt b/src/test/java/com/nextcloud/client/etm/TestEtmViewModel.kt index 292df4bbdd..5423a9ca16 100644 --- a/src/test/java/com/nextcloud/client/etm/TestEtmViewModel.kt +++ b/src/test/java/com/nextcloud/client/etm/TestEtmViewModel.kt @@ -2,7 +2,7 @@ * Nextcloud Android client application * * @author Chris Narkiewicz - * Copyright (C) 2019 Chris Narkiewicz + * Copyright (C) 2020 Chris Narkiewicz * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -19,9 +19,12 @@ */ package com.nextcloud.client.etm +import android.accounts.AccountManager import android.content.SharedPreferences +import android.content.res.Resources import androidx.arch.core.executor.testing.InstantTaskExecutorRule import androidx.lifecycle.Observer +import com.nhaarman.mockitokotlin2.any import com.nhaarman.mockitokotlin2.anyOrNull import com.nhaarman.mockitokotlin2.eq import com.nhaarman.mockitokotlin2.mock @@ -54,13 +57,18 @@ class TestEtmViewModel { @get:Rule val rule = InstantTaskExecutorRule() + protected lateinit var platformAccountManager: AccountManager protected lateinit var sharedPreferences: SharedPreferences protected lateinit var vm: EtmViewModel + protected lateinit var resources: Resources @Before fun setUpBase() { sharedPreferences = mock() - vm = EtmViewModel(sharedPreferences) + platformAccountManager = mock() + resources = mock() + whenever(resources.getString(any())).thenReturn("mock-account-type") + vm = EtmViewModel(sharedPreferences, platformAccountManager, resources) } }