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"]) + } + } +}