Compare commits

...

13 Commits

Author SHA1 Message Date
Adam Brown
28c9b0cf4b including screenshot test output to showcase comparisons -
these should not be merged directly into the repo but instead pulled in via git lfs
2022-09-01 16:40:48 +01:00
Adam Brown
bc6a0a842f add screenshot tests around the combined server selection 2022-09-01 16:40:43 +01:00
Adam Brown
51888e176c switching button to material button as the colorPrimary isn't working with the default button 2022-09-01 16:40:37 +01:00
Adam Brown
d403d16731 extracting inline fragment view logic to a controller 2022-09-01 16:40:15 +01:00
Adam Brown
92c615af31 adding room settings example 2022-09-01 16:33:53 +01:00
Adam Brown
a8190cf9c1 adding paparazzi screenshot testing lib setup 2022-09-01 16:33:23 +01:00
Adam Brown
c773eda3d6 enabling build caching by default to improve build times 2022-09-01 16:26:12 +01:00
Adam Brown
0e633bad46 split commit to make fdroid changes to avoid files being seen as new 2022-09-01 16:26:06 +01:00
Adam Brown
d689778470 lifting fdroid and gplay variants to the application module 2022-09-01 16:25:57 +01:00
Adam Brown
59cb6a03c8 split commit to make debug changes to avoid files being seen as new! 2022-09-01 16:24:41 +01:00
Adam Brown
4e9618fd0d lifting debug build type to the application module 2022-09-01 16:24:33 +01:00
Adam Brown
b1035a808e lifting the release build type to the application module 2022-09-01 16:24:16 +01:00
Adam Brown
0193695aa3 lifting nightly to the application module 2022-09-01 16:24:07 +01:00
95 changed files with 370 additions and 148 deletions

View File

@ -33,6 +33,7 @@ buildscript {
classpath "org.jetbrains.dokka:dokka-gradle-plugin:1.7.10"
classpath "org.jetbrains.kotlinx:kotlinx-knit:0.4.0"
classpath 'com.jakewharton:butterknife-gradle-plugin:10.2.3'
classpath 'app.cash.paparazzi:paparazzi-gradle-plugin:1.0.0'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}

View File

