Merge pull request #5436 from nextcloud/ezaquarii/make-user-model-parcelable

Make user model parcelable
This commit is contained in:
Tobias Kaminsky 2020-02-11 09:26:26 +01:00 committed by GitHub
commit f520f2f9a2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 456 additions and 8 deletions

View file

@ -0,0 +1,53 @@
/*
* Nextcloud Android client application
*
* @author Chris Narkiewicz <hello@ezaquarii.com>
* 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 <https://www.gnu.org/licenses/>.
*/
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>(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)
}
}

View file

@ -0,0 +1,119 @@
/*
* Nextcloud Android client application
*
* @author Chris Narkiewicz <hello@ezaquarii.com>
* 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 <https://www.gnu.org/licenses/>.
*/
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>(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)
}
}

View file

@ -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<AnonymousUser> = object : Parcelable.Creator<AnonymousUser> {
override fun createFromParcel(source: Parcel): AnonymousUser = AnonymousUser(source)
override fun newArray(size: Int): Array<AnonymousUser?> = 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)
}
}

View file

@ -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<RegisteredUser> = object : Parcelable.Creator<RegisteredUser> {
override fun createFromParcel(source: Parcel): RegisteredUser = RegisteredUser(source)
override fun newArray(size: Int): Array<RegisteredUser?> = arrayOfNulls(size)
}
}
private constructor(source: Parcel) : this(
source.readParcelable<Account>(Account::class.java.classLoader) as Account,
source.readParcelable<OwnCloudAccount>(OwnCloudAccount::class.java.classLoader) as OwnCloudAccount,
source.readParcelable<Server>(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)
}
}

View file

@ -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<Parcelable>(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<Server> = object : Parcelable.Creator<Server> {
override fun createFromParcel(source: Parcel): Server = Server(source)
override fun newArray(size: Int): Array<Server?> = arrayOfNulls(size)
}
}
}

View file

@ -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
}

View file

@ -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<String, String?>)
val currentPage: LiveData<EtmMenuEntry?> = MutableLiveData()
val pages: List<EtmMenuEntry> = 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<AccountData> get() {
val accountType = resources.getString(R.string.account_type)
return platformAccountManager.getAccountsByType(accountType).map { account ->
val userData: Map<String, String?> = ACCOUNT_USER_DATA_KEYS.map { key ->
key to platformAccountManager.getUserData(account, key)
}.toMap()
AccountData(account, userData)
}
}
init {
(currentPage as MutableLiveData).apply {
value = null

View file

@ -0,0 +1,79 @@
/*
* Nextcloud Android client application
*
* @author Chris Narkiewicz
* Copyright (C) 2019 Chris Narkiewicz <hello@ezaquarii.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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)
}
}

View file

@ -0,0 +1,33 @@
<!--
Nextcloud Android client application
@author Chris Narkiewicz
Copyright (C) 2019 Chris Narkiewicz <hello@ezaquarii.com>
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 <http://www.gnu.org/licenses/>.
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.nextcloud.client.etm.pages.EtmAccountsFragment">
<TextView
android:id="@+id/etm_accounts_text"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="@dimen/standard_padding"
android:scrollbars="vertical"/>
</FrameLayout>

View file

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Nextcloud Android client application
@author Chris Narkiewicz
Copyright (C) 2019 Chris Narkiewicz <hello@ezaquarii.com>
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 <http://www.gnu.org/licenses/>.
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:ignore="AppCompatResource">
<item
android:id="@+id/etm_accounts_share"
android:title="@string/common_share"
app:showAsAction="ifRoom"
android:showAsAction="ifRoom"
android:icon="@drawable/ic_share" />
</menu>

View file

@ -885,6 +885,7 @@
<string name="autoupload_disable_power_save_check">Disable power save check</string>
<string name="power_save_check_dialog_message">Disabling power save check might result in uploading files when in low battery state!</string>
<string name="etm_title">Engineering Test Mode</string>
<string name="etm_accounts">Accounts</string>
<string name="etm_preferences">Preferences</string>
<string name="logs_status_loading">Loading…</string>

View file

@ -2,7 +2,7 @@
* Nextcloud Android client application
*
* @author Chris Narkiewicz
* Copyright (C) 2019 Chris Narkiewicz <hello@ezaquarii.com>
* Copyright (C) 2020 Chris Narkiewicz <hello@ezaquarii.com>
*
* 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)
}
}