diff --git a/app/build.gradle b/app/build.gradle index f154a3a..ff19ac1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -46,7 +46,7 @@ dependencies { implementation 'androidx.activity:activity-ktx:1.1.0' implementation "androidx.recyclerview:recyclerview:1.1.0" implementation 'androidx.coordinatorlayout:coordinatorlayout:1.1.0' - + implementation 'androidx.preference:preference:1.1.1' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9' @@ -60,8 +60,11 @@ dependencies { kapt "androidx.room:room-compiler:$room_version" implementation "androidx.room:room-ktx:$room_version" - testImplementation 'junit:junit:4.13' + testImplementation 'junit:junit:4.13.1' androidTestImplementation 'androidx.test.ext:junit:1.1.2' androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' + androidTestImplementation 'androidx.test:runner:1.3.0' + androidTestImplementation 'androidx.test:rules:1.3.0' + androidTestImplementation 'com.google.truth:truth:1.0' } \ No newline at end of file diff --git a/app/src/androidTest/java/oppen/ariane/io/gemini/DeviceTLSTests.kt b/app/src/androidTest/java/oppen/ariane/io/gemini/DeviceTLSTests.kt new file mode 100644 index 0000000..3de1ad9 --- /dev/null +++ b/app/src/androidTest/java/oppen/ariane/io/gemini/DeviceTLSTests.kt @@ -0,0 +1,44 @@ +package oppen.ariane.io.gemini + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import java.security.SecureRandom +import javax.net.ssl.SSLContext +import javax.net.ssl.SSLSocket +import javax.net.ssl.SSLSocketFactory + +@RunWith(AndroidJUnit4::class) +class DeviceTLSTests { + + lateinit var socket: SSLSocket + + @Before + fun setupSocket(){ + val sslContext = SSLContext.getInstance("TLS") + sslContext.init(null, null, SecureRandom()) + val factory: SSLSocketFactory = sslContext.socketFactory + socket = factory.createSocket() as SSLSocket + } + + @Test + fun supportsTLSv1(){ + socket.supportedProtocols.contains("TLSv1") + } + + @Test + fun supportsTLSv1_1(){ + socket.supportedProtocols.contains("TLSv1.1") + } + + @Test + fun supportsTLSv1_2(){ + socket.supportedProtocols.contains("TLSv1.2") + } + + @Test + fun supportsTLSv1_3(){ + socket.supportedProtocols.contains("TLSv1.3") + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/oppen/ariane/io/gemini/GeminiDatasourceTests.kt b/app/src/androidTest/java/oppen/ariane/io/gemini/GeminiDatasourceTests.kt new file mode 100644 index 0000000..f1aae7d --- /dev/null +++ b/app/src/androidTest/java/oppen/ariane/io/gemini/GeminiDatasourceTests.kt @@ -0,0 +1,86 @@ +package oppen.ariane.io.gemini + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import oppen.ariane.Ariane +import oppen.toURI +import com.google.common.truth.Truth.assertThat +import oppen.ariane.io.GemState +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class GeminiDatasourceTests { + + private lateinit var gemini: Datasource + private val capsules = listOf( + "gemini://gemini.circumlunar.space", + "gemini://rawtext.club", + "gemini://drewdevault.com", + "gemini://talon.computer", + "gemini://tilde.team", + "gemini://tilde.pink", + "gemini://gemini.conman.org", + "gemini://idiomdrottning.org" + ) + + private val capsuleIndex = 3 + + private fun setTLSProtocol(protocol: String){ + gemini = Datasource.factory(InstrumentationRegistry.getInstrumentation().targetContext, protocol) + } + + @Test + fun arianeHomePageTest(){ + setTLSProtocol("TLSv1") + var hasRequested = false + var hasResponded = false + + gemini.request(Ariane.DEFAULT_HOME_CAPSULE.toURI()){ state -> + + when(state){ + is GemState.Requesting -> { + assertThat(state.uri.toString()).isEqualTo(Ariane.DEFAULT_HOME_CAPSULE) + hasRequested = true + } + is GemState.ResponseGemtext -> { + assertThat(state.uri.toString()).isEqualTo(Ariane.DEFAULT_HOME_CAPSULE) + hasResponded = true + } + else -> { + //This will cause a failed test if request fails + assertThat(hasRequested).isTrue() + assertThat(hasResponded).isTrue() + } + } + } + } + + @Test + fun aCapsuleTest(){ + setTLSProtocol("TLSv1.3") + var hasRequested = false + var hasResponded = false + + + + gemini.request(capsules[capsuleIndex].toURI()){ state -> + + when(state){ + is GemState.Requesting -> { + assertThat(state.uri.toString()).isEqualTo(capsules[capsuleIndex]) + hasRequested = true + } + is GemState.ResponseGemtext -> { + assertThat(state.uri.toString()).isEqualTo(capsules[capsuleIndex]) + hasResponded = true + } + else -> { + //This will cause a failed test if request fails + assertThat(hasRequested).isTrue() + assertThat(hasResponded).isTrue() + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9978b79..3c039f6 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -34,6 +34,8 @@ + + \ No newline at end of file diff --git a/app/src/main/java/oppen/Extensions.kt b/app/src/main/java/oppen/Extensions.kt index 24b5945..8fce839 100644 --- a/app/src/main/java/oppen/Extensions.kt +++ b/app/src/main/java/oppen/Extensions.kt @@ -5,6 +5,7 @@ import android.os.CountDownTimer import android.view.View import android.view.inputmethod.InputMethodManager import androidx.core.content.ContextCompat.getSystemService +import java.net.URI fun View.visible(visible: Boolean) = when { @@ -22,6 +23,10 @@ fun View.hideKeyboard(){ imm?.hideSoftInputFromWindow(windowToken, 0) } +fun String.toURI(): URI { + return URI.create(this) +} + fun delay(ms: Long, action: () -> Unit){ object : CountDownTimer(ms, ms/2) { override fun onTick(millisUntilFinished: Long) {} diff --git a/app/src/main/java/oppen/ariane/io/gemini/Datasource.kt b/app/src/main/java/oppen/ariane/io/gemini/Datasource.kt index 12a39cd..74f67e5 100644 --- a/app/src/main/java/oppen/ariane/io/gemini/Datasource.kt +++ b/app/src/main/java/oppen/ariane/io/gemini/Datasource.kt @@ -8,8 +8,8 @@ interface Datasource { fun request(uri: URI, onUpdate: (state: GemState) -> Unit) companion object{ - fun factory(context: Context): Datasource { - return GeminiDatasource(context) + fun factory(context: Context, protocol: String): Datasource { + return GeminiDatasource(context, protocol) } } } \ No newline at end of file diff --git a/app/src/main/java/oppen/ariane/io/gemini/GeminiDatasource.kt b/app/src/main/java/oppen/ariane/io/gemini/GeminiDatasource.kt index f56473f..4839951 100644 --- a/app/src/main/java/oppen/ariane/io/gemini/GeminiDatasource.kt +++ b/app/src/main/java/oppen/ariane/io/gemini/GeminiDatasource.kt @@ -14,7 +14,15 @@ import javax.net.ssl.* const val GEMINI_SCHEME = "gemini" -class GeminiDatasource(val context: Context): Datasource { +/** + * + * + * @param protocol see: https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#SSLContext + * + */ +class GeminiDatasource( + private val context: Context, + private val protocol: String): Datasource { private var last: URI? = null override fun request(uri: URI, onUpdate: (state: GemState) -> Unit) { @@ -116,28 +124,15 @@ class GeminiDatasource(val context: Context): Datasource { last = uri val port = if(uri.port == -1) 1965 else uri.port - val sslContext = SSLContext.getInstance("TLS") + val sslContext = SSLContext.getInstance(protocol) sslContext.init(null, trustAllCerts, SecureRandom()) val factory: SSLSocketFactory = sslContext.socketFactory - val allCipher = factory.supportedCipherSuites - - allCipher.forEach { suite -> - println("Supported cipher suite: $suite") - } - val socket: SSLSocket? try { socket = factory.createSocket(uri.host, port) as SSLSocket - - socket.supportedProtocols.forEach { protocol -> - println("Supported protocol $protocol") - } - - socket.enabledCipherSuites = allCipher - - //socket.enabledProtocols = socket.supportedProtocols + socket.enabledCipherSuites = factory.supportedCipherSuites socket.enabledProtocols = socket.supportedProtocols socket.startHandshake() }catch(ce: ConnectException){ diff --git a/app/src/main/java/oppen/ariane/ui/GemActivity.kt b/app/src/main/java/oppen/ariane/ui/GemActivity.kt index 3b2dd16..77a9848 100644 --- a/app/src/main/java/oppen/ariane/ui/GemActivity.kt +++ b/app/src/main/java/oppen/ariane/ui/GemActivity.kt @@ -34,6 +34,7 @@ import oppen.ariane.ui.modals_menus.history.HistoryDialog import oppen.ariane.ui.modals_menus.input.InputDialog import oppen.ariane.ui.modals_menus.overflow.OverflowPopup import oppen.ariane.ui.modals_menus.set_home.SetHomeDialog +import oppen.ariane.ui.settings.SettingsActivity import oppen.hideKeyboard import oppen.visibleRetainingSpace import java.io.File @@ -87,7 +88,6 @@ class GemActivity : AppCompatActivity() { bookmarkDatasource = BookmarksDatasource.getDefault(applicationContext) - binding = DataBindingUtil.setContentView(this, R.layout.activity_gem) binding.viewmodel = model binding.lifecycleOwner = this @@ -97,9 +97,13 @@ class GemActivity : AppCompatActivity() { history = HistoryInterface.default(this) + val prefs = getSharedPreferences("oppen.tva.ui.dialogs.set_home", Context.MODE_PRIVATE) + val home = prefs.getString("home", Ariane.DEFAULT_HOME_CAPSULE) + model.initialise( - Datasource.factory(this), - BookmarksDatasource.getDefault(applicationContext) + home = home ?: Ariane.DEFAULT_HOME_CAPSULE, + gemini = Datasource.factory(this, "TLSv1.2"), + bookmarks = BookmarksDatasource.getDefault(applicationContext) ){ state -> binding.pullToRefresh.isRefreshing = false @@ -206,6 +210,9 @@ class GemActivity : AppCompatActivity() { showAlert("Home capsule updated") } } + R.id.overflow_menu_settings -> { + startActivity(Intent(this, SettingsActivity::class.java)) + } } } } diff --git a/app/src/main/java/oppen/ariane/ui/GemViewModel.kt b/app/src/main/java/oppen/ariane/ui/GemViewModel.kt index 46fdad0..2c08bfd 100644 --- a/app/src/main/java/oppen/ariane/ui/GemViewModel.kt +++ b/app/src/main/java/oppen/ariane/ui/GemViewModel.kt @@ -15,12 +15,12 @@ class GemViewModel: ViewModel() { private val history = mutableListOf() - fun initialise(gemini: Datasource, bookmarks: BookmarksDatasource, onState: (state: GemState) -> Unit){ + fun initialise(home: String, gemini: Datasource, bookmarks: BookmarksDatasource, onState: (state: GemState) -> Unit){ this.gemini = gemini this.bookmarks = bookmarks this.onState = onState - request(URI.create(Ariane.DEFAULT_HOME_CAPSULE))//todo - regression: should check prefs... + request(home) } fun request(address: String) { diff --git a/app/src/main/java/oppen/ariane/ui/settings/SettingsActivity.kt b/app/src/main/java/oppen/ariane/ui/settings/SettingsActivity.kt new file mode 100644 index 0000000..f49065c --- /dev/null +++ b/app/src/main/java/oppen/ariane/ui/settings/SettingsActivity.kt @@ -0,0 +1,14 @@ +package oppen.ariane.ui.settings + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import oppen.ariane.R + +class SettingsActivity: AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContentView(R.layout.activity_settings) + supportFragmentManager.beginTransaction().replace(R.id.settings_container, SettingsFragment()).commit() + } +} diff --git a/app/src/main/java/oppen/ariane/ui/settings/SettingsFragment.kt b/app/src/main/java/oppen/ariane/ui/settings/SettingsFragment.kt new file mode 100644 index 0000000..23b35bd --- /dev/null +++ b/app/src/main/java/oppen/ariane/ui/settings/SettingsFragment.kt @@ -0,0 +1,59 @@ +package oppen.ariane.ui.settings + +import android.os.Bundle +import androidx.preference.Preference +import androidx.preference.PreferenceCategory +import androidx.preference.PreferenceFragmentCompat +import androidx.preference.SwitchPreferenceCompat +import java.security.SecureRandom +import java.util.* +import javax.net.ssl.SSLContext +import javax.net.ssl.SSLSocket +import javax.net.ssl.SSLSocketFactory + +class SettingsFragment: PreferenceFragmentCompat(), Preference.OnPreferenceChangeListener { + + lateinit var protocols: Array + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + val context = preferenceManager.context + val screen = preferenceManager.createPreferenceScreen(context) + + val tlsCategory = PreferenceCategory(context) + tlsCategory.key = "tls_category" + tlsCategory.title = "TLS Config" + screen.addPreference(tlsCategory) + + val sslContext = SSLContext.getInstance("TLS") + sslContext.init(null, null, SecureRandom()) + val factory: SSLSocketFactory = sslContext.socketFactory + val socket = factory.createSocket() as SSLSocket + protocols = socket.supportedProtocols + protocols.forEach { protocol -> + val tlsPreference = SwitchPreferenceCompat(context) + tlsPreference.key = "tls_${protocol.toLowerCase(Locale.getDefault())}" + tlsPreference.title = protocol + tlsPreference.onPreferenceChangeListener = this + tlsCategory.addPreference(tlsPreference) + } + + preferenceScreen = screen + } + + override fun onPreferenceChange(preference: Preference?, newValue: Any?): Boolean { + + preference?.key?.let{ key -> + if(key.startsWith("tls_")){ + protocols.forEach {protocol -> + val tlsSwitchKey = "tls_${protocol.toLowerCase(Locale.getDefault())}" + if(tlsSwitchKey != key){ + val otherTLSSwitch = preferenceScreen.findPreference(tlsSwitchKey) + otherTLSSwitch?.isChecked = false + } + } + } + } + + return true + } +} diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_settings.xml new file mode 100644 index 0000000..6dbc60f --- /dev/null +++ b/app/src/main/res/layout/activity_settings.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/overflow_menu.xml b/app/src/main/res/menu/overflow_menu.xml index 9f3c4bf..60501fe 100644 --- a/app/src/main/res/menu/overflow_menu.xml +++ b/app/src/main/res/menu/overflow_menu.xml @@ -33,6 +33,10 @@ android:id="@+id/overflow_menu_set_home" android:title="@string/set_home" android:icon="@drawable/vector_set_home"/> + Gemini address Share Set Home + Settings Home icon by Icongeek26 on FlatIcon.com Ariane: Gemini protocol client from Öppenlab GPL v3