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