Procházet zdrojové kódy

Make user model parcelable

Signed-off-by: Chris Narkiewicz <hello@ezaquarii.com>
Chris Narkiewicz před 5 roky
rodič
revize
4ce54bc21e

+ 53 - 0
src/androidTest/java/com/nextcloud/client/account/AnonymousUserTest.kt

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

+ 119 - 0
src/androidTest/java/com/nextcloud/client/account/RegisteredUserTest.kt

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

+ 24 - 2
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<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)
+    }
 }

+ 30 - 1
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<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)
+    }
 }

+ 24 - 1
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<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)
+        }
+    }
+}

+ 9 - 1
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
 }

+ 41 - 1
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<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

+ 79 - 0
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 <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)
+    }
+}

+ 33 - 0
src/main/res/layout/fragment_etm_accounts.xml

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

+ 33 - 0
src/main/res/menu/fragment_etm_accounts.xml

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

+ 1 - 0
src/main/res/values/strings.xml

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

+ 10 - 2
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 <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)
         }
     }