From 1c28dc3a007f9748e61a9171ba7d64658a6a4aea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Santos?= Date: Wed, 31 Mar 2021 22:25:49 +0100 Subject: [PATCH 1/3] feat: Add peer screen --- app/build.gradle | 1 + .../relaycorp/ping/test/ActivityAssertions.kt | 34 ++++++ .../relaycorp/ping/test/AppTestComponent.kt | 4 +- .../ping/test/BaseActivityTestRule.kt | 2 + .../tech/relaycorp/ping/test/IntentsRule.kt | 17 +++ .../ui/peers/AddPublicPeerActivityTest.kt | 89 +++++++++++++++ .../ui/{peer => peers}/PeerActivityTest.kt | 3 +- app/src/main/AndroidManifest.xml | 5 +- .../ping/common/di/ActivityComponent.kt | 4 +- .../java/tech/relaycorp/ping/data/ReadFile.kt | 16 +++ .../tech/relaycorp/ping/ui/BaseActivity.kt | 16 ++- .../ping/ui/common/ActivityResult.kt | 9 ++ .../ping/ui/common/SimpleTextWatcher.kt | 18 +++ .../ping/ui/peers/AddPublicPeerActivity.kt | 108 ++++++++++++++++++ .../ping/ui/peers/AddPublicPeerViewModel.kt | 105 +++++++++++++++++ .../ping/ui/{peer => peers}/PeerActivity.kt | 2 +- .../relaycorp/ping/ui/peers/PeerItemView.kt | 4 + .../ping/ui/{peer => peers}/PeerViewModel.kt | 2 +- .../relaycorp/ping/ui/peers/PeersActivity.kt | 8 ++ app/src/main/res/drawable/ic_clear.xml | 10 ++ .../res/layout/activity_add_public_peer.xml | 89 +++++++++++++++ app/src/main/res/layout/activity_peer.xml | 2 +- app/src/main/res/layout/item_peer.xml | 2 +- app/src/main/res/layout/view_field.xml | 6 +- app/src/main/res/menu/add_peer.xml | 8 ++ app/src/main/res/values/strings.xml | 8 ++ app/src/main/res/values/styles.xml | 18 +++ 27 files changed, 574 insertions(+), 16 deletions(-) create mode 100644 app/src/androidTest/java/tech/relaycorp/ping/test/ActivityAssertions.kt create mode 100644 app/src/androidTest/java/tech/relaycorp/ping/test/IntentsRule.kt create mode 100644 app/src/androidTest/java/tech/relaycorp/ping/ui/peers/AddPublicPeerActivityTest.kt rename app/src/androidTest/java/tech/relaycorp/ping/ui/{peer => peers}/PeerActivityTest.kt (95%) create mode 100644 app/src/main/java/tech/relaycorp/ping/data/ReadFile.kt create mode 100644 app/src/main/java/tech/relaycorp/ping/ui/common/ActivityResult.kt create mode 100644 app/src/main/java/tech/relaycorp/ping/ui/common/SimpleTextWatcher.kt create mode 100644 app/src/main/java/tech/relaycorp/ping/ui/peers/AddPublicPeerActivity.kt create mode 100644 app/src/main/java/tech/relaycorp/ping/ui/peers/AddPublicPeerViewModel.kt rename app/src/main/java/tech/relaycorp/ping/ui/{peer => peers}/PeerActivity.kt (98%) rename app/src/main/java/tech/relaycorp/ping/ui/{peer => peers}/PeerViewModel.kt (97%) create mode 100644 app/src/main/res/drawable/ic_clear.xml create mode 100644 app/src/main/res/layout/activity_add_public_peer.xml create mode 100644 app/src/main/res/menu/add_peer.xml diff --git a/app/build.gradle b/app/build.gradle index 30d215a6..5e5fcb89 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -129,6 +129,7 @@ dependencies { androidTestImplementation 'androidx.test.ext:junit:1.1.2' androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinCoroutinesVersion" androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' + androidTestImplementation 'androidx.test.espresso:espresso-intents:3.3.0' androidTestImplementation('com.schibsted.spain:barista:3.7.0') { exclude group: 'org.jetbrains.kotlin' } diff --git a/app/src/androidTest/java/tech/relaycorp/ping/test/ActivityAssertions.kt b/app/src/androidTest/java/tech/relaycorp/ping/test/ActivityAssertions.kt new file mode 100644 index 00000000..6e19f78c --- /dev/null +++ b/app/src/androidTest/java/tech/relaycorp/ping/test/ActivityAssertions.kt @@ -0,0 +1,34 @@ +package tech.relaycorp.ping.test + +import android.app.Activity +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.runner.lifecycle.ActivityLifecycleMonitorRegistry +import androidx.test.runner.lifecycle.Stage +import org.junit.Assert +import tech.relaycorp.ping.test.WaitAssertions.waitFor +import kotlin.reflect.KClass + +object ActivityAssertions { + val currentActivity: Activity? + get() { + InstrumentationRegistry.getInstrumentation().waitForIdleSync() + val activity = arrayOfNulls(1) + InstrumentationRegistry.getInstrumentation().runOnMainSync { + val activities = + ActivityLifecycleMonitorRegistry.getInstance() + .getActivitiesInStage(Stage.RESUMED) + if (activities.iterator().hasNext()) { + activity[0] = activities.iterator().next() + } + } + return activity[0] + } + + fun assertCurrentActivity(activityKlass: KClass) { + Assert.assertEquals(activityKlass.java.name, currentActivity?.componentName?.className) + } + + fun waitForCurrentActivityToBe(activityKlass: KClass) { + waitFor { assertCurrentActivity(activityKlass) } + } +} diff --git a/app/src/androidTest/java/tech/relaycorp/ping/test/AppTestComponent.kt b/app/src/androidTest/java/tech/relaycorp/ping/test/AppTestComponent.kt index ede6702a..a4faafd0 100644 --- a/app/src/androidTest/java/tech/relaycorp/ping/test/AppTestComponent.kt +++ b/app/src/androidTest/java/tech/relaycorp/ping/test/AppTestComponent.kt @@ -5,7 +5,8 @@ import tech.relaycorp.ping.AppModule import tech.relaycorp.ping.awala.AwalaModule import tech.relaycorp.ping.common.di.AppComponent import tech.relaycorp.ping.ui.main.MainActivityTest -import tech.relaycorp.ping.ui.peer.PeerActivityTest +import tech.relaycorp.ping.ui.peers.AddPublicPeerActivityTest +import tech.relaycorp.ping.ui.peers.PeerActivityTest import tech.relaycorp.ping.ui.peers.PeersActivityTest import tech.relaycorp.ping.ui.ping.PingActivityTest import javax.inject.Singleton @@ -21,6 +22,7 @@ interface AppTestComponent : AppComponent { // Tests + fun inject(test: AddPublicPeerActivityTest) fun inject(test: MainActivityTest) fun inject(test: PeerActivityTest) fun inject(test: PeersActivityTest) diff --git a/app/src/androidTest/java/tech/relaycorp/ping/test/BaseActivityTestRule.kt b/app/src/androidTest/java/tech/relaycorp/ping/test/BaseActivityTestRule.kt index 0a488abe..10b59260 100644 --- a/app/src/androidTest/java/tech/relaycorp/ping/test/BaseActivityTestRule.kt +++ b/app/src/androidTest/java/tech/relaycorp/ping/test/BaseActivityTestRule.kt @@ -19,6 +19,7 @@ class BaseActivityTestRule( private val clearPreferencesRule: ClearPreferencesRule = ClearPreferencesRule() private val clearDatabaseRule: ClearTestDatabaseRule = ClearTestDatabaseRule() private val clearFilesRule: ClearFilesRule = ClearFilesRule() + private val intentsRule: IntentsRule = IntentsRule() private val activityTestRule: ActivityTestRule = ActivityTestRule( activityClass.java, true, @@ -30,6 +31,7 @@ class BaseActivityTestRule( .around(clearPreferencesRule) .around(clearDatabaseRule) .around(clearFilesRule) + .around(intentsRule) .apply(base, description) } diff --git a/app/src/androidTest/java/tech/relaycorp/ping/test/IntentsRule.kt b/app/src/androidTest/java/tech/relaycorp/ping/test/IntentsRule.kt new file mode 100644 index 00000000..64c92278 --- /dev/null +++ b/app/src/androidTest/java/tech/relaycorp/ping/test/IntentsRule.kt @@ -0,0 +1,17 @@ +package tech.relaycorp.ping.test + +import androidx.test.espresso.intent.Intents +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runners.model.Statement + +class IntentsRule : TestRule { + override fun apply(base: Statement, description: Description?) = + object : Statement() { + override fun evaluate() { + Intents.init() + base.evaluate() + Intents.release() + } + } +} diff --git a/app/src/androidTest/java/tech/relaycorp/ping/ui/peers/AddPublicPeerActivityTest.kt b/app/src/androidTest/java/tech/relaycorp/ping/ui/peers/AddPublicPeerActivityTest.kt new file mode 100644 index 00000000..646dab5c --- /dev/null +++ b/app/src/androidTest/java/tech/relaycorp/ping/ui/peers/AddPublicPeerActivityTest.kt @@ -0,0 +1,89 @@ +package tech.relaycorp.ping.ui.peers + +import android.app.Activity +import android.app.Instrumentation +import android.content.Intent +import android.net.Uri +import androidx.test.espresso.intent.Intents.intending +import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.schibsted.spain.barista.assertion.BaristaEnabledAssertions.assertDisabled +import com.schibsted.spain.barista.assertion.BaristaEnabledAssertions.assertEnabled +import com.schibsted.spain.barista.assertion.BaristaVisibilityAssertions.assertDisplayed +import com.schibsted.spain.barista.interaction.BaristaClickInteractions.clickOn +import com.schibsted.spain.barista.interaction.BaristaEditTextInteractions.writeTo +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import tech.relaycorp.ping.R +import tech.relaycorp.ping.test.ActivityAssertions.waitForCurrentActivityToBe +import tech.relaycorp.ping.test.AppTestProvider.component +import tech.relaycorp.ping.test.AppTestProvider.context +import tech.relaycorp.ping.test.BaseActivityTestRule + +@RunWith(AndroidJUnit4::class) +class AddPublicPeerActivityTest { + + @Rule + @JvmField + val testRule = BaseActivityTestRule(PeersActivity::class, false) + + @Before + fun setUp() { + component.inject(this) + } + + @Test + fun addPublicPeerSuccessfully() { + testRule.start() + clickOn(R.id.addPeer) + clickOn(R.string.peer_public) + + assertDisabled(R.id.save) + + val address = "ping.awala.services" + writeTo(R.id.addressEdit, address) + + intending(hasAction(Intent.ACTION_OPEN_DOCUMENT)) + .respondWith( + Instrumentation.ActivityResult( + Activity.RESULT_OK, + Intent().setData( + Uri.parse("android.resource://${context.packageName}/${R.raw.ping_awala_identity}") + ) + ) + ) + clickOn(R.string.peer_certificate_button) + + assertEnabled(R.id.save) + clickOn(R.id.save) + + waitForCurrentActivityToBe(PeersActivity::class) + assertDisplayed(address) + } + + @Test + fun addPublicPeerFailure() { + testRule.start() + clickOn(R.id.addPeer) + clickOn(R.string.peer_public) + + val address = "ping.awala.services" + writeTo(R.id.addressEdit, address) + + intending(hasAction(Intent.ACTION_OPEN_DOCUMENT)) + .respondWith( + Instrumentation.ActivityResult( + Activity.RESULT_OK, + Intent().setData( + Uri.parse("android.resource://invalid") + ) + ) + ) + clickOn(R.string.peer_certificate_button) + + clickOn(R.id.save) + assertDisplayed(R.string.peer_add_error) + } +} diff --git a/app/src/androidTest/java/tech/relaycorp/ping/ui/peer/PeerActivityTest.kt b/app/src/androidTest/java/tech/relaycorp/ping/ui/peers/PeerActivityTest.kt similarity index 95% rename from app/src/androidTest/java/tech/relaycorp/ping/ui/peer/PeerActivityTest.kt rename to app/src/androidTest/java/tech/relaycorp/ping/ui/peers/PeerActivityTest.kt index ad3e16cd..196a8c6c 100644 --- a/app/src/androidTest/java/tech/relaycorp/ping/ui/peer/PeerActivityTest.kt +++ b/app/src/androidTest/java/tech/relaycorp/ping/ui/peers/PeerActivityTest.kt @@ -1,4 +1,4 @@ -package tech.relaycorp.ping.ui.peer +package tech.relaycorp.ping.ui.peers import androidx.test.ext.junit.runners.AndroidJUnit4 import com.schibsted.spain.barista.assertion.BaristaVisibilityAssertions.assertDisplayed @@ -18,6 +18,7 @@ import tech.relaycorp.ping.test.BaseActivityTestRule import tech.relaycorp.ping.test.PublicPeerEntityFactory import tech.relaycorp.ping.test.WaitAssertions.suspendWaitFor import tech.relaycorp.ping.test.WaitAssertions.waitFor +import tech.relaycorp.ping.ui.peers.PeerActivity import javax.inject.Inject @RunWith(AndroidJUnit4::class) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 064de170..ff291a46 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -29,7 +29,10 @@ android:name=".ui.peers.PeersActivity" android:label="@string/peers" /> + diff --git a/app/src/main/java/tech/relaycorp/ping/common/di/ActivityComponent.kt b/app/src/main/java/tech/relaycorp/ping/common/di/ActivityComponent.kt index 283f12ec..ca5a2fc5 100644 --- a/app/src/main/java/tech/relaycorp/ping/common/di/ActivityComponent.kt +++ b/app/src/main/java/tech/relaycorp/ping/common/di/ActivityComponent.kt @@ -3,7 +3,8 @@ package tech.relaycorp.ping.common.di import dagger.Subcomponent import tech.relaycorp.gateway.common.di.PerActivity import tech.relaycorp.ping.ui.main.MainActivity -import tech.relaycorp.ping.ui.peer.PeerActivity +import tech.relaycorp.ping.ui.peers.AddPublicPeerActivity +import tech.relaycorp.ping.ui.peers.PeerActivity import tech.relaycorp.ping.ui.peers.PeersActivity import tech.relaycorp.ping.ui.ping.PingActivity @@ -13,6 +14,7 @@ interface ActivityComponent { // Activities + fun inject(activity: AddPublicPeerActivity) fun inject(activity: MainActivity) fun inject(activity: PeerActivity) fun inject(activity: PeersActivity) diff --git a/app/src/main/java/tech/relaycorp/ping/data/ReadFile.kt b/app/src/main/java/tech/relaycorp/ping/data/ReadFile.kt new file mode 100644 index 00000000..43884516 --- /dev/null +++ b/app/src/main/java/tech/relaycorp/ping/data/ReadFile.kt @@ -0,0 +1,16 @@ +package tech.relaycorp.ping.data + +import android.content.Context +import android.net.Uri +import javax.inject.Inject + +class ReadFile +@Inject constructor( + private val context: Context +) { + + fun read(uri: Uri) = + context.contentResolver.openInputStream(uri)?.use { + it.readBytes() + } ?: ByteArray(0) +} diff --git a/app/src/main/java/tech/relaycorp/ping/ui/BaseActivity.kt b/app/src/main/java/tech/relaycorp/ping/ui/BaseActivity.kt index 8bdccead..74dd526d 100644 --- a/app/src/main/java/tech/relaycorp/ping/ui/BaseActivity.kt +++ b/app/src/main/java/tech/relaycorp/ping/ui/BaseActivity.kt @@ -1,17 +1,19 @@ package tech.relaycorp.ping.ui -import android.os.Build +import android.content.Intent import android.os.Bundle -import android.view.View -import android.view.WindowManager import androidx.annotation.DrawableRes import androidx.appcompat.app.AppCompatActivity import androidx.core.view.WindowCompat import dev.chrisbanes.insetter.applyInsetter import kotlinx.android.synthetic.main.common_app_bar.* +import kotlinx.coroutines.channels.sendBlocking +import kotlinx.coroutines.flow.asFlow import tech.relaycorp.gateway.ui.common.MessageManager import tech.relaycorp.ping.App import tech.relaycorp.ping.R +import tech.relaycorp.ping.common.PublishFlow +import tech.relaycorp.ping.ui.common.ActivityResult abstract class BaseActivity : AppCompatActivity() { @@ -20,6 +22,9 @@ abstract class BaseActivity : AppCompatActivity() { protected val messageManager by lazy { MessageManager(this) } + protected val results get() = _results.asFlow() + private val _results = PublishFlow() + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -27,6 +32,11 @@ abstract class BaseActivity : AppCompatActivity() { WindowCompat.setDecorFitsSystemWindows(window, true) } + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + _results.sendBlocking(ActivityResult(requestCode, resultCode, data)) + } + override fun setContentView(layoutResID: Int) { super.setContentView(layoutResID) toolbarTitle?.text = title diff --git a/app/src/main/java/tech/relaycorp/ping/ui/common/ActivityResult.kt b/app/src/main/java/tech/relaycorp/ping/ui/common/ActivityResult.kt new file mode 100644 index 00000000..335ca281 --- /dev/null +++ b/app/src/main/java/tech/relaycorp/ping/ui/common/ActivityResult.kt @@ -0,0 +1,9 @@ +package tech.relaycorp.ping.ui.common + +import android.content.Intent + +data class ActivityResult( + val requestCode: Int, + val resultCode: Int, + val data: Intent? +) diff --git a/app/src/main/java/tech/relaycorp/ping/ui/common/SimpleTextWatcher.kt b/app/src/main/java/tech/relaycorp/ping/ui/common/SimpleTextWatcher.kt new file mode 100644 index 00000000..c98ae470 --- /dev/null +++ b/app/src/main/java/tech/relaycorp/ping/ui/common/SimpleTextWatcher.kt @@ -0,0 +1,18 @@ +package tech.relaycorp.ping.ui.common + +import android.text.Editable +import android.text.TextWatcher + +class SimpleTextWatcher( + private val textChanged: (String) -> Unit +) : TextWatcher { + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { + } + + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { + textChanged(s?.toString() ?: "") + } + + override fun afterTextChanged(s: Editable?) { + } +} diff --git a/app/src/main/java/tech/relaycorp/ping/ui/peers/AddPublicPeerActivity.kt b/app/src/main/java/tech/relaycorp/ping/ui/peers/AddPublicPeerActivity.kt new file mode 100644 index 00000000..9485a9fa --- /dev/null +++ b/app/src/main/java/tech/relaycorp/ping/ui/peers/AddPublicPeerActivity.kt @@ -0,0 +1,108 @@ +package tech.relaycorp.ping.ui.peers + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.core.view.isVisible +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope +import kotlinx.android.synthetic.main.activity_add_public_peer.* +import kotlinx.android.synthetic.main.common_app_bar.* +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import tech.relaycorp.ping.R +import tech.relaycorp.ping.common.di.ViewModelFactory +import tech.relaycorp.ping.ui.BaseActivity +import tech.relaycorp.ping.ui.common.SimpleTextWatcher +import javax.inject.Inject + +class AddPublicPeerActivity : BaseActivity() { + + @Inject + lateinit var viewModelFactory: ViewModelFactory + + private val viewModel by lazy { + ViewModelProvider(this, viewModelFactory).get(AddPublicPeerViewModel::class.java) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + component.inject(this) + setContentView(R.layout.activity_add_public_peer) + setupNavigation(R.drawable.ic_close) + + toolbar.inflateMenu(R.menu.add_peer) + toolbar.setOnMenuItemClickListener { + when (it.itemId) { + R.id.save -> viewModel.saveClicked() + } + true + } + + addressEdit.addTextChangedListener(SimpleTextWatcher { + viewModel.aliasChanged(it) + }) + certificateButton.setOnClickListener { + openFileDialog() + } + certificateClear.setOnClickListener { + viewModel.removeCertificateClicked() + } + + viewModel + .hasCertificate() + .onEach { + certificateButton.isVisible = !it + certificateName.isVisible = it + certificateClear.isVisible = it + } + .launchIn(lifecycleScope) + + viewModel + .saveEnabled() + .onEach { + toolbar.menu.findItem(R.id.save).isEnabled = it + } + .launchIn(lifecycleScope) + + viewModel + .errors() + .onEach { + messageManager.showError( + when (it) { + AddPublicPeerViewModel.Error.Save -> R.string.peer_add_error + } + ) + } + .launchIn(lifecycleScope) + + viewModel + .finish() + .onEach { finish() } + .launchIn(lifecycleScope) + + results + .onEach { + if (it.requestCode == PICK_CERTIFICATE && it.resultCode == Activity.RESULT_OK) { + it.data?.data?.let { uri -> viewModel.certificatePicked(uri) } + } + } + .launchIn(lifecycleScope) + } + + private fun openFileDialog() { + startActivityForResult( + Intent(Intent.ACTION_OPEN_DOCUMENT).apply { + type = "*/*" + }, + PICK_CERTIFICATE + ) + } + + companion object { + private const val PICK_CERTIFICATE = 11 + + fun getIntent(context: Context) = Intent(context, AddPublicPeerActivity::class.java) + } +} diff --git a/app/src/main/java/tech/relaycorp/ping/ui/peers/AddPublicPeerViewModel.kt b/app/src/main/java/tech/relaycorp/ping/ui/peers/AddPublicPeerViewModel.kt new file mode 100644 index 00000000..ec61e377 --- /dev/null +++ b/app/src/main/java/tech/relaycorp/ping/ui/peers/AddPublicPeerViewModel.kt @@ -0,0 +1,105 @@ +package tech.relaycorp.ping.ui.peers + +import android.net.Uri +import kotlinx.coroutines.flow.* +import tech.relaycorp.gateway.ui.BaseViewModel +import tech.relaycorp.ping.common.PublishFlow +import tech.relaycorp.ping.data.ReadFile +import tech.relaycorp.ping.domain.AddPublicPeer +import tech.relaycorp.ping.ui.common.Click +import tech.relaycorp.ping.ui.common.Finish +import tech.relaycorp.ping.ui.common.clicked +import tech.relaycorp.ping.ui.common.finish +import java.util.* +import javax.inject.Inject + +class AddPublicPeerViewModel +@Inject constructor( + addPublicPeer: AddPublicPeer, + readFile: ReadFile +) : BaseViewModel() { + + // Inputs + + private val alias = MutableStateFlow("") + fun aliasChanged(value: String) { + alias.value = value + } + + private val certificatePicks = MutableStateFlow(Optional.empty()) + fun certificatePicked(value: Uri) { + certificatePicks.value = Optional.of(value) + } + + private val removeCertificateClicks = PublishFlow() + fun removeCertificateClicked() { + removeCertificateClicks.clicked() + } + + private val saveClicks = PublishFlow() + fun saveClicked() { + saveClicks.clicked() + } + + // Outputs + + private val _hasCertificate = MutableStateFlow(false) + fun hasCertificate(): Flow = _hasCertificate.asStateFlow() + + private val _saveEnabled = MutableStateFlow(false) + fun saveEnabled(): Flow = _saveEnabled.asStateFlow() + + private val _errors = PublishFlow() + fun errors(): Flow = _errors.asFlow() + + private val _finish = PublishFlow() + fun finish(): Flow = _finish.asFlow() + + init { + val form = alias.combine(certificatePicks, ::Form) + + form + .onEach { _saveEnabled.value = it.isValid() } + .launchIn(backgroundScope) + + certificatePicks + .onEach { _hasCertificate.value = it.isPresent } + .launchIn(backgroundScope) + + removeCertificateClicks + .asFlow() + .onEach { + certificatePicks.value = Optional.empty() + _hasCertificate.value = false + } + .launchIn(backgroundScope) + + saveClicks + .asFlow() + .flatMapLatest { form.take(1) } + .filter { it.isValid() } + .onEach { f -> + val cert = readFile.read(f.certificateUri.get()) + addPublicPeer.add(f.alias, cert) + _finish.finish() + } + .catch { + _errors.send(Error.Save) + } + .launchIn(backgroundScope) + } + + enum class Error { + Save + } + + data class Form( + val alias: String, + val certificateUri: Optional + ) { + fun isValid() = + alias.isNotBlank() + && alias.matches(Regex("(\\w+\\.)+\\w+")) + && certificateUri.isPresent + } +} diff --git a/app/src/main/java/tech/relaycorp/ping/ui/peer/PeerActivity.kt b/app/src/main/java/tech/relaycorp/ping/ui/peers/PeerActivity.kt similarity index 98% rename from app/src/main/java/tech/relaycorp/ping/ui/peer/PeerActivity.kt rename to app/src/main/java/tech/relaycorp/ping/ui/peers/PeerActivity.kt index 90136aca..88d41640 100644 --- a/app/src/main/java/tech/relaycorp/ping/ui/peer/PeerActivity.kt +++ b/app/src/main/java/tech/relaycorp/ping/ui/peers/PeerActivity.kt @@ -1,4 +1,4 @@ -package tech.relaycorp.ping.ui.peer +package tech.relaycorp.ping.ui.peers import android.content.Context import android.content.Intent diff --git a/app/src/main/java/tech/relaycorp/ping/ui/peers/PeerItemView.kt b/app/src/main/java/tech/relaycorp/ping/ui/peers/PeerItemView.kt index bafa1ca3..8085eb77 100644 --- a/app/src/main/java/tech/relaycorp/ping/ui/peers/PeerItemView.kt +++ b/app/src/main/java/tech/relaycorp/ping/ui/peers/PeerItemView.kt @@ -25,5 +25,9 @@ constructor( @ModelProp fun setItem(item: Peer) { alias.text = item.alias + + setOnClickListener { + context.startActivity(PeerActivity.getIntent(context, item.privateAddress)) + } } } diff --git a/app/src/main/java/tech/relaycorp/ping/ui/peer/PeerViewModel.kt b/app/src/main/java/tech/relaycorp/ping/ui/peers/PeerViewModel.kt similarity index 97% rename from app/src/main/java/tech/relaycorp/ping/ui/peer/PeerViewModel.kt rename to app/src/main/java/tech/relaycorp/ping/ui/peers/PeerViewModel.kt index 72864df5..9e3d1869 100644 --- a/app/src/main/java/tech/relaycorp/ping/ui/peer/PeerViewModel.kt +++ b/app/src/main/java/tech/relaycorp/ping/ui/peers/PeerViewModel.kt @@ -1,4 +1,4 @@ -package tech.relaycorp.ping.ui.peer +package tech.relaycorp.ping.ui.peers import kotlinx.coroutines.flow.* import tech.relaycorp.gateway.ui.BaseViewModel diff --git a/app/src/main/java/tech/relaycorp/ping/ui/peers/PeersActivity.kt b/app/src/main/java/tech/relaycorp/ping/ui/peers/PeersActivity.kt index 6b3f2b39..d05124ac 100644 --- a/app/src/main/java/tech/relaycorp/ping/ui/peers/PeersActivity.kt +++ b/app/src/main/java/tech/relaycorp/ping/ui/peers/PeersActivity.kt @@ -65,6 +65,14 @@ class PeersActivity : BaseActivity() { .setLabel(R.string.peer_private) .create() ) + addPeer.setOnActionSelectedListener { + when (it.id) { + PUBLIC_PEER_ITEM_ID -> + startActivity(AddPublicPeerActivity.getIntent(this)) + } + addPeer.close() + true + } } private fun updateList(peers: List) { diff --git a/app/src/main/res/drawable/ic_clear.xml b/app/src/main/res/drawable/ic_clear.xml new file mode 100644 index 00000000..1ab7bacb --- /dev/null +++ b/app/src/main/res/drawable/ic_clear.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/activity_add_public_peer.xml b/app/src/main/res/layout/activity_add_public_peer.xml new file mode 100644 index 00000000..a549cc28 --- /dev/null +++ b/app/src/main/res/layout/activity_add_public_peer.xml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_peer.xml b/app/src/main/res/layout/activity_peer.xml index d8638d6c..6dba79d5 100644 --- a/app/src/main/res/layout/activity_peer.xml +++ b/app/src/main/res/layout/activity_peer.xml @@ -4,7 +4,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context=".ui.peer.PeerActivity"> + tools:context=".ui.peers.PeerActivity"> diff --git a/app/src/main/res/layout/item_peer.xml b/app/src/main/res/layout/item_peer.xml index a4aee849..1d1160f4 100644 --- a/app/src/main/res/layout/item_peer.xml +++ b/app/src/main/res/layout/item_peer.xml @@ -24,7 +24,7 @@ android:id="@+id/alias" android:layout_width="match_parent" android:layout_height="wrap_content" - android:textSize="20sp" + android:textSize="18sp" tools:text="example.org" /> diff --git a/app/src/main/res/layout/view_field.xml b/app/src/main/res/layout/view_field.xml index 396c0b0d..0ade9094 100644 --- a/app/src/main/res/layout/view_field.xml +++ b/app/src/main/res/layout/view_field.xml @@ -8,11 +8,7 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 956cea3a..2480865d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -6,6 +6,8 @@ Copied to clipboard Delete Cancel + Clear + Save Ping @@ -24,5 +26,11 @@ Add Endpoint Private Address Are you sure you want to delete this Endpoint? + Address * + Type an address + Identity Certificate * + Upload certificate + Certificate picked + Error saving new peer diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index e1f1b7d2..72ccd45e 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -1,5 +1,7 @@ + @@ -28,6 +30,9 @@ + + + + From 870fe931b296683d25010e805d545b83dfe13c4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Santos?= Date: Thu, 1 Apr 2021 12:20:18 +0100 Subject: [PATCH 2/3] Code review changes --- .../tech/relaycorp/ping/test/IntentsRule.kt | 7 +- .../ui/peers/AddPublicPeerActivityTest.kt | 20 +----- app/src/main/AndroidManifest.xml | 3 +- .../java/tech/relaycorp/ping/data/ReadFile.kt | 14 ++++ .../relaycorp/ping/domain/AddPublicPeer.kt | 10 ++- .../ping/ui/peers/AddPublicPeerActivity.kt | 42 ++++++----- .../ping/ui/peers/AddPublicPeerViewModel.kt | 72 ++++++++++++------- .../res/layout/activity_add_public_peer.xml | 2 +- app/src/main/res/values/colors.xml | 1 + app/src/main/res/values/strings.xml | 5 +- 10 files changed, 108 insertions(+), 68 deletions(-) diff --git a/app/src/androidTest/java/tech/relaycorp/ping/test/IntentsRule.kt b/app/src/androidTest/java/tech/relaycorp/ping/test/IntentsRule.kt index 64c92278..750c1e72 100644 --- a/app/src/androidTest/java/tech/relaycorp/ping/test/IntentsRule.kt +++ b/app/src/androidTest/java/tech/relaycorp/ping/test/IntentsRule.kt @@ -6,10 +6,15 @@ import org.junit.runner.Description import org.junit.runners.model.Statement class IntentsRule : TestRule { + override fun apply(base: Statement, description: Description?) = object : Statement() { override fun evaluate() { - Intents.init() + try { + Intents.init() + } catch (_: IllegalStateException) { + // Occasionally `Intents.init()` might be called twice before `Intents.release()` is called + } base.evaluate() Intents.release() } diff --git a/app/src/androidTest/java/tech/relaycorp/ping/ui/peers/AddPublicPeerActivityTest.kt b/app/src/androidTest/java/tech/relaycorp/ping/ui/peers/AddPublicPeerActivityTest.kt index 646dab5c..a7c9edfd 100644 --- a/app/src/androidTest/java/tech/relaycorp/ping/ui/peers/AddPublicPeerActivityTest.kt +++ b/app/src/androidTest/java/tech/relaycorp/ping/ui/peers/AddPublicPeerActivityTest.kt @@ -7,8 +7,6 @@ import android.net.Uri import androidx.test.espresso.intent.Intents.intending import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.schibsted.spain.barista.assertion.BaristaEnabledAssertions.assertDisabled -import com.schibsted.spain.barista.assertion.BaristaEnabledAssertions.assertEnabled import com.schibsted.spain.barista.assertion.BaristaVisibilityAssertions.assertDisplayed import com.schibsted.spain.barista.interaction.BaristaClickInteractions.clickOn import com.schibsted.spain.barista.interaction.BaristaEditTextInteractions.writeTo @@ -40,8 +38,6 @@ class AddPublicPeerActivityTest { clickOn(R.id.addPeer) clickOn(R.string.peer_public) - assertDisabled(R.id.save) - val address = "ping.awala.services" writeTo(R.id.addressEdit, address) @@ -56,7 +52,6 @@ class AddPublicPeerActivityTest { ) clickOn(R.string.peer_certificate_button) - assertEnabled(R.id.save) clickOn(R.id.save) waitForCurrentActivityToBe(PeersActivity::class) @@ -64,7 +59,7 @@ class AddPublicPeerActivityTest { } @Test - fun addPublicPeerFailure() { + fun addPublicPeerMissingCertificate() { testRule.start() clickOn(R.id.addPeer) clickOn(R.string.peer_public) @@ -72,18 +67,7 @@ class AddPublicPeerActivityTest { val address = "ping.awala.services" writeTo(R.id.addressEdit, address) - intending(hasAction(Intent.ACTION_OPEN_DOCUMENT)) - .respondWith( - Instrumentation.ActivityResult( - Activity.RESULT_OK, - Intent().setData( - Uri.parse("android.resource://invalid") - ) - ) - ) - clickOn(R.string.peer_certificate_button) - clickOn(R.id.save) - assertDisplayed(R.string.peer_add_error) + assertDisplayed(R.string.peer_add_missing_certificate) } } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ff291a46..095a65f3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -33,6 +33,7 @@ android:label="@string/peer" /> + android:label="@string/peer_public" + android:windowSoftInputMode="adjustResize" /> diff --git a/app/src/main/java/tech/relaycorp/ping/data/ReadFile.kt b/app/src/main/java/tech/relaycorp/ping/data/ReadFile.kt index 43884516..3abbe0ab 100644 --- a/app/src/main/java/tech/relaycorp/ping/data/ReadFile.kt +++ b/app/src/main/java/tech/relaycorp/ping/data/ReadFile.kt @@ -2,6 +2,7 @@ package tech.relaycorp.ping.data import android.content.Context import android.net.Uri +import android.provider.OpenableColumns import javax.inject.Inject class ReadFile @@ -13,4 +14,17 @@ class ReadFile context.contentResolver.openInputStream(uri)?.use { it.readBytes() } ?: ByteArray(0) + + fun getFileName(uri: Uri): String { + if (uri.scheme.equals("content")) { + context.contentResolver + .query(uri, null, null, null, null) + ?.use { cursor -> + if (cursor.moveToFirst()) { + return cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)) + } + } + } + return uri.path?.split("/")?.lastOrNull() ?: "" + } } diff --git a/app/src/main/java/tech/relaycorp/ping/domain/AddPublicPeer.kt b/app/src/main/java/tech/relaycorp/ping/domain/AddPublicPeer.kt index b410af78..09dd1cf8 100644 --- a/app/src/main/java/tech/relaycorp/ping/domain/AddPublicPeer.kt +++ b/app/src/main/java/tech/relaycorp/ping/domain/AddPublicPeer.kt @@ -1,5 +1,6 @@ package tech.relaycorp.ping.domain +import tech.relaycorp.awaladroid.endpoint.InvalidThirdPartyEndpoint import tech.relaycorp.awaladroid.endpoint.PublicThirdPartyEndpoint import tech.relaycorp.ping.data.database.dao.PublicPeerDao import tech.relaycorp.ping.data.database.entity.PublicPeerEntity @@ -10,8 +11,13 @@ class AddPublicPeer private val publicPeerDao: PublicPeerDao ) { + @Throws(InvalidIdentityCertificate::class) suspend fun add(address: String, identity: ByteArray): PublicThirdPartyEndpoint { - val endpoint = PublicThirdPartyEndpoint.import(address, identity) + val endpoint = try { + PublicThirdPartyEndpoint.import(address, identity) + } catch (e: InvalidThirdPartyEndpoint) { + throw InvalidIdentityCertificate(e) + } publicPeerDao.save( PublicPeerEntity( privateAddress = endpoint.privateAddress, @@ -21,3 +27,5 @@ class AddPublicPeer return endpoint } } + +class InvalidIdentityCertificate(cause: Throwable) : Exception(cause) diff --git a/app/src/main/java/tech/relaycorp/ping/ui/peers/AddPublicPeerActivity.kt b/app/src/main/java/tech/relaycorp/ping/ui/peers/AddPublicPeerActivity.kt index 9485a9fa..698e96dd 100644 --- a/app/src/main/java/tech/relaycorp/ping/ui/peers/AddPublicPeerActivity.kt +++ b/app/src/main/java/tech/relaycorp/ping/ui/peers/AddPublicPeerActivity.kt @@ -51,30 +51,21 @@ class AddPublicPeerActivity : BaseActivity() { } viewModel - .hasCertificate() + .certificate() .onEach { - certificateButton.isVisible = !it - certificateName.isVisible = it - certificateClear.isVisible = it - } - .launchIn(lifecycleScope) - - viewModel - .saveEnabled() - .onEach { - toolbar.menu.findItem(R.id.save).isEnabled = it + val hasCertificate = it.isPresent + certificateButton.isVisible = !hasCertificate + certificateName.isVisible = hasCertificate + certificateName.text = if (hasCertificate) { + it.get().ifBlank { getString(R.string.peer_certificate_picked) } + } else "" + certificateClear.isVisible = hasCertificate } .launchIn(lifecycleScope) viewModel .errors() - .onEach { - messageManager.showError( - when (it) { - AddPublicPeerViewModel.Error.Save -> R.string.peer_add_error - } - ) - } + .onEach(this::showError) .launchIn(lifecycleScope) viewModel @@ -100,6 +91,21 @@ class AddPublicPeerActivity : BaseActivity() { ) } + private fun showError(error: AddPublicPeerViewModel.Error) { + messageManager.showError( + when (error) { + AddPublicPeerViewModel.Error.InvalidAddress -> + R.string.peer_add_invalid_address + AddPublicPeerViewModel.Error.MissingCertificate -> + R.string.peer_add_missing_certificate + AddPublicPeerViewModel.Error.InvalidCertificate -> + R.string.peer_add_invalid_certificate + AddPublicPeerViewModel.Error.GenericSave -> + R.string.peer_add_error + } + ) + } + companion object { private const val PICK_CERTIFICATE = 11 diff --git a/app/src/main/java/tech/relaycorp/ping/ui/peers/AddPublicPeerViewModel.kt b/app/src/main/java/tech/relaycorp/ping/ui/peers/AddPublicPeerViewModel.kt index ec61e377..c6695c4a 100644 --- a/app/src/main/java/tech/relaycorp/ping/ui/peers/AddPublicPeerViewModel.kt +++ b/app/src/main/java/tech/relaycorp/ping/ui/peers/AddPublicPeerViewModel.kt @@ -4,8 +4,10 @@ import android.net.Uri import kotlinx.coroutines.flow.* import tech.relaycorp.gateway.ui.BaseViewModel import tech.relaycorp.ping.common.PublishFlow +import tech.relaycorp.ping.common.element import tech.relaycorp.ping.data.ReadFile import tech.relaycorp.ping.domain.AddPublicPeer +import tech.relaycorp.ping.domain.InvalidIdentityCertificate import tech.relaycorp.ping.ui.common.Click import tech.relaycorp.ping.ui.common.Finish import tech.relaycorp.ping.ui.common.clicked @@ -21,9 +23,9 @@ class AddPublicPeerViewModel // Inputs - private val alias = MutableStateFlow("") + private val address = MutableStateFlow("") fun aliasChanged(value: String) { - alias.value = value + address.value = value } private val certificatePicks = MutableStateFlow(Optional.empty()) @@ -43,11 +45,8 @@ class AddPublicPeerViewModel // Outputs - private val _hasCertificate = MutableStateFlow(false) - fun hasCertificate(): Flow = _hasCertificate.asStateFlow() - - private val _saveEnabled = MutableStateFlow(false) - fun saveEnabled(): Flow = _saveEnabled.asStateFlow() + private val _certificate = MutableStateFlow(Optional.empty()) + fun certificate(): Flow> = _certificate.asStateFlow() private val _errors = PublishFlow() fun errors(): Flow = _errors.asFlow() @@ -56,50 +55,69 @@ class AddPublicPeerViewModel fun finish(): Flow = _finish.asFlow() init { - val form = alias.combine(certificatePicks, ::Form) - - form - .onEach { _saveEnabled.value = it.isValid() } - .launchIn(backgroundScope) + val form = address.combine(certificatePicks, ::Form) certificatePicks - .onEach { _hasCertificate.value = it.isPresent } + .element() + .onEach { _certificate.value = Optional.of(readFile.getFileName(it)) } .launchIn(backgroundScope) removeCertificateClicks .asFlow() .onEach { certificatePicks.value = Optional.empty() - _hasCertificate.value = false + _certificate.value = Optional.empty() } .launchIn(backgroundScope) saveClicks .asFlow() .flatMapLatest { form.take(1) } - .filter { it.isValid() } - .onEach { f -> - val cert = readFile.read(f.certificateUri.get()) - addPublicPeer.add(f.alias, cert) - _finish.finish() + .map { f -> + if (f.isValid()) { + val cert = readFile.read(f.certificateUri.get()) + addPublicPeer.add(f.address, cert) + _finish.finish() + } else { + _errors.send( + when { + !f.isAddressValid() -> Error.InvalidAddress + !f.isCertificatePresent() -> Error.MissingCertificate + else -> Error.GenericSave + } + ) + } } - .catch { - _errors.send(Error.Save) + .catch { exception -> + _errors.send( + when (exception) { + is InvalidIdentityCertificate -> Error.InvalidCertificate + else -> Error.GenericSave + } + ) + emit(Unit) } .launchIn(backgroundScope) } enum class Error { - Save + InvalidAddress, MissingCertificate, InvalidCertificate, GenericSave } data class Form( - val alias: String, + val address: String, val certificateUri: Optional ) { - fun isValid() = - alias.isNotBlank() - && alias.matches(Regex("(\\w+\\.)+\\w+")) - && certificateUri.isPresent + fun isAddressValid() = + address.isNotBlank() && address.matches(PUBLIC_ADDRESS_REGEX) + + fun isCertificatePresent() = certificateUri.isPresent + + fun isValid() = isAddressValid() && isCertificatePresent() + } + + companion object { + internal val PUBLIC_ADDRESS_REGEX = + Regex("([a-zA-Z0-9][a-zA-Z0-9-]{1,61}[a-zA-Z0-9]\\.)+[a-zA-Z]{2,}") } } diff --git a/app/src/main/res/layout/activity_add_public_peer.xml b/app/src/main/res/layout/activity_add_public_peer.xml index a549cc28..c87d11e6 100644 --- a/app/src/main/res/layout/activity_add_public_peer.xml +++ b/app/src/main/res/layout/activity_add_public_peer.xml @@ -64,9 +64,9 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" - android:text="@string/peer_certificate_added" android:textColor="?colorPrimary" android:visibility="gone" + tools:text="file.der" tools:visibility="visible" /> #666666 #27302B #F44336 + #8027302B @color/white diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2480865d..254ef8be 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -30,7 +30,10 @@ Type an address Identity Certificate * Upload certificate - Certificate picked + Certificate picked Error saving new peer + Invalid Address + Missing Identity Certificate + Invalid Identity Certificate From 311e1dc17d61e1e49076be8664c0f5810a873bf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Santos?= Date: Mon, 5 Apr 2021 11:46:48 +0100 Subject: [PATCH 3/3] Fix lint --- app/src/main/java/tech/relaycorp/ping/data/ReadFile.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/tech/relaycorp/ping/data/ReadFile.kt b/app/src/main/java/tech/relaycorp/ping/data/ReadFile.kt index 3abbe0ab..d26917ba 100644 --- a/app/src/main/java/tech/relaycorp/ping/data/ReadFile.kt +++ b/app/src/main/java/tech/relaycorp/ping/data/ReadFile.kt @@ -21,10 +21,14 @@ class ReadFile .query(uri, null, null, null, null) ?.use { cursor -> if (cursor.moveToFirst()) { - return cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)) + val index = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) + if (index != -1) { + return cursor.getString(index) + } } } } + return uri.path?.split("/")?.lastOrNull() ?: "" } }