Engineering Test Mode (#4133)

Engineering Test Mode
This commit is contained in:
Tobias Kaminsky 2019-07-29 14:52:19 +02:00 committed by GitHub
commit bc0b9e78f1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 992 additions and 1 deletions

View file

@ -257,6 +257,7 @@ dependencies {
implementation 'androidx.appcompat:appcompat:1.0.2' implementation 'androidx.appcompat:appcompat:1.0.2'
implementation 'androidx.cardview:cardview:1.0.0' implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.exifinterface:exifinterface: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.github.albfernandez:juniversalchardet:2.0.3' // need this version for Android <7
implementation 'com.google.code.findbugs:annotations:2.0.1' implementation 'com.google.code.findbugs:annotations:2.0.1'
implementation 'commons-io:commons-io:2.6' implementation 'commons-io:commons-io:2.6'
@ -306,6 +307,7 @@ dependencies {
testImplementation 'org.powermock:powermock-api-mockito2:2.0.2' testImplementation 'org.powermock:powermock-api-mockito2:2.0.2'
testImplementation 'org.json:json:20180813' testImplementation 'org.json:json:20180813'
testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.1.0" testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.1.0"
testImplementation "androidx.arch.core:core-testing:2.0.1"
// dependencies for instrumented tests // dependencies for instrumented tests
// JUnit4 Rules // JUnit4 Rules

View file

@ -386,6 +386,10 @@
android:name=".ui.activity.SsoGrantPermissionActivity" android:name=".ui.activity.SsoGrantPermissionActivity"
android:exported="true" android:exported="true"
android:theme="@style/Theme.ownCloud.Dialog.NoTitle" /> android:theme="@style/Theme.ownCloud.Dialog.NoTitle" />
<activity
android:name="com.nextcloud.client.etm.EtmActivity"
android:theme="@style/Theme.ownCloud.Toolbar"/>
</application> </application>
</manifest> </manifest>

View file

@ -42,10 +42,12 @@ import dagger.android.support.AndroidSupportInjectionModule;
AppInfoModule.class, AppInfoModule.class,
NetworkModule.class, NetworkModule.class,
DeviceModule.class, DeviceModule.class,
OnboardingModule.class OnboardingModule.class,
ViewModelModule.class
}) })
@Singleton @Singleton
public interface AppComponent { public interface AppComponent {
void inject(MainApp app); void inject(MainApp app);
@Component.Builder @Component.Builder

View file

@ -21,6 +21,7 @@
package com.nextcloud.client.di; package com.nextcloud.client.di;
import com.nextcloud.client.onboarding.FirstRunActivity; import com.nextcloud.client.onboarding.FirstRunActivity;
import com.nextcloud.client.etm.EtmActivity;
import com.nextcloud.client.onboarding.WhatsNewActivity; import com.nextcloud.client.onboarding.WhatsNewActivity;
import com.owncloud.android.authentication.AuthenticatorActivity; import com.owncloud.android.authentication.AuthenticatorActivity;
import com.owncloud.android.authentication.DeepLinkLoginActivity; import com.owncloud.android.authentication.DeepLinkLoginActivity;
@ -121,6 +122,7 @@ abstract class ComponentsModule {
@ContributesAndroidInjector abstract UploadPathActivity uploadPathActivity(); @ContributesAndroidInjector abstract UploadPathActivity uploadPathActivity();
@ContributesAndroidInjector abstract UserInfoActivity userInfoActivity(); @ContributesAndroidInjector abstract UserInfoActivity userInfoActivity();
@ContributesAndroidInjector abstract WhatsNewActivity whatsNewActivity(); @ContributesAndroidInjector abstract WhatsNewActivity whatsNewActivity();
@ContributesAndroidInjector abstract EtmActivity etmActivity();
@ContributesAndroidInjector abstract ExtendedListFragment extendedListFragment(); @ContributesAndroidInjector abstract ExtendedListFragment extendedListFragment();
@ContributesAndroidInjector abstract FileDetailFragment fileDetailFragment(); @ContributesAndroidInjector abstract FileDetailFragment fileDetailFragment();

View file

@ -0,0 +1,64 @@
/*
* 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.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<Class<out ViewModel>, @JvmSuppressWildcards Provider<ViewModel>>
) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
var vmProvider: Provider<ViewModel>? = 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)
}
}
}

View file

@ -0,0 +1,30 @@
/*
* 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.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<out ViewModel>)

View file

@ -0,0 +1,38 @@
/*
* 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.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
}

View file

@ -0,0 +1,91 @@
/*
* 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
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))
}
}
}

View file

@ -0,0 +1,28 @@
/*
* 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
import androidx.fragment.app.Fragment
abstract class EtmBaseFragment : Fragment() {
protected val vm: EtmViewModel get() {
return (activity as EtmActivity).vm
}
}

View file

@ -0,0 +1,68 @@
/*
* 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
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<EtmMenuAdapter.PageViewHolder>() {
private val layoutInflater = LayoutInflater.from(context)
var pages: List<EtmMenuEntry> = 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
}
}

View file

@ -0,0 +1,25 @@
/*
* 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
import androidx.fragment.app.Fragment
import kotlin.reflect.KClass
data class EtmMenuEntry(val iconRes: Int, val titleRes: Int, val pageClass: KClass<out Fragment>)

View file

@ -0,0 +1,53 @@
/*
* 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
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)
}
}

View file

@ -0,0 +1,71 @@
/*
* 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
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<EtmMenuEntry?> = MutableLiveData()
val pages: List<EtmMenuEntry> = listOf(
EtmMenuEntry(
iconRes = R.drawable.ic_settings,
titleRes = R.string.etm_preferences,
pageClass = EtmPreferencesFragment::class
)
)
val preferences: Map<String, String> 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
}
}
}

View file

@ -0,0 +1,73 @@
/*
* 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_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)
}
}

View file

@ -45,6 +45,7 @@ import com.nextcloud.client.account.UserAccountManager;
import com.nextcloud.client.appinfo.AppInfo; import com.nextcloud.client.appinfo.AppInfo;
import com.nextcloud.client.device.PowerManagementService; import com.nextcloud.client.device.PowerManagementService;
import com.nextcloud.client.di.ActivityInjector; import com.nextcloud.client.di.ActivityInjector;
import com.nextcloud.client.di.AppComponent;
import com.nextcloud.client.di.DaggerAppComponent; import com.nextcloud.client.di.DaggerAppComponent;
import com.nextcloud.client.network.ConnectivityService; import com.nextcloud.client.network.ConnectivityService;
import com.nextcloud.client.onboarding.OnboardingService; import com.nextcloud.client.onboarding.OnboardingService;

View file

@ -55,6 +55,7 @@ import android.webkit.URLUtil;
import com.nextcloud.client.account.UserAccountManager; import com.nextcloud.client.account.UserAccountManager;
import com.nextcloud.client.di.Injectable; import com.nextcloud.client.di.Injectable;
import com.nextcloud.client.etm.EtmActivity;
import com.nextcloud.client.preferences.AppPreferences; import com.nextcloud.client.preferences.AppPreferences;
import com.nextcloud.client.preferences.AppPreferencesImpl; import com.nextcloud.client.preferences.AppPreferencesImpl;
import com.owncloud.android.BuildConfig; import com.owncloud.android.BuildConfig;
@ -207,6 +208,15 @@ public class SettingsActivity extends PreferenceActivity
return true; return true;
}); });
} }
/* Engineering Test Mode */
Preference pEtm = findPreference("etm");
if (pEtm != null) {
pEtm.setOnPreferenceClickListener(preference -> {
EtmActivity.launch(this);
return true;
});
}
} else { } else {
preferenceScreen.removePreference(preferenceCategoryDev); preferenceScreen.removePreference(preferenceCategoryDev);
} }

View file

@ -0,0 +1,39 @@
<?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/>.
-->
<androidx.coordinatorlayout.widget.CoordinatorLayout
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"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".etm.EtmActivity">
<include layout="@layout/toolbar_standard"/>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
android:id="@+id/etm_page_container">
</FrameLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -0,0 +1,32 @@
<!--
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.EtmMenuFragment">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/etm_menu_list"
android:layout_width="match_parent"
android:layout_height="match_parent">
</androidx.recyclerview.widget.RecyclerView>
</FrameLayout>

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.EtmPreferencesFragment">
<TextView
android:id="@+id/etm_preferences_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,51 @@
<?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/>.
-->
<!--
This is a generic, single row list item matching Material Design specification.
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
style="@style/MaterialListItemSingleLine">
<androidx.appcompat.widget.AppCompatImageView
style="@style/MaterialListItemPrimaryAction"
android:id="@+id/primary_action"
tools:src="@drawable/ic_alert"/>
<TextView
android:id="@+id/text"
android:layout_gravity="center_vertical"
android:layout_width="0dip"
android:layout_weight="1"
android:layout_height="wrap_content"
android:textSize="16sp"
android:lines="1"
android:ellipsize="end"
android:textColor="?android:attr/textColorPrimary"
tools:text="Single line of text"/>
<androidx.appcompat.widget.AppCompatImageView
style="@style/MaterialListItemSecondaryAction"
android:id="@+id/secondary_action"
tools:src="@drawable/ic_alert"/>
</LinearLayout>

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_preferences_share"
android:title="@string/etm_share"
app:showAsAction="ifRoom"
android:showAsAction="ifRoom"
android:icon="@drawable/ic_share" />
</menu>

View file

@ -872,4 +872,8 @@
<string name="copy_internal_link">Copy internal link</string> <string name="copy_internal_link">Copy internal link</string>
<string name="copy_internal_link_subline">Only works for users with access to this folder</string> <string name="copy_internal_link_subline">Only works for users with access to this folder</string>
<string name="failed_to_download">Failed to pass file to download manager</string> <string name="failed_to_download">Failed to pass file to download manager</string>
<string name="etm_title">Engineering Test Mode</string>
<string name="etm_preferences">Preferences</string>
<string name="etm_share">Share</string>
</resources> </resources>

View file

@ -4,6 +4,7 @@
Copyright (C) 2012 Bartek Przybylski Copyright (C) 2012 Bartek Przybylski
Copyright (C) 2015 ownCloud Inc. Copyright (C) 2015 ownCloud Inc.
Copyright (C) 2019 Chris Narkiewicz <hello@ezaquarii.com>
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License version 2, it under the terms of the GNU General Public License version 2,
@ -284,4 +285,33 @@
<item name="android:textSize">16sp</item> <item name="android:textSize">16sp</item>
<item name="textAllCaps">false</item> <item name="textAllCaps">false</item>
</style> </style>
<style name="MaterialListItemSingleLine">
<item name="android:clickable">true</item>
<item name="android:background">?android:selectableItemBackground</item>
<item name="android:paddingLeft">16dp</item>
<item name="android:paddingRight">16dp</item>
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">48dp</item>
<item name="android:gravity">center_vertical</item>
</style>
<style name="MaterialListItemPrimaryAction">
<item name="tint">?android:attr/textColorSecondary</item>
<item name="android:layout_width">32dp</item>
<item name="android:layout_height">32dp</item>
<item name="android:layout_marginRight">16dp</item>
<item name="android:scaleType">fitCenter</item>
<item name="android:layout_gravity">center_vertical</item>
</style>
<style name="MaterialListItemSecondaryAction">
<item name="tint">?android:attr/textColorSecondary</item>
<item name="android:layout_width">24dp</item>
<item name="android:layout_height">24dp</item>
<item name="android:layout_marginLeft">16dp</item>
<item name="android:scaleType">fitCenter</item>
<item name="android:layout_gravity">center_vertical</item>
</style>
</resources> </resources>

View file

@ -4,6 +4,7 @@
Copyright (C) 2012 Bartek Przybylski Copyright (C) 2012 Bartek Przybylski
Copyright (C) 2012-2013 ownCloud Inc. Copyright (C) 2012-2013 ownCloud Inc.
Copyright (C) 2019 Chris Narkiewicz <hello@ezaquarii.com>
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License version 2, it under the terms of the GNU General Public License version 2,
@ -95,6 +96,10 @@
<Preference android:id="@+id/changelog_link" <Preference android:id="@+id/changelog_link"
android:title="Changelog dev version" android:title="Changelog dev version"
android:key="changelog_link" /> android:key="changelog_link" />
<Preference android:id="@+id/etm"
android:title="@string/etm_title"
android:key="etm" />
</PreferenceCategory> </PreferenceCategory>
</PreferenceScreen> </PreferenceScreen>

View file

@ -0,0 +1,202 @@
/*
* 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
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<EtmMenuEntry?> = 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<EtmMenuEntry?> = 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<EtmMenuEntry?> = 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<String, Any> = 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"])
}
}
}