diff --git a/build.gradle b/build.gradle
index aaaf272a7314..847677217d34 100644
--- a/build.gradle
+++ b/build.gradle
@@ -256,6 +256,7 @@ dependencies {
implementation 'androidx.appcompat:appcompat:1.0.2'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.exifinterface:exifinterface:1.0.0'
+ implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.0.0"
implementation 'com.github.albfernandez:juniversalchardet:2.0.3' // need this version for Android <7
implementation 'com.google.code.findbugs:annotations:2.0.1'
implementation 'commons-io:commons-io:2.6'
@@ -305,6 +306,7 @@ dependencies {
testImplementation 'org.powermock:powermock-api-mockito2:2.0.2'
testImplementation 'org.json:json:20180813'
testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.1.0"
+ testImplementation "androidx.arch.core:core-testing:2.0.1"
// dependencies for instrumented tests
// JUnit4 Rules
diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml
index 0c7e21a9d933..2346de7808b8 100644
--- a/src/main/AndroidManifest.xml
+++ b/src/main/AndroidManifest.xml
@@ -386,6 +386,10 @@
android:name=".ui.activity.SsoGrantPermissionActivity"
android:exported="true"
android:theme="@style/Theme.ownCloud.Dialog.NoTitle" />
+
+
diff --git a/src/main/java/com/nextcloud/client/di/AppComponent.java b/src/main/java/com/nextcloud/client/di/AppComponent.java
index 818b97417020..1d28612c9786 100644
--- a/src/main/java/com/nextcloud/client/di/AppComponent.java
+++ b/src/main/java/com/nextcloud/client/di/AppComponent.java
@@ -42,10 +42,12 @@
AppInfoModule.class,
NetworkModule.class,
DeviceModule.class,
- OnboardingModule.class
+ OnboardingModule.class,
+ ViewModelModule.class
})
@Singleton
public interface AppComponent {
+
void inject(MainApp app);
@Component.Builder
diff --git a/src/main/java/com/nextcloud/client/di/ComponentsModule.java b/src/main/java/com/nextcloud/client/di/ComponentsModule.java
index da0f0f820bae..5eee7f04e2ae 100644
--- a/src/main/java/com/nextcloud/client/di/ComponentsModule.java
+++ b/src/main/java/com/nextcloud/client/di/ComponentsModule.java
@@ -21,6 +21,7 @@
package com.nextcloud.client.di;
import com.nextcloud.client.onboarding.FirstRunActivity;
+import com.nextcloud.client.etm.EtmActivity;
import com.nextcloud.client.onboarding.WhatsNewActivity;
import com.owncloud.android.authentication.AuthenticatorActivity;
import com.owncloud.android.authentication.DeepLinkLoginActivity;
@@ -121,6 +122,7 @@ abstract class ComponentsModule {
@ContributesAndroidInjector abstract UploadPathActivity uploadPathActivity();
@ContributesAndroidInjector abstract UserInfoActivity userInfoActivity();
@ContributesAndroidInjector abstract WhatsNewActivity whatsNewActivity();
+ @ContributesAndroidInjector abstract EtmActivity etmActivity();
@ContributesAndroidInjector abstract ExtendedListFragment extendedListFragment();
@ContributesAndroidInjector abstract FileDetailFragment fileDetailFragment();
diff --git a/src/main/java/com/nextcloud/client/di/ViewModelFactory.kt b/src/main/java/com/nextcloud/client/di/ViewModelFactory.kt
new file mode 100644
index 000000000000..cfbb9c18127a
--- /dev/null
+++ b/src/main/java/com/nextcloud/client/di/ViewModelFactory.kt
@@ -0,0 +1,64 @@
+/*
+ * 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.di
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import javax.inject.Inject
+import javax.inject.Provider
+
+/**
+ * This factory provide [ViewModel] instances initialized by Dagger 2 dependency injection system.
+ *
+ * Each [javax.inject.Provider] instance accesses Dagger machinery, which provide
+ * fully-initialized [ViewModel] instance.
+ *
+ * @see ViewModelModule
+ * @see ViewModelKey
+ */
+class ViewModelFactory @Inject constructor(
+ private val viewModelProviders: Map, @JvmSuppressWildcards Provider>
+) : ViewModelProvider.Factory {
+
+ override fun create(modelClass: Class): T {
+ var vmProvider: Provider? = viewModelProviders.get(modelClass)
+
+ if (vmProvider == null) {
+ for (entry in viewModelProviders.entries) {
+ if (modelClass.isAssignableFrom(entry.key)) {
+ vmProvider = entry.value
+ break
+ }
+ }
+ }
+
+ if (vmProvider == null) {
+ throw IllegalArgumentException("${modelClass.simpleName} view model class is not supported")
+ }
+
+ @Suppress("TooGenericExceptionCaught", "TooGenericExceptionThrown", "UNCHECKED_CAST")
+ try {
+ val vm = vmProvider.get() as T
+ return vm
+ } catch (e: Exception) {
+ throw RuntimeException(e)
+ }
+ }
+}
diff --git a/src/main/java/com/nextcloud/client/di/ViewModelKey.kt b/src/main/java/com/nextcloud/client/di/ViewModelKey.kt
new file mode 100644
index 000000000000..3888c68df58f
--- /dev/null
+++ b/src/main/java/com/nextcloud/client/di/ViewModelKey.kt
@@ -0,0 +1,30 @@
+/*
+ * 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.di
+
+import androidx.lifecycle.ViewModel
+import dagger.MapKey
+import kotlin.reflect.KClass
+
+@MustBeDocumented
+@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER)
+@Retention(AnnotationRetention.RUNTIME)
+@MapKey
+annotation class ViewModelKey(val value: KClass)
diff --git a/src/main/java/com/nextcloud/client/di/ViewModelModule.kt b/src/main/java/com/nextcloud/client/di/ViewModelModule.kt
new file mode 100644
index 000000000000..05108156538e
--- /dev/null
+++ b/src/main/java/com/nextcloud/client/di/ViewModelModule.kt
@@ -0,0 +1,38 @@
+/*
+ * 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.di
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import com.nextcloud.client.etm.EtmViewModel
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.IntoMap
+
+@Module
+abstract class ViewModelModule {
+ @Binds
+ @IntoMap
+ @ViewModelKey(EtmViewModel::class)
+ abstract fun etmViewModel(vm: EtmViewModel): ViewModel
+
+ @Binds
+ abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory
+}
diff --git a/src/main/java/com/nextcloud/client/etm/EtmActivity.kt b/src/main/java/com/nextcloud/client/etm/EtmActivity.kt
new file mode 100644
index 000000000000..020bc72ad4cf
--- /dev/null
+++ b/src/main/java/com/nextcloud/client/etm/EtmActivity.kt
@@ -0,0 +1,91 @@
+/*
+ * 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
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import android.view.MenuItem
+import androidx.lifecycle.Observer
+import androidx.lifecycle.ViewModelProvider
+import com.nextcloud.client.di.Injectable
+import com.nextcloud.client.di.ViewModelFactory
+import com.owncloud.android.R
+import com.owncloud.android.ui.activity.ToolbarActivity
+import javax.inject.Inject
+
+class EtmActivity : ToolbarActivity(), Injectable {
+
+ companion object {
+ @JvmStatic
+ fun launch(context: Context) {
+ val etmIntent = Intent(context, EtmActivity::class.java)
+ context.startActivity(etmIntent)
+ }
+ }
+
+ @Inject
+ lateinit var viewModelFactory: ViewModelFactory
+ internal lateinit var vm: EtmViewModel
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_etm)
+ setupToolbar()
+ updateActionBarTitleAndHomeButtonByString(getString(R.string.etm_title))
+ vm = ViewModelProvider(this, viewModelFactory).get(EtmViewModel::class.java)
+ vm.currentPage.observe(this, Observer {
+ onPageChanged(it)
+ })
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem?): Boolean {
+ return when (item?.itemId) {
+ android.R.id.home -> {
+ if (!vm.onBackPressed()) {
+ finish()
+ }
+ true
+ }
+ else -> super.onOptionsItemSelected(item)
+ }
+ }
+
+ override fun onBackPressed() {
+ if (!vm.onBackPressed()) {
+ super.onBackPressed()
+ }
+ }
+
+ private fun onPageChanged(page: EtmMenuEntry?) {
+ if (page != null) {
+ val fragment = page.pageClass.java.getConstructor().newInstance()
+ supportFragmentManager.beginTransaction()
+ .replace(R.id.etm_page_container, fragment)
+ .commit()
+ updateActionBarTitleAndHomeButtonByString("ETM - ${getString(page.titleRes)}")
+ } else {
+ supportFragmentManager.beginTransaction()
+ .replace(R.id.etm_page_container, EtmMenuFragment())
+ .commitNow()
+ updateActionBarTitleAndHomeButtonByString(getString(R.string.etm_title))
+ }
+ }
+}
diff --git a/src/main/java/com/nextcloud/client/etm/EtmBaseFragment.kt b/src/main/java/com/nextcloud/client/etm/EtmBaseFragment.kt
new file mode 100644
index 000000000000..8bf8a98fd553
--- /dev/null
+++ b/src/main/java/com/nextcloud/client/etm/EtmBaseFragment.kt
@@ -0,0 +1,28 @@
+/*
+ * 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
+
+import androidx.fragment.app.Fragment
+
+abstract class EtmBaseFragment : Fragment() {
+ protected val vm: EtmViewModel get() {
+ return (activity as EtmActivity).vm
+ }
+}
diff --git a/src/main/java/com/nextcloud/client/etm/EtmMenuAdapter.kt b/src/main/java/com/nextcloud/client/etm/EtmMenuAdapter.kt
new file mode 100644
index 000000000000..a91bfb009239
--- /dev/null
+++ b/src/main/java/com/nextcloud/client/etm/EtmMenuAdapter.kt
@@ -0,0 +1,68 @@
+/*
+ * 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
+
+import android.content.Context
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.recyclerview.widget.RecyclerView
+import com.owncloud.android.R
+
+class EtmMenuAdapter(
+ context: Context,
+ val onItemClicked: (Int) -> Unit
+) : RecyclerView.Adapter() {
+
+ private val layoutInflater = LayoutInflater.from(context)
+ var pages: List = listOf()
+ set(value) {
+ field = value
+ notifyDataSetChanged()
+ }
+
+ class PageViewHolder(view: View, onClick: (Int) -> Unit) : RecyclerView.ViewHolder(view) {
+ val primaryAction: ImageView = view.findViewById(R.id.primary_action)
+ val text: TextView = view.findViewById(R.id.text)
+ val secondaryAction: ImageView = view.findViewById(R.id.secondary_action)
+
+ init {
+ itemView.setOnClickListener { onClick(adapterPosition) }
+ }
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PageViewHolder {
+ val view = layoutInflater.inflate(R.layout.material_list_item_single_line, parent, false)
+ return PageViewHolder(view, onItemClicked)
+ }
+
+ override fun onBindViewHolder(holder: PageViewHolder, position: Int) {
+ val page = pages[position]
+ holder.primaryAction.setImageResource(page.iconRes)
+ holder.text.setText(page.titleRes)
+ holder.secondaryAction.setImageResource(0)
+ }
+
+ override fun getItemCount(): Int {
+ return pages.size
+ }
+}
diff --git a/src/main/java/com/nextcloud/client/etm/EtmMenuEntry.kt b/src/main/java/com/nextcloud/client/etm/EtmMenuEntry.kt
new file mode 100644
index 000000000000..ef1b1a0bb97d
--- /dev/null
+++ b/src/main/java/com/nextcloud/client/etm/EtmMenuEntry.kt
@@ -0,0 +1,25 @@
+/*
+ * 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
+
+import androidx.fragment.app.Fragment
+import kotlin.reflect.KClass
+
+data class EtmMenuEntry(val iconRes: Int, val titleRes: Int, val pageClass: KClass)
diff --git a/src/main/java/com/nextcloud/client/etm/EtmMenuFragment.kt b/src/main/java/com/nextcloud/client/etm/EtmMenuFragment.kt
new file mode 100644
index 000000000000..bb1db79536b3
--- /dev/null
+++ b/src/main/java/com/nextcloud/client/etm/EtmMenuFragment.kt
@@ -0,0 +1,53 @@
+/*
+ * 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
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.owncloud.android.R
+
+class EtmMenuFragment : EtmBaseFragment() {
+
+ private lateinit var adapter: EtmMenuAdapter
+ private lateinit var list: RecyclerView
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
+ adapter = EtmMenuAdapter(context!!, this::onClickedItem)
+ adapter.pages = vm.pages
+ val view = inflater.inflate(R.layout.fragment_etm_menu, container, false)
+ list = view.findViewById(R.id.etm_menu_list)
+ list.layoutManager = LinearLayoutManager(context!!)
+ list.adapter = adapter
+ return view
+ }
+
+ override fun onResume() {
+ super.onResume()
+ activity?.setTitle(R.string.etm_title)
+ }
+
+ private fun onClickedItem(position: Int) {
+ vm.onPageSelected(position)
+ }
+}
diff --git a/src/main/java/com/nextcloud/client/etm/EtmViewModel.kt b/src/main/java/com/nextcloud/client/etm/EtmViewModel.kt
new file mode 100644
index 000000000000..e1d96d44c76f
--- /dev/null
+++ b/src/main/java/com/nextcloud/client/etm/EtmViewModel.kt
@@ -0,0 +1,71 @@
+/*
+ * 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
+
+import android.content.SharedPreferences
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import com.nextcloud.client.etm.pages.EtmPreferencesFragment
+import com.owncloud.android.R
+import javax.inject.Inject
+
+class EtmViewModel @Inject constructor(
+ private val defaultPreferences: SharedPreferences
+) : ViewModel() {
+ val currentPage: LiveData = MutableLiveData()
+ val pages: List = listOf(
+ EtmMenuEntry(
+ iconRes = R.drawable.ic_settings,
+ titleRes = R.string.etm_preferences,
+ pageClass = EtmPreferencesFragment::class
+ )
+ )
+
+ val preferences: Map get() {
+ return defaultPreferences.all
+ .map { it.key to "${it.value}" }
+ .sortedBy { it.first }
+ .toMap()
+ }
+
+ init {
+ (currentPage as MutableLiveData).apply {
+ value = null
+ }
+ }
+
+ fun onPageSelected(index: Int) {
+ if (index < pages.size) {
+ currentPage as MutableLiveData
+ currentPage.value = pages[index]
+ }
+ }
+
+ fun onBackPressed(): Boolean {
+ (currentPage as MutableLiveData)
+ return if (currentPage.value != null) {
+ currentPage.value = null
+ true
+ } else {
+ false
+ }
+ }
+}
diff --git a/src/main/java/com/nextcloud/client/etm/pages/EtmPreferencesFragment.kt b/src/main/java/com/nextcloud/client/etm/pages/EtmPreferencesFragment.kt
new file mode 100644
index 000000000000..9c55fa77d72e
--- /dev/null
+++ b/src/main/java/com/nextcloud/client/etm/pages/EtmPreferencesFragment.kt
@@ -0,0 +1,73 @@
+/*
+ * 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_preferences.*
+
+class EtmPreferencesFragment : 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_preferences, container, false)
+ }
+
+ override fun onResume() {
+ super.onResume()
+ val builder = StringBuilder()
+ vm.preferences.forEach { builder.append("${it.key}: ${it.value}\n") }
+ etm_preferences_text.text = builder
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu?, inflater: MenuInflater?) {
+ super.onCreateOptionsMenu(menu, inflater)
+ inflater?.inflate(R.menu.etm_preferences, menu)
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem?): Boolean {
+ return when (item?.itemId) {
+ R.id.etm_preferences_share -> {
+ onClickedShare(); true
+ }
+ else -> super.onOptionsItemSelected(item)
+ }
+ }
+
+ private fun onClickedShare() {
+ val intent = Intent(Intent.ACTION_SEND)
+ intent.putExtra(Intent.EXTRA_SUBJECT, "Nextcloud preferences")
+ intent.putExtra(Intent.EXTRA_TEXT, etm_preferences_text.text)
+ intent.type = "text/plain"
+ startActivity(intent)
+ }
+}
diff --git a/src/main/java/com/owncloud/android/MainApp.java b/src/main/java/com/owncloud/android/MainApp.java
index 2e7f118d277e..d9ca3c8bf89e 100644
--- a/src/main/java/com/owncloud/android/MainApp.java
+++ b/src/main/java/com/owncloud/android/MainApp.java
@@ -48,6 +48,7 @@
import com.nextcloud.client.appinfo.AppInfo;
import com.nextcloud.client.device.PowerManagementService;
import com.nextcloud.client.di.ActivityInjector;
+import com.nextcloud.client.di.AppComponent;
import com.nextcloud.client.di.DaggerAppComponent;
import com.nextcloud.client.network.ConnectivityService;
import com.nextcloud.client.onboarding.OnboardingService;
diff --git a/src/main/java/com/owncloud/android/ui/activity/SettingsActivity.java b/src/main/java/com/owncloud/android/ui/activity/SettingsActivity.java
index 6af4ad2c5855..7f4effa72797 100644
--- a/src/main/java/com/owncloud/android/ui/activity/SettingsActivity.java
+++ b/src/main/java/com/owncloud/android/ui/activity/SettingsActivity.java
@@ -55,6 +55,7 @@
import com.nextcloud.client.account.UserAccountManager;
import com.nextcloud.client.di.Injectable;
+import com.nextcloud.client.etm.EtmActivity;
import com.nextcloud.client.preferences.AppPreferences;
import com.nextcloud.client.preferences.AppPreferencesImpl;
import com.owncloud.android.BuildConfig;
@@ -207,6 +208,15 @@ private void setupDevCategory(int accentColor, PreferenceScreen preferenceScreen
return true;
});
}
+
+ /* Engineering Test Mode */
+ Preference pEtm = findPreference("etm");
+ if (pEtm != null) {
+ pEtm.setOnPreferenceClickListener(preference -> {
+ EtmActivity.launch(this);
+ return true;
+ });
+ }
} else {
preferenceScreen.removePreference(preferenceCategoryDev);
}
diff --git a/src/main/res/layout/activity_etm.xml b/src/main/res/layout/activity_etm.xml
new file mode 100644
index 000000000000..f0c5a33b3d92
--- /dev/null
+++ b/src/main/res/layout/activity_etm.xml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/res/layout/fragment_etm_menu.xml b/src/main/res/layout/fragment_etm_menu.xml
new file mode 100644
index 000000000000..aa3806692b35
--- /dev/null
+++ b/src/main/res/layout/fragment_etm_menu.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
diff --git a/src/main/res/layout/fragment_etm_preferences.xml b/src/main/res/layout/fragment_etm_preferences.xml
new file mode 100644
index 000000000000..dd88b4c950eb
--- /dev/null
+++ b/src/main/res/layout/fragment_etm_preferences.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
diff --git a/src/main/res/layout/material_list_item_single_line.xml b/src/main/res/layout/material_list_item_single_line.xml
new file mode 100644
index 000000000000..db58963c7e17
--- /dev/null
+++ b/src/main/res/layout/material_list_item_single_line.xml
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/res/menu/etm_preferences.xml b/src/main/res/menu/etm_preferences.xml
new file mode 100644
index 000000000000..bf8b9e630f92
--- /dev/null
+++ b/src/main/res/menu/etm_preferences.xml
@@ -0,0 +1,33 @@
+
+
+
diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml
index 285b07223318..c9fc57d76a9d 100644
--- a/src/main/res/values/strings.xml
+++ b/src/main/res/values/strings.xml
@@ -872,4 +872,8 @@
Copy internal link
Only works for users with access to this folder
Failed to pass file to download manager
+
+ Engineering Test Mode
+ Preferences
+ Share
diff --git a/src/main/res/values/styles.xml b/src/main/res/values/styles.xml
index f824c05984f5..c88ccb96dfd2 100644
--- a/src/main/res/values/styles.xml
+++ b/src/main/res/values/styles.xml
@@ -4,6 +4,7 @@
Copyright (C) 2012 Bartek Przybylski
Copyright (C) 2015 ownCloud Inc.
+ Copyright (C) 2019 Chris Narkiewicz
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License version 2,
@@ -284,4 +285,33 @@
- 16sp
- false
+
+
+
+
+
+
+
diff --git a/src/main/res/xml/preferences.xml b/src/main/res/xml/preferences.xml
index 4354a36440c0..9c9bec635834 100644
--- a/src/main/res/xml/preferences.xml
+++ b/src/main/res/xml/preferences.xml
@@ -4,6 +4,7 @@
Copyright (C) 2012 Bartek Przybylski
Copyright (C) 2012-2013 ownCloud Inc.
+ Copyright (C) 2019 Chris Narkiewicz
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License version 2,
@@ -95,6 +96,10 @@
+
+
diff --git a/src/test/java/com/nextcloud/client/etm/TestEtmViewModel.kt b/src/test/java/com/nextcloud/client/etm/TestEtmViewModel.kt
new file mode 100644
index 000000000000..292df4bbdd77
--- /dev/null
+++ b/src/test/java/com/nextcloud/client/etm/TestEtmViewModel.kt
@@ -0,0 +1,202 @@
+/*
+ * 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
+
+import android.content.SharedPreferences
+import androidx.arch.core.executor.testing.InstantTaskExecutorRule
+import androidx.lifecycle.Observer
+import com.nhaarman.mockitokotlin2.anyOrNull
+import com.nhaarman.mockitokotlin2.eq
+import com.nhaarman.mockitokotlin2.mock
+import com.nhaarman.mockitokotlin2.never
+import com.nhaarman.mockitokotlin2.reset
+import com.nhaarman.mockitokotlin2.same
+import com.nhaarman.mockitokotlin2.times
+import com.nhaarman.mockitokotlin2.verify
+import com.nhaarman.mockitokotlin2.whenever
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertNotNull
+import org.junit.Assert.assertNull
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Suite
+
+@RunWith(Suite::class)
+@Suite.SuiteClasses(
+ TestEtmViewModel.MainPage::class,
+ TestEtmViewModel.PreferencesPage::class
+)
+class TestEtmViewModel {
+
+ internal abstract class Base {
+
+ @get:Rule
+ val rule = InstantTaskExecutorRule()
+
+ protected lateinit var sharedPreferences: SharedPreferences
+ protected lateinit var vm: EtmViewModel
+
+ @Before
+ fun setUpBase() {
+ sharedPreferences = mock()
+ vm = EtmViewModel(sharedPreferences)
+ }
+ }
+
+ internal class MainPage : Base() {
+
+ @Test
+ fun `current page is not set`() {
+ // GIVEN
+ // main page is displayed
+ // THEN
+ // current page is null
+ assertNull(vm.currentPage.value)
+ }
+
+ @Test
+ fun `back key is not handled`() {
+ // GIVEN
+ // main page is displayed
+ // WHEN
+ // back key is pressed
+ val handled = vm.onBackPressed()
+
+ // THEN
+ // is not handled
+ assertFalse(handled)
+ }
+
+ @Test
+ fun `page is selected`() {
+ val observer: Observer = mock()
+ val selectedPageIndex = 0
+ val expectedPage = vm.pages[selectedPageIndex]
+
+ // GIVEN
+ // main page is displayed
+ // current page observer is registered
+ vm.currentPage.observeForever(observer)
+ reset(observer)
+
+ // WHEN
+ // page is selected
+ vm.onPageSelected(selectedPageIndex)
+
+ // THEN
+ // current page is set
+ // page observer is called once with selected entry
+ assertNotNull(vm.currentPage.value)
+ verify(observer, times(1)).onChanged(same(expectedPage))
+ }
+
+ @Test
+ fun `out of range index is ignored`() {
+ val maxIndex = vm.pages.size
+ // GIVEN
+ // observer is registered
+ val observer: Observer = mock()
+ vm.currentPage.observeForever(observer)
+ reset(observer)
+
+ // WHEN
+ // out of range page index is selected
+ vm.onPageSelected(maxIndex + 1)
+
+ // THEN
+ // nothing happens
+ verify(observer, never()).onChanged(anyOrNull())
+ assertNull(vm.currentPage.value)
+ }
+ }
+
+ internal class PreferencesPage : Base() {
+
+ @Before
+ fun setUp() {
+ vm.onPageSelected(0)
+ }
+
+ @Test
+ fun `back goes back to main page`() {
+ val observer: Observer = mock()
+
+ // GIVEN
+ // a page is selected
+ // page observer is registered
+ assertNotNull(vm.currentPage.value)
+ vm.currentPage.observeForever(observer)
+
+ // WHEN
+ // back is pressed
+ val handled = vm.onBackPressed()
+
+ // THEN
+ // back press is handled
+ // observer is called with null page
+ assertTrue(handled)
+ verify(observer).onChanged(eq(null))
+ }
+
+ @Test
+ fun `back is handled only once`() {
+ // GIVEN
+ // a page is selected
+ assertNotNull(vm.currentPage.value)
+
+ // WHEN
+ // back is pressed twice
+ val first = vm.onBackPressed()
+ val second = vm.onBackPressed()
+
+ // THEN
+ // back is handled only once
+ assertTrue(first)
+ assertFalse(second)
+ }
+
+ @Test
+ fun `preferences are loaded from shared preferences`() {
+ // GIVEN
+ // shared preferences contain values of different types
+ val preferenceValues: Map = mapOf(
+ "key1" to 1,
+ "key2" to "value2",
+ "key3" to false
+ )
+ whenever(sharedPreferences.all).thenReturn(preferenceValues)
+
+ // WHEN
+ // vm preferences are read
+ val prefs = vm.preferences
+
+ // THEN
+ // all preferences are converted to strings
+ assertEquals(preferenceValues.size, prefs.size)
+ assertEquals("1", prefs["key1"])
+ assertEquals("value2", prefs["key2"])
+ assertEquals("false", prefs["key3"])
+ }
+ }
+}