@ -42,6 +42,7 @@ ext.groups = [
regex: [
],
group: [
'app.cash.paparazzi',
'ch.qos.logback',
'com.adevinta.android',
'com.airbnb.android',
@ -145,11 +146,14 @@ ext.groups = [
'it.unimi.dsi',
'jakarta.activation',
'jakarta.xml.bind',
'javax.activation',
'javax.annotation',
'javax.inject',
'javax.xml.bind',
'jline',
'jp.wasabeef',
'junit',
'kxml2',
'me.saket',
'net.bytebuddy',
'net.java',
@ -178,11 +182,13 @@ ext.groups = [
'org.hamcrest',
'org.jacoco',
'org.java-websocket',
'org.jcodec',
'org.jetbrains',
'org.jetbrains.dokka',
'org.jetbrains.intellij.deps',
'org.jetbrains.kotlin',
'org.jetbrains.kotlinx',
'org.jetbrains.trove4j',
'org.json',
'org.jsoup',
'org.junit',

View File

@ -12,10 +12,12 @@ org.gradle.jvmargs=-Xmx4g -Xms512M -XX:MaxPermSize=2048m -XX:MaxMetaspaceSize=1g
org.gradle.configureondemand=true
org.gradle.parallel=true
org.gradle.vfs.watch=true
org.gradle.caching=true
# Android Settings
android.enableJetifier=true
android.useAndroidX=true
android.jetifier.ignorelist=android-base-common,common
#Project Settings
# Change debugPrivateData to true for debugging

View File

@ -341,6 +341,13 @@ android {
]
}
sourceSets {
// Add sourceSets for `release` version when building `nightly`
nightly {
java.srcDirs += "src/release/java"
}
}
buildFeatures {
viewBinding true
}
@ -349,11 +356,39 @@ android {
dependencies {
implementation project(':vector')
implementation project(':vector-config')
debugImplementation project(':library:ui-styles')
implementation libs.dagger.hilt
implementation 'androidx.multidex:multidex:2.0.1'
implementation "androidx.sharetarget:sharetarget:1.1.0"
// Flipper, debug builds only
debugImplementation(libs.flipper.flipper) {
exclude group: 'com.facebook.fbjni', module: 'fbjni'
}
debugImplementation(libs.flipper.flipperNetworkPlugin) {
exclude group: 'com.facebook.fbjni', module: 'fbjni'
}
debugImplementation 'com.facebook.soloader:soloader:0.10.4'
debugImplementation "com.kgurgul.flipper:flipper-realm-android:2.2.0"
gplayImplementation "com.google.android.gms:play-services-location:16.0.0"
// UnifiedPush gplay flavor only
gplayImplementation('com.github.UnifiedPush:android-embedded_fcm_distributor:2.1.2') {
exclude group: 'com.google.firebase', module: 'firebase-core'
exclude group: 'com.google.firebase', module: 'firebase-analytics'
exclude group: 'com.google.firebase', module: 'firebase-measurement-connector'
}
// Nightly
// API-only library
gplayImplementation libs.google.appdistributionApi
// Full SDK implementation
gplayImplementation libs.google.appdistribution
// OSS License, gplay flavor only
gplayImplementation 'com.google.android.gms:play-services-oss-licenses:17.0.0'
kapt libs.dagger.hiltCompiler
kapt libs.airbnb.epoxyProcessor
androidTestImplementation libs.androidx.testCore
androidTestImplementation libs.androidx.testRunner
@ -378,5 +413,6 @@ dependencies {
androidTestImplementation libs.androidx.fragmentTesting
androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:1.7.10"
debugImplementation libs.androidx.fragmentTesting
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.9.1'
}

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity android:name="im.vector.app.features.debug.TestLinkifyActivity" />
<activity android:name="im.vector.app.features.debug.DebugPermissionActivity" />
<activity android:name="im.vector.app.features.debug.analytics.DebugAnalyticsActivity" />
<activity android:name="im.vector.app.features.debug.settings.DebugPrivateSettingsActivity" />
<activity android:name="im.vector.app.features.debug.sas.DebugSasEmojiActivity" />
<activity android:name="im.vector.app.features.debug.features.DebugFeaturesSettingsActivity" />
<activity android:name="im.vector.app.features.debug.DebugMenuActivity" />
<activity android:name="im.vector.app.features.debug.leak.DebugMemoryLeaksActivity" />
<activity
android:name="com.facebook.flipper.android.diagnostics.FlipperDiagnosticActivity"
android:exported="true" />
<!-- Used for UI tests to display the BiometricPrompt. It's normal that it appears as an error. -->
<activity android:exported="false" android:name=".features.pin.lockscreen.tests.LockScreenTestActivity" />
</application>
</manifest>

View File

@ -34,13 +34,13 @@ import im.vector.app.core.utils.PERMISSIONS_FOR_TAKING_PHOTO
import im.vector.app.core.utils.checkPermissions
import im.vector.app.core.utils.registerForPermissionsResult
import im.vector.app.core.utils.toast
import im.vector.app.databinding.ActivityDebugMenuBinding
import im.vector.app.features.debug.analytics.DebugAnalyticsActivity
import im.vector.app.features.debug.features.DebugFeaturesSettingsActivity
import im.vector.app.features.debug.leak.DebugMemoryLeaksActivity
import im.vector.app.features.debug.sas.DebugSasEmojiActivity
import im.vector.app.features.debug.settings.DebugPrivateSettingsActivity
import im.vector.app.features.qrcode.QrCodeScannerActivity
import im.vector.application.databinding.ActivityDebugMenuBinding
import im.vector.lib.ui.styles.debug.DebugMaterialThemeDarkDefaultActivity
import im.vector.lib.ui.styles.debug.DebugMaterialThemeDarkTestActivity
import im.vector.lib.ui.styles.debug.DebugMaterialThemeDarkVectorActivity

View File

@ -23,13 +23,13 @@ import android.widget.Toast
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.R
import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.core.utils.checkPermissions
import im.vector.app.core.utils.onPermissionDeniedDialog
import im.vector.app.core.utils.onPermissionDeniedSnackbar
import im.vector.app.core.utils.registerForPermissionsResult
import im.vector.app.databinding.ActivityDebugPermissionBinding
import im.vector.application.R
import im.vector.application.databinding.ActivityDebugPermissionBinding
import timber.log.Timber
@AndroidEntryPoint

View File

@ -20,9 +20,9 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import im.vector.app.R
import im.vector.app.databinding.ActivityTestLinkifyBinding
import im.vector.app.databinding.ItemTestLinkifyBinding
import im.vector.application.R
import im.vector.application.databinding.ActivityTestLinkifyBinding
import im.vector.application.databinding.ItemTestLinkifyBinding
class TestLinkifyActivity : AppCompatActivity() {

View File

@ -25,7 +25,7 @@ import com.airbnb.mvrx.withState
import im.vector.app.core.epoxy.onClick
import im.vector.app.core.extensions.toOnOff
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.databinding.FragmentDebugAnalyticsBinding
import im.vector.application.databinding.FragmentDebugAnalyticsBinding
import me.gujun.android.span.span
class DebugAnalyticsFragment : VectorBaseFragment<FragmentDebugAnalyticsBinding>() {

View File

@ -23,9 +23,9 @@ import android.widget.Spinner
import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R
import im.vector.app.core.epoxy.VectorEpoxyHolder
import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.application.R
@EpoxyModelClass
abstract class BooleanFeatureItem : VectorEpoxyModel<BooleanFeatureItem.Holder>(R.layout.item_feature) {
@ -70,8 +70,8 @@ abstract class BooleanFeatureItem : VectorEpoxyModel<BooleanFeatureItem.Holder>(
}
class Holder : VectorEpoxyHolder() {
val label by bind<TextView>(im.vector.app.R.id.feature_label)
val optionsSpinner by bind<Spinner>(im.vector.app.R.id.feature_options)
val label by bind<TextView>(R.id.feature_label)
val optionsSpinner by bind<Spinner>(R.id.feature_options)
}
interface Listener {

View File

@ -66,13 +66,13 @@ class DebugVectorOverrides(private val context: Context) : VectorOverrides {
suspend fun setHomeserverCapabilities(block: HomeserverCapabilitiesOverride.() -> HomeserverCapabilitiesOverride) {
val capabilitiesOverride = block(forceHomeserverCapabilities.firstOrNull() ?: HomeserverCapabilitiesOverride(null, null))
context.dataStore.edit { settings ->
when (capabilitiesOverride.canChangeDisplayName) {
when (val canChangeDisplayName = capabilitiesOverride.canChangeDisplayName) {
null -> settings.remove(forceCanChangeDisplayName)
else -> settings[forceCanChangeDisplayName] = capabilitiesOverride.canChangeDisplayName
else -> settings[forceCanChangeDisplayName] = canChangeDisplayName
}
when (capabilitiesOverride.canChangeAvatar) {
when (val canChangeAvatar = capabilitiesOverride.canChangeAvatar) {
null -> settings.remove(forceCanChangeAvatar)
else -> settings[forceCanChangeAvatar] = capabilitiesOverride.canChangeAvatar
else -> settings[forceCanChangeAvatar] = canChangeAvatar
}
}
}

View File

@ -23,9 +23,9 @@ import android.widget.Spinner
import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R
import im.vector.app.core.epoxy.VectorEpoxyHolder
import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.application.R
@EpoxyModelClass
abstract class EnumFeatureItem : VectorEpoxyModel<EnumFeatureItem.Holder>(R.layout.item_feature) {
@ -70,8 +70,8 @@ abstract class EnumFeatureItem : VectorEpoxyModel<EnumFeatureItem.Holder>(R.layo
}
class Holder : VectorEpoxyHolder() {
val label by bind<TextView>(im.vector.app.R.id.feature_label)
val optionsSpinner by bind<Spinner>(im.vector.app.R.id.feature_options)
val label by bind<TextView>(R.id.feature_label)
val optionsSpinner by bind<Spinner>(R.id.feature_options)
}
interface Listener {

View File

@ -25,7 +25,7 @@ import com.airbnb.mvrx.withState
import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.core.epoxy.onClick
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.databinding.FragmentDebugMemoryLeaksBinding
import im.vector.application.databinding.FragmentDebugMemoryLeaksBinding
@AndroidEntryPoint
class DebugMemoryLeaksFragment :

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2021 New Vector Ltd
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.

View File

@ -21,9 +21,9 @@ import android.widget.TextView
import androidx.core.content.ContextCompat
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R
import im.vector.app.core.epoxy.VectorEpoxyHolder
import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.application.R
import me.gujun.android.span.image
import me.gujun.android.span.span
import org.matrix.android.sdk.api.session.crypto.verification.EmojiRepresentation

View File

@ -23,7 +23,7 @@ import android.view.ViewGroup
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.databinding.FragmentDebugPrivateSettingsBinding
import im.vector.application.databinding.FragmentDebugPrivateSettingsBinding
class DebugPrivateSettingsFragment : VectorBaseFragment<FragmentDebugPrivateSettingsBinding>() {

View File

@ -24,7 +24,7 @@ import android.view.View
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.LinearLayout
import im.vector.app.databinding.ViewBooleanDropdownBinding
import im.vector.application.databinding.ViewBooleanDropdownBinding
class OverrideDropdownView @JvmOverloads constructor(
context: Context,

View File

@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="im.vector.app">
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
@ -15,7 +14,7 @@
<application>
<receiver
android:name=".fdroid.receiver.OnApplicationUpgradeOrRebootReceiver"
android:name="im.vector.app.fdroid.receiver.OnApplicationUpgradeOrRebootReceiver"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
@ -24,12 +23,12 @@
</receiver>
<receiver
android:name=".fdroid.receiver.AlarmSyncBroadcastReceiver"
android:name="im.vector.app.fdroid.receiver.AlarmSyncBroadcastReceiver"
android:enabled="true"
android:exported="false" />
<receiver
android:name=".fdroid.receiver.KeepInternalDistributor"
android:name="im.vector.app.fdroid.receiver.KeepInternalDistributor"
android:enabled="true"
android:exported="false">
<intent-filter>
@ -43,7 +42,7 @@
</receiver>
<service
android:name=".fdroid.service.GuardAndroidService"
android:name="im.vector.app.fdroid.service.GuardAndroidService"
android:exported="false"
tools:ignore="Instantiatable" />

View File

@ -3,6 +3,7 @@ apply plugin: 'kotlin-android'
apply plugin: 'kotlin-parcelize'
apply plugin: 'kotlin-kapt'
apply plugin: 'dagger.hilt.android.plugin'
apply plugin: 'app.cash.paparazzi'
if (project.hasProperty("coverage")) {
apply plugin: 'jacoco'
@ -66,30 +67,6 @@ android {
testCoverageEnabled = coverage.enableTestCoverage
}
}
nightly {
initWith release
matchingFallbacks = ['release']
}
release
}
flavorDimensions "store"
productFlavors {
gplay {
dimension "store"
isDefault = true
buildConfigField "String", "SHORT_FLAVOR_DESCRIPTION", "\"G\""
buildConfigField "String", "FLAVOR_DESCRIPTION", "\"GooglePlay\""
}
fdroid {
dimension "store"
buildConfigField "String", "SHORT_FLAVOR_DESCRIPTION", "\"F\""
buildConfigField "String", "FLAVOR_DESCRIPTION", "\"FDroid\""
}
}
compileOptions {
@ -118,10 +95,6 @@ android {
test {
java.srcDirs += "src/sharedTest/java"
}
// Add sourceSets for `release` version when building `nightly`
nightly {
java.srcDirs += "src/release/java"
}
}
buildFeatures {
@ -190,12 +163,6 @@ dependencies {
// Snap Helper https://github.com/rubensousa/GravitySnapHelper
api 'com.github.rubensousa:gravitysnaphelper:2.2.2'
// Nightly
// API-only library
gplayImplementation libs.google.appdistributionApi
// Full SDK implementation
gplayImplementation libs.google.appdistribution
// Work
api libs.androidx.work
@ -211,7 +178,7 @@ dependencies {
// UI
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
implementation libs.google.material
implementation 'me.gujun.android:span:1.7'
api 'me.gujun.android:span:1.7'
implementation libs.markwon.core
implementation libs.markwon.extLatex
implementation libs.markwon.inlineParser
@ -263,15 +230,6 @@ dependencies {
// UnifiedPush
implementation 'com.github.UnifiedPush:android-connector:2.0.1'
// UnifiedPush gplay flavor only
gplayImplementation('com.github.UnifiedPush:android-embedded_fcm_distributor:2.1.2') {
exclude group: 'com.google.firebase', module: 'firebase-core'
exclude group: 'com.google.firebase', module: 'firebase-analytics'
exclude group: 'com.google.firebase', module: 'firebase-measurement-connector'
}
// OSS License, gplay flavor only
gplayImplementation 'com.google.android.gms:play-services-oss-licenses:17.0.0'
implementation "androidx.emoji2:emoji2:1.1.0"
@ -307,14 +265,12 @@ dependencies {
implementation 'commons-codec:commons-codec:1.15'
// MapTiler
fdroidApi(libs.maplibre.androidSdk) {
api(libs.maplibre.androidSdk) {
exclude group: 'com.google.android.gms', module: 'play-services-location'
}
fdroidApi(libs.maplibre.pluginAnnotation) {
api(libs.maplibre.pluginAnnotation) {
exclude group: 'com.google.android.gms', module: 'play-services-location'
}
gplayApi libs.maplibre.androidSdk
gplayApi libs.maplibre.pluginAnnotation
// TESTS
testImplementation libs.tests.junit
@ -327,19 +283,6 @@ dependencies {
exclude group: "org.jetbrains.kotlinx", module: "kotlinx-coroutines-debug"
}
// Flipper, debug builds only
debugImplementation(libs.flipper.flipper) {
exclude group: 'com.facebook.fbjni', module: 'fbjni'
}
debugImplementation(libs.flipper.flipperNetworkPlugin) {
exclude group: 'com.facebook.fbjni', module: 'fbjni'
}
debugImplementation 'com.facebook.soloader:soloader:0.10.4'
debugImplementation "com.kgurgul.flipper:flipper-realm-android:2.2.0"
// Activate when you want to check for leaks, from time to time.
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.9.1'
androidTestImplementation libs.androidx.testCore
androidTestImplementation libs.androidx.testRunner
androidTestImplementation libs.androidx.testRules

View File

@ -1,23 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="im.vector.app">
<application>
<activity android:name=".features.debug.TestLinkifyActivity" />
<activity android:name=".features.debug.DebugPermissionActivity" />
<activity android:name=".features.debug.analytics.DebugAnalyticsActivity" />
<activity android:name=".features.debug.settings.DebugPrivateSettingsActivity" />
<activity android:name=".features.debug.sas.DebugSasEmojiActivity" />
<activity android:name=".features.debug.features.DebugFeaturesSettingsActivity" />
<activity android:name=".features.debug.DebugMenuActivity" />
<activity android:name=".features.debug.leak.DebugMemoryLeaksActivity" />
<activity
android:name="com.facebook.flipper.android.diagnostics.FlipperDiagnosticActivity"
android:exported="true" />
<!-- Used for UI tests to display the BiometricPrompt. It's normal that it appears as an error. -->
<activity android:exported="false" android:name=".features.pin.lockscreen.tests.LockScreenTestActivity" />
</application>
</manifest>

View File

@ -20,8 +20,11 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.R
import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.extensions.associateContentStateWith
import im.vector.app.core.extensions.clearErrorOnChange
import im.vector.app.core.extensions.content
@ -30,6 +33,7 @@ import im.vector.app.core.extensions.realignPercentagesToParent
import im.vector.app.core.extensions.setOnImeDoneListener
import im.vector.app.core.extensions.showKeyboard
import im.vector.app.core.extensions.toReducedUrl
import im.vector.app.core.resources.StringProvider
import im.vector.app.core.utils.ensureProtocol
import im.vector.app.core.utils.ensureTrailingSlash
import im.vector.app.core.utils.openUrlInExternalBrowser
@ -37,42 +41,41 @@ import im.vector.app.databinding.FragmentFtueServerSelectionCombinedBinding
import im.vector.app.features.onboarding.OnboardingAction
import im.vector.app.features.onboarding.OnboardingFlow
import im.vector.app.features.onboarding.OnboardingViewEvents
import im.vector.app.features.onboarding.OnboardingViewModel
import im.vector.app.features.onboarding.OnboardingViewState
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import org.matrix.android.sdk.api.failure.isHomeserverUnavailable
import reactivecircus.flowbinding.android.view.clicks
import javax.inject.Inject
@AndroidEntryPoint
class FtueAuthCombinedServerSelectionFragment :
AbstractFtueAuthFragment<FragmentFtueServerSelectionCombinedBinding>() {
class FtueAuthCombinedServerSelectionFragment : AbstractFtueAuthFragment<FragmentFtueServerSelectionCombinedBinding>() {
@Inject lateinit var stringProvider: StringProvider
private lateinit var controller: Controller
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentFtueServerSelectionCombinedBinding {
return FragmentFtueServerSelectionCombinedBinding.inflate(inflater, container, false)
return FragmentFtueServerSelectionCombinedBinding.inflate(inflater, container, false).also {
controller = Controller(viewLifecycleOwner, it, stringProvider, errorFormatter)
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupViews()
}
controller.listener = object : Controller.Listener {
override fun onNavigationClicked() {
viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnBack))
}
private fun setupViews() {
views.chooseServerRoot.realignPercentagesToParent()
views.chooseServerToolbar.setNavigationOnClickListener {
viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnBack))
}
views.chooseServerInput.associateContentStateWith(button = views.chooseServerSubmit, enabledPredicate = { canSubmit(it) })
views.chooseServerInput.setOnImeDoneListener {
if (canSubmit(views.chooseServerInput.content())) {
updateServerUrl()
override fun updateServerUrl() {
viewModel.handle(OnboardingAction.HomeServerChange.EditHomeServer(views.chooseServerInput.content().ensureProtocol().ensureTrailingSlash()))
}
override fun getInTouchClicked() {
openUrlInExternalBrowser(requireContext(), getString(R.string.ftue_ems_url))
}
}
views.chooseServerGetInTouch.debouncedClicks { openUrlInExternalBrowser(requireContext(), getString(R.string.ftue_ems_url)) }
views.chooseServerSubmit.debouncedClicks { updateServerUrl() }
views.chooseServerInput.clearErrorOnChange(viewLifecycleOwner)
}
private fun canSubmit(url: String) = url.isNotEmpty()
private fun updateServerUrl() {
viewModel.handle(OnboardingAction.HomeServerChange.EditHomeServer(views.chooseServerInput.content().ensureProtocol().ensureTrailingSlash()))
}
override fun resetViewModel() {
@ -80,13 +83,47 @@ class FtueAuthCombinedServerSelectionFragment :
}
override fun updateWithState(state: OnboardingViewState) {
views.chooseServerHeaderSubtitle.setText(
when (state.onboardingFlow) {
OnboardingFlow.SignIn -> R.string.ftue_auth_choose_server_sign_in_subtitle
OnboardingFlow.SignUp -> R.string.ftue_auth_choose_server_subtitle
else -> throw IllegalStateException("Invalid flow state")
controller.setData(state)
}
override fun onError(throwable: Throwable) {
controller.setError(throwable)
}
class Controller(
lifecycleOwner: LifecycleOwner,
private val views: FragmentFtueServerSelectionCombinedBinding,
private val stringProvider: StringProvider,
private val errorFormatter: ErrorFormatter,
private val viewModel: OnboardingViewModel,
) {
var listener: Listener? = null
init {
views.chooseServerRoot.realignPercentagesToParent()
views.chooseServerToolbar.setNavigationOnClickListener { listener?.onNavigationClicked() }
views.chooseServerInput.associateContentStateWith(button = views.chooseServerSubmit, enabledPredicate = { canSubmit(it) })
views.chooseServerInput.setOnImeDoneListener {
if (canSubmit(views.chooseServerInput.content())) {
listener?.updateServerUrl()
}
)
}
views.chooseServerGetInTouch.debouncedClicks(lifecycleOwner) { listener?.getInTouchClicked() }
views.chooseServerSubmit.debouncedClicks(lifecycleOwner) { listener?.updateServerUrl() }
views.chooseServerInput.clearErrorOnChange(lifecycleOwner)
}
private fun canSubmit(url: String) = url.isNotEmpty()
fun setData(state: OnboardingViewState) {
views.chooseServerHeaderSubtitle.setText(
when (state.onboardingFlow) {
OnboardingFlow.SignIn -> R.string.ftue_auth_choose_server_sign_in_subtitle
OnboardingFlow.SignUp -> R.string.ftue_auth_choose_server_subtitle
else -> throw IllegalStateException("Invalid flow state")
}
)
if (views.chooseServerInput.content().isEmpty()) {
val userUrlInput = state.selectedHomeserver.userFacingUrl?.toReducedUrlKeepingSchemaIfInsecure() ?: viewModel.getDefaultHomeserverUrl()
@ -97,12 +134,26 @@ class FtueAuthCombinedServerSelectionFragment :
views.chooseServerInput.editText().showKeyboard(true)
}
override fun onError(throwable: Throwable) {
views.chooseServerInput.error = when {
throwable.isHomeserverUnavailable() -> getString(R.string.login_error_homeserver_not_found)
else -> errorFormatter.toHumanReadable(throwable)
fun setError(throwable: Throwable) {
views.chooseServerInput.error = when {
throwable.isHomeserverUnavailable() -> stringProvider.getString(R.string.login_error_homeserver_not_found)
else -> errorFormatter.toHumanReadable(throwable)
}
println(views.chooseServerInput.error)
}
private fun String.toReducedUrlKeepingSchemaIfInsecure() = toReducedUrl(keepSchema = this.startsWith("http://"))
interface Listener {
fun onNavigationClicked()
fun updateServerUrl()
fun getInTouchClicked()
}
}
private fun String.toReducedUrlKeepingSchemaIfInsecure() = toReducedUrl(keepSchema = this.startsWith("http://"))
}
private fun View.debouncedClicks(lifecycleOwner: LifecycleOwner, onClicked: () -> Unit) {
clicks()
.onEach { onClicked() }
.launchIn(lifecycleOwner.lifecycleScope)
}

View File

@ -120,7 +120,7 @@
app:layout_constraintHeight_percent="0.03"
app:layout_constraintTop_toBottomOf="@id/chooseServerInput" />
<Button
<com.google.android.material.button.MaterialButton
android:id="@+id/chooseServerSubmit"
style="@style/Widget.Vector.Button.Login"
android:layout_width="0dp"

View File

@ -0,0 +1,181 @@
/*
* Copyright (c) 2022 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app
import android.os.Build
import android.view.View
import android.widget.FrameLayout
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import androidx.recyclerview.widget.RecyclerView
import app.cash.paparazzi.DeviceConfig.Companion.PIXEL_3
import app.cash.paparazzi.Paparazzi
import com.airbnb.epoxy.EpoxyController
import com.airbnb.mvrx.Success
import im.vector.app.core.error.DefaultErrorFormatter
import im.vector.app.core.extensions.configureWith
import im.vector.app.core.resources.StringProvider
import im.vector.app.core.utils.DimensionConverter
import im.vector.app.databinding.FragmentFtueServerSelectionCombinedBinding
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.detail.timeline.format.RoomHistoryVisibilityFormatter
import im.vector.app.features.onboarding.OnboardingFlow
import im.vector.app.features.onboarding.OnboardingViewState
import im.vector.app.features.onboarding.SelectedHomeserverState
import im.vector.app.features.onboarding.ftueauth.FtueAuthCombinedServerSelectionFragment
import im.vector.app.features.roomprofile.settings.RoomSettingsController
import im.vector.app.features.roomprofile.settings.RoomSettingsViewState
import im.vector.app.test.fakes.FakeErrorFormatter
import im.vector.app.test.fakes.FakeVectorPreferences
import io.mockk.coJustRun
import io.mockk.every
import io.mockk.mockk
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.util.MatrixItem
import java.lang.reflect.Field
import java.lang.reflect.Modifier
class PaparazziScreenshotTest {
@get:Rule
val paparazzi = Paparazzi(
deviceConfig = PIXEL_3,
theme = "Theme.Vector.Light",
)
@Before
fun setUp() {
setFinalStaticValue(Build::class.java.getDeclaredField("MANUFACTURER"), "GOOGLE")
}
@Test
fun `empty server selection`() {
val (view, binding) = paparazzi.inflate(R.layout.fragment_ftue_server_selection_combined, FragmentFtueServerSelectionCombinedBinding::bind)
val stringProvider = StringProvider(paparazzi.resources)
val lifecycleOwner = fakeLifecycleOwner()
FtueAuthCombinedServerSelectionFragment.Controller(lifecycleOwner, binding, stringProvider, FakeErrorFormatter())
.setData(
OnboardingViewState(
onboardingFlow = OnboardingFlow.SignIn,
)
)
paparazzi.snapshot(view)
}
@Test
fun `server selection with content`() {
val (view, binding) = paparazzi.inflate(R.layout.fragment_ftue_server_selection_combined, FragmentFtueServerSelectionCombinedBinding::bind)
val stringProvider = StringProvider(paparazzi.resources)
val lifecycleOwner = fakeLifecycleOwner()
FtueAuthCombinedServerSelectionFragment.Controller(lifecycleOwner, binding, stringProvider, DefaultErrorFormatter(stringProvider))
.setData(
OnboardingViewState(
onboardingFlow = OnboardingFlow.SignIn,
selectedHomeserver = SelectedHomeserverState(userFacingUrl = "matrix.org")
)
)
paparazzi.snapshot(view)
}
@Test
fun `room settings`() {
// Material components aren't fully supported yet https://github.com/cashapp/paparazzi/issues/223
val strings = StringProvider(paparazzi.resources)
val fakeAvatarRender = FakeAvatarRender()
val fakeDimensionConverter = FakeDimensionConverter()
val fakeVectorPreferences = FakeVectorPreferences().also { it.givenDeveloperMode(false) }
val controller = RoomSettingsController(
strings,
fakeAvatarRender.instance,
fakeDimensionConverter.instance,
RoomHistoryVisibilityFormatter(strings),
fakeVectorPreferences.instance,
)
val view = inflateRecyclerView(controller)
controller.setData(
RoomSettingsViewState(
roomId = "room-id",
roomSummary = Success(
RoomSummary(
roomId = "!room-id",
isEncrypted = true,
encryptionEventTs = null,
typingUsers = emptyList(),
displayName = "A room name",
topic = "A room topic",
)
)
)
)
paparazzi.snapshot(view)
}
private fun inflateRecyclerView(controller: EpoxyController): View {
val view = paparazzi.inflate<FrameLayout>(R.layout.fragment_generic_recycler)
val recyclerView = view.findViewById<RecyclerView>(R.id.genericRecyclerView)
recyclerView.configureWith(controller)
return view
}
}
fun setFinalStaticValue(field: Field, value: Any) {
val modifiersField = Field::class.java.getDeclaredField("modifiers")
modifiersField.isAccessible = true
modifiersField.setInt(field, field.modifiers and Modifier.FINAL.inv())
field.isAccessible = true
field.set(null, value)
}
private fun <B> Paparazzi.inflate(layoutId: Int, binder: (View) -> B): Pair<View, B> {
val view = inflate<View>(layoutId)
val binding = binder(view)
return view to binding
}
private fun fakeLifecycleOwner(): LifecycleOwner {
val lifecycleOwner = mockk<LifecycleOwner>()
every { lifecycleOwner.lifecycle } returns LifecycleRegistry(lifecycleOwner)
return lifecycleOwner
}
class FakeDimensionConverter {
val instance = mockk<DimensionConverter>().also {
every { it.dpToPx(any()) }.answers { arg ->
arg.invocation.args.first() as Int
}
}
}
class FakeAvatarRender {
val instance = mockk<AvatarRenderer>().also {
coJustRun { it.render(any<MatrixItem>(), any()) }
}
}

View File

@ -36,4 +36,8 @@ class FakeVectorPreferences {
fun verifySetSpaceBackstack(value: List<String?>, inverse: Boolean = false) {
verify(inverse = inverse) { instance.setSpaceBackstack(value) }
}
fun givenDeveloperMode(isDeveloperMode: Boolean) {
every { instance.developerMode() } returns isDeveloperMode
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB