Commit cf8ce165 authored by Waweic's avatar Waweic 💬
Browse files

Initial commit

parents
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
Gemini Client
\ No newline at end of file
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<JetCodeStyleSettings>
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</JetCodeStyleSettings>
<codeStyleSettings language="XML">
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
</indentOptions>
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>ANDROID_ATTRIBUTE_ORDER</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
<codeStyleSettings language="kotlin">
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
</codeStyleSettings>
</code_scheme>
</component>
\ No newline at end of file
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="testRunner" value="PLATFORM" />
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
<option name="resolveModulePerSourceSet" value="false" />
</GradleProjectSettings>
</option>
</component>
</project>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_7" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" />
</set>
</option>
</component>
</project>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>
\ No newline at end of file
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
android {
compileSdkVersion 29
buildToolsVersion "29.0.3"
defaultConfig {
applicationId "rocks.ism.decentral.geminiclient"
minSdkVersion 16
targetSdkVersion 29
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1'
implementation 'androidx.appcompat:appcompat:1.0.2'
implementation 'androidx.core:core-ktx:1.0.2'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
}
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
package rocks.ism.decentral.geminiclient
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("rocks.ism.decentral.geminiclient", appContext.packageName)
}
}
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="rocks.ism.decentral.geminiclient">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="gemini" />
</intent-filter>
</activity>
</application>
</manifest>
\ No newline at end of file
package rocks.ism.decentral.geminiclient
import android.graphics.Bitmap
import android.os.AsyncTask
import java.io.*
import java.net.URI
import javax.net.ssl.SSLSocket
import javax.net.ssl.SSLSocketFactory
class GeminiConnection {
var currentURI : URI? = null
private set
suspend fun request(uri: URI): GeminiResult {
currentURI = uri
if(uri.scheme != "gemini") {
return GeminiResult(this, GeminiConnectionState.FAILED_WRONG_URI_SCHEME)
}
var port = 1965
if(uri.port != -1) {
port = uri.port
}
val factory: SSLSocketFactory = SSLSocketFactory.getDefault() as SSLSocketFactory
val socket: SSLSocket = factory.createSocket(uri.host, port) as SSLSocket
socket.enabledProtocols = arrayOf("TLSv1.2")
socket.startHandshake()
val outWriter = PrintWriter(
BufferedWriter(
OutputStreamWriter(
socket.outputStream
)
)
)
outWriter.print(uri.toString() + "\r\n") //TODO: See if I can set <CR><LF> globally
outWriter.flush()
if (outWriter.checkError()) {
return GeminiResult(this, GeminiConnectionState.FAILED_PRINTER_WRITER)
}
var inReader = BufferedReader(
InputStreamReader(
socket.inputStream
)
)
var responseHeader: String = inReader.readLine()
var responseBody: String = inReader.readText()
inReader.close()
outWriter.close()
socket.close()
return GeminiResult(this, GeminiConnectionState.SUCCESS, responseHeader, responseBody)
}
}
enum class GeminiConnectionState {
FAILED,
FAILED_WRONG_URI_SCHEME,
FAILED_PRINTER_WRITER,
SUCCESS
}
class GeminiResult(val connection: GeminiConnection, val connectionState: GeminiConnectionState) {
var header: String = ""
private set
var statusCode: Int = 0
private set
var meta: String = ""
private set
var body: String = ""
private set
constructor (connection: GeminiConnection, connectionState: GeminiConnectionState, header: String) : this(connection, connectionState) {
this.header = header
val headerParts = header.split("\\s+".toRegex(), 2)
print(headerParts)
this.statusCode = headerParts[0].toInt() //TODO: Check if not an int
this.meta = headerParts[1]
}
constructor (connection: GeminiConnection, connectionState: GeminiConnectionState, header: String, body: String) : this(connection, connectionState, header) {
this.body = body
}
}
\ No newline at end of file
package rocks.ism.decentral.geminiclient
import android.text.Html
import android.text.SpannableString
import android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
import android.text.method.LinkMovementMethod
import android.text.style.BulletSpan
import android.text.style.URLSpan
import android.widget.TextView
import android.widget.Toast
import kotlinx.coroutines.*
import java.io.BufferedReader
import java.io.StringReader
class GeminiRenderer (val textView: TextView) {
var preRenderMode = false
fun render(connection: GeminiConnection, resultBody: String, mimetype: String) {
if(mimetype == "text/gemini") {
renderGemini(resultBody, connection)
}
else {
renderPlaintext(resultBody)
}
}
fun renderGemini(resultBody: String, connection: GeminiConnection) {
val bodyReader = BufferedReader(StringReader(resultBody))
preRenderMode = false
textView.text = ""
textView.movementMethod = LinkMovementMethod.getInstance()
var line: String? = null
while ({ line = bodyReader.readLine(); line }() != null) {
line?.let {
renderGeminiLine(it, connection)
}
}
}
fun renderGeminiLine (line: String, connection: GeminiConnection) {
if(line.startsWith("=>")) {
val linkParts = line.substring(2).trim().split("\\s+".toRegex(), 2)
val linkUrl = connection.currentURI?.resolve(linkParts[0]).toString()
var linkName = linkParts[0]
if(linkParts.size > 1) {
linkName = linkParts[1]
}
val spannable = SpannableString(linkName)
spannable.setSpan(URLSpan(linkUrl), 0, spannable.length, SPAN_EXCLUSIVE_EXCLUSIVE)
textView.append(spannable)
} else if(line.startsWith("*")) {
val spannable = SpannableString(line.substring(1))
spannable.setSpan(BulletSpan(), 0, spannable.length, SPAN_EXCLUSIVE_EXCLUSIVE)
textView.append(spannable)
}
else {
textView.append(line)
}
textView.append("\n")
}
fun renderPlaintext(resultBody: String) {
textView.text = resultBody
}
}
\ No newline at end of file
package rocks.ism.decentral.geminiclient
import android.content.Context
import android.content.Intent
import android.net.Uri
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class GeminiResponseHandler (val renderer: GeminiRenderer, private val context: Context) {
fun handle (deferredResult: Deferred<GeminiResult>) {
CoroutineScope(Dispatchers.Main).launch {
val result = deferredResult.await()
if(result.header.startsWith("3")) {
//TODO: Check stuff (like how many redirects were before this)
val newUriString = result.connection.currentURI?.resolve(result.meta).toString()
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(newUriString), context, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(intent)
}
else if(result.header.startsWith("2")){
if(result.meta == "text/gemini") {
renderer.render(result.connection, result.body, "text/gemini")
} else if (result.meta.startsWith("text/")) {
renderer.render(result.connection, result.body,"text/plain")
}
}
else {
renderer.render(result.connection, result.header,"text/plain")
}
}
}
}
\ No newline at end of file
package rocks.ism.decentral.geminiclient
import android.content.Intent
import android.os.Bundle
import android.text.TextUtils
import android.view.View
import android.widget.Button
import android.widget.EditText
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import java.net.URI
class MainActivity : AppCompatActivity() {
private lateinit var connection : GeminiConnection
private lateinit var renderer : GeminiRenderer
private lateinit var responseHandler : GeminiResponseHandler
private lateinit var backButton: Button
private lateinit var goButton: Button
private lateinit var uriField: EditText
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Find UI elements
backButton = findViewById(R.id.back_button)
goButton = findViewById(R.id.go_button)
uriField = findViewById(R.id.gemini_uri)
// Initialize Protocol objects
connection = GeminiConnection()
renderer = GeminiRenderer(findViewById(R.id.mainContentView))
responseHandler = GeminiResponseHandler(renderer, applicationContext)
// Create handlers
goButton.setOnClickListener {
goToPage(URI.create(uriField.text.toString()))
}
if(intent.data != null) {
intent.data.toString().let {goToPage(URI.create(it))}
}
else {
goToPage(URI.create(getString(R.string.default_url)))
}
}
fun goToPage(uri: URI) {
uriField.setText(uri.toString())
val deferredResult = GlobalScope.async { connection.request(uri) }
responseHandler.handle(deferredResult)
}
}
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"