started android / compose app (#301)

* new compose project

* classes for chat command and response

* use val with get() for commands and responses

* chat model

* initial jetpack compose set up

* wire it up with chat

* first ability to send and receive messages

* refactor model/controller interface

* JSON samples

* terminal view with items

* playing around with json

* JSON serialization works

* parsing API responses in the terminal

* add subclass for contactSubscribed reponse

* remove android-poc

* remove JSON example

Co-authored-by: IanRDavies <ian_davies_@hotmail.co.uk>
This commit is contained in:
Evgeny Poberezkin 2022-02-16 12:49:47 +00:00 committed by GitHub
parent 322ab9d854
commit ce02c514cf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 665 additions and 352 deletions

View file

@ -8,17 +8,8 @@
/.idea/navEditor.xml /.idea/navEditor.xml
/.idea/assetWizardSettings.xml /.idea/assetWizardSettings.xml
.DS_Store .DS_Store
build/ /build
release/
debug/
/captures /captures
.externalNativeBuild .externalNativeBuild
.cxx .cxx
local.properties local.properties
*.jar
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar

3
apps/android/.idea/.gitignore generated vendored
View file

@ -1,3 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml

View file

@ -1,117 +0,0 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<codeStyleSettings language="XML">
<option name="FORCE_REARRANGE_MODE" value="1" />
<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>
</code_scheme>
</component>

View file

@ -1,5 +0,0 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
</state>
</component>

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetDropDown">
<runningDeviceTargetSelectedWithDropDown>
<Target>
<type value="RUNNING_DEVICE_TARGET" />
<deviceKey>
<Key>
<type value="VIRTUAL_DEVICE_PATH" />
<value value="$USER_HOME$/.android/avd/Pixel_4a_API_32.avd" />
</Key>
</deviceKey>
</Target>
</targetSelectedWithDropDown>
<timeTargetWasSelectedWithDropDown value="2022-02-15T15:32:14.669079Z" />
</component>
</project>

View file

@ -0,0 +1,20 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="previewFile" value="true" />
</inspection_tool>
</profile>
</component>

View file

@ -3,9 +3,10 @@
<component name="DesignSurface"> <component name="DesignSurface">
<option name="filePathToZoomLevelMap"> <option name="filePathToZoomLevelMap">
<map> <map>
<entry key="app/src/main/res/drawable-v24/ic_launcher_foreground.xml" value="0.2328125" /> <entry key="../../../../../../../layout/compose-model-1644940819446.xml" value="0.4484797297297297" />
<entry key="app/src/main/res/drawable/ic_launcher_background.xml" value="0.2328125" /> <entry key="../../../../../../../layout/compose-model-1644941851914.xml" value="0.28378378378378377" />
<entry key="app/src/main/res/layout/activity_main.xml" value="1.0" /> <entry key="../../../../../../../layout/compose-model-1644956742665.xml" value="1.0" />
<entry key="../../../../../../../layout/compose-model-1644963789622.xml" value="0.8420454545454545" />
</map> </map>
</option> </option>
</component> </component>

1
apps/android/app/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -1,6 +1,7 @@
plugins { plugins {
id 'com.android.application' id 'com.android.application'
id 'kotlin-android' id 'org.jetbrains.kotlin.android'
id 'org.jetbrains.kotlin.plugin.serialization'
} }
android { android {
@ -17,6 +18,9 @@ android {
ndk { ndk {
abiFilters 'arm64-v8a' abiFilters 'arm64-v8a'
} }
vectorDrawables {
useSupportLibrary true
}
externalNativeBuild { externalNativeBuild {
cmake { cmake {
cppFlags '' cppFlags ''
@ -44,17 +48,29 @@ android {
} }
} }
buildFeatures { buildFeatures {
viewBinding true compose true
}
composeOptions {
kotlinCompilerExtensionVersion compose_version
}
packagingOptions {
resources {
excludes += '/META-INF/{AL2.0,LGPL2.1}'
}
} }
} }
dependencies { dependencies {
implementation 'androidx.core:core-ktx:1.7.0' implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.appcompat:appcompat:1.4.1' implementation "androidx.compose.ui:ui:$compose_version"
implementation 'com.google.android.material:material:1.5.0' implementation "androidx.compose.material:material:$compose_version"
implementation 'androidx.constraintlayout:constraintlayout:2.1.3' implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
testImplementation 'junit:junit:4.+' implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
implementation 'androidx.activity:activity-compose:1.3.1'
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
} androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version"
debugImplementation "androidx.compose.ui:ui-tooling:$compose_version"
}

View file

@ -7,6 +7,7 @@
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application <application
android:name="SimplexApp"
android:allowBackup="true" android:allowBackup="true"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
@ -15,7 +16,9 @@
android:theme="@style/Theme.SimpleX"> android:theme="@style/Theme.SimpleX">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true"> android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.SimpleX">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />

View file

@ -8,7 +8,7 @@ void setLineBuffering(void);
int pipe_std_to_socket(const char * name); int pipe_std_to_socket(const char * name);
JNIEXPORT jint JNICALL JNIEXPORT jint JNICALL
Java_chat_simplex_app_MainActivityKt_pipeStdOutToSocket(JNIEnv *env, __unused jclass clazz, jstring socket_name) { Java_chat_simplex_app_SimplexAppKt_pipeStdOutToSocket(JNIEnv *env, __unused jclass clazz, jstring socket_name) {
const char *name = (*env)->GetStringUTFChars(env, socket_name, JNI_FALSE); const char *name = (*env)->GetStringUTFChars(env, socket_name, JNI_FALSE);
int ret = pipe_std_to_socket(name); int ret = pipe_std_to_socket(name);
(*env)->ReleaseStringUTFChars(env, socket_name, name); (*env)->ReleaseStringUTFChars(env, socket_name, name);
@ -16,7 +16,7 @@ Java_chat_simplex_app_MainActivityKt_pipeStdOutToSocket(JNIEnv *env, __unused jc
} }
JNIEXPORT void JNICALL JNIEXPORT void JNICALL
Java_chat_simplex_app_MainActivityKt_initHS(__unused JNIEnv *env, __unused jclass clazz) { Java_chat_simplex_app_SimplexAppKt_initHS(__unused JNIEnv *env, __unused jclass clazz) {
hs_init(NULL, NULL); hs_init(NULL, NULL);
setLineBuffering(); setLineBuffering();
} }
@ -33,7 +33,7 @@ extern char *chat_send_cmd(controller ctl, const char *cmd);
extern char *chat_recv_msg(controller ctl); extern char *chat_recv_msg(controller ctl);
JNIEXPORT jlong JNICALL JNIEXPORT jlong JNICALL
Java_chat_simplex_app_MainActivityKt_chatInit(JNIEnv *env, __unused jclass clazz, jstring datadir) { Java_chat_simplex_app_SimplexAppKt_chatInit(JNIEnv *env, __unused jclass clazz, jstring datadir) {
const char *_data = (*env)->GetStringUTFChars(env, datadir, JNI_FALSE); const char *_data = (*env)->GetStringUTFChars(env, datadir, JNI_FALSE);
jlong res = (jlong)chat_init_store(_data); jlong res = (jlong)chat_init_store(_data);
(*env)->ReleaseStringUTFChars(env, datadir, _data); (*env)->ReleaseStringUTFChars(env, datadir, _data);
@ -41,12 +41,12 @@ Java_chat_simplex_app_MainActivityKt_chatInit(JNIEnv *env, __unused jclass clazz
} }
JNIEXPORT jstring JNICALL JNIEXPORT jstring JNICALL
Java_chat_simplex_app_MainActivityKt_chatGetUser(JNIEnv *env, __unused jclass clazz, jlong controller) { Java_chat_simplex_app_SimplexAppKt_chatGetUser(JNIEnv *env, __unused jclass clazz, jlong controller) {
return (*env)->NewStringUTF(env, chat_get_user((void*)controller)); return (*env)->NewStringUTF(env, chat_get_user((void*)controller));
} }
JNIEXPORT jstring JNICALL JNIEXPORT jstring JNICALL
Java_chat_simplex_app_MainActivityKt_chatCreateUser(JNIEnv *env, __unused jclass clazz, jlong controller, jstring data) { Java_chat_simplex_app_SimplexAppKt_chatCreateUser(JNIEnv *env, __unused jclass clazz, jlong controller, jstring data) {
const char *_data = (*env)->GetStringUTFChars(env, data, JNI_FALSE); const char *_data = (*env)->GetStringUTFChars(env, data, JNI_FALSE);
jstring res = (*env)->NewStringUTF(env, chat_create_user((void*)controller, _data)); jstring res = (*env)->NewStringUTF(env, chat_create_user((void*)controller, _data));
(*env)->ReleaseStringUTFChars(env, data, _data); (*env)->ReleaseStringUTFChars(env, data, _data);
@ -54,12 +54,12 @@ Java_chat_simplex_app_MainActivityKt_chatCreateUser(JNIEnv *env, __unused jclass
} }
JNIEXPORT jlong JNICALL JNIEXPORT jlong JNICALL
Java_chat_simplex_app_MainActivityKt_chatStart(JNIEnv *env, jclass clazz, jlong controller) { Java_chat_simplex_app_SimplexAppKt_chatStart(JNIEnv *env, jclass clazz, jlong controller) {
return (jlong)chat_start((void*)controller); return (jlong)chat_start((void*)controller);
} }
JNIEXPORT jstring JNICALL JNIEXPORT jstring JNICALL
Java_chat_simplex_app_MainActivityKt_chatSendCmd(JNIEnv *env, __unused jclass clazz, jlong controller, jstring msg) { Java_chat_simplex_app_SimplexAppKt_chatSendCmd(JNIEnv *env, __unused jclass clazz, jlong controller, jstring msg) {
const char *_msg = (*env)->GetStringUTFChars(env, msg, JNI_FALSE); const char *_msg = (*env)->GetStringUTFChars(env, msg, JNI_FALSE);
jstring res = (*env)->NewStringUTF(env, chat_send_cmd((void*)controller, _msg)); jstring res = (*env)->NewStringUTF(env, chat_send_cmd((void*)controller, _msg));
(*env)->ReleaseStringUTFChars(env, msg, _msg); (*env)->ReleaseStringUTFChars(env, msg, _msg);
@ -67,6 +67,6 @@ Java_chat_simplex_app_MainActivityKt_chatSendCmd(JNIEnv *env, __unused jclass cl
} }
JNIEXPORT jstring JNICALL JNIEXPORT jstring JNICALL
Java_chat_simplex_app_MainActivityKt_chatRecvMsg(JNIEnv *env, __unused jclass clazz, jlong controller) { Java_chat_simplex_app_SimplexAppKt_chatRecvMsg(JNIEnv *env, __unused jclass clazz, jlong controller) {
return (*env)->NewStringUTF(env, chat_recv_msg((void*)controller)); return (*env)->NewStringUTF(env, chat_recv_msg((void*)controller));
} }

View file

@ -1,123 +1,37 @@
package chat.simplex.app package chat.simplex.app
import android.net.LocalServerSocket import android.app.Application
import android.os.Bundle import android.os.Bundle
import android.util.Log import androidx.activity.ComponentActivity
import android.view.inputmethod.EditorInfo import androidx.activity.compose.setContent
import android.widget.ScrollView import androidx.activity.viewModels
import android.widget.TextView import androidx.compose.runtime.Composable
import androidx.appcompat.app.AppCompatActivity import chat.simplex.app.ui.theme.SimpleXTheme
import androidx.appcompat.widget.AppCompatEditText import androidx.lifecycle.AndroidViewModel
import java.io.BufferedReader import chat.simplex.app.model.*
import java.io.InputStreamReader import chat.simplex.app.views.TerminalView
import java.lang.ref.WeakReference import kotlinx.serialization.*
import java.util.* import kotlinx.serialization.json.*
import java.util.concurrent.Semaphore import kotlinx.serialization.modules.*
import kotlin.concurrent.thread
// ghc's rts
external fun initHS()
// android-support
external fun pipeStdOutToSocket(socketName: String) : Int
// simplex-chat class MainActivity: ComponentActivity() {
typealias Store = Long private val viewModel by viewModels<SimplexViewModel>()
typealias Controller = Long
external fun chatInit(filesDir: String): Store
external fun chatGetUser(controller: Store) : String
external fun chatCreateUser(controller: Store, data: String) : String
external fun chatStart(controller: Store) : Controller
external fun chatSendCmd(controller: Controller, msg: String) : String
external fun chatRecvMsg(controller: Controller) : String
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
weakActivity = WeakReference(this)
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main) setContent {
SimpleXTheme {
val store : Store = chatInit(this.applicationContext.filesDir.toString()) MainPage(viewModel)
// create user if needed
if(chatGetUser(store) == "{}") {
chatCreateUser(store, """
{"displayName": "test", "fullName": "android test"}
""".trimIndent())
}
Log.d("SIMPLEX (user)", chatGetUser(store))
val controller = chatStart(store)
val cmdinput = this.findViewById<AppCompatEditText>(R.id.cmdInput)
cmdinput.setOnEditorActionListener { _, actionId, _ ->
when (actionId) {
EditorInfo.IME_ACTION_SEND -> {
Log.d("SIMPLEX SEND", chatSendCmd(controller, cmdinput.text.toString()))
cmdinput.text?.clear()
true
}
else -> false
} }
} }
thread(name="receiver") {
val chatlog = FifoQueue<String>(500)
while(true) {
val msg = chatRecvMsg(controller)
Log.d("SIMPLEX RECV", msg)
chatlog.add(msg)
val currentText = chatlog.joinToString("\n")
weakActivity.get()?.runOnUiThread {
val log = weakActivity.get()?.findViewById<TextView>(R.id.chatlog)
val scroll = weakActivity.get()?.findViewById<ScrollView>(R.id.scroller)
log?.text = currentText
scroll?.scrollTo(0, scroll.getChildAt(0).height)
}
}
}
}
companion object {
lateinit var weakActivity : WeakReference<MainActivity>
init {
val socketName = "local.socket.address.listen.native.cmd2"
val s = Semaphore(0)
thread(name="stdout/stderr pipe") {
Log.d("SIMPLEX", "starting server")
val server = LocalServerSocket(socketName)
Log.d("SIMPLEX", "started server")
s.release()
val receiver = server.accept()
Log.d("SIMPLEX", "started receiver")
val logbuffer = FifoQueue<String>(500)
if (receiver != null) {
val inStream = receiver.inputStream
val inStreamReader = InputStreamReader(inStream)
val input = BufferedReader(inStreamReader)
while(true) {
val line = input.readLine() ?: break
Log.d("SIMPLEX (stdout/stderr)", line)
logbuffer.add(line)
}
}
}
System.loadLibrary("app-lib")
s.acquire()
pipeStdOutToSocket(socketName)
initHS()
}
} }
} }
class FifoQueue<E>(private var capacity: Int) : LinkedList<E>() { class SimplexViewModel(application: Application) : AndroidViewModel(application) {
override fun add(element: E): Boolean { val chatModel = getApplication<SimplexApp>().chatModel
if(size > capacity) removeFirst() }
return super.add(element)
} @Composable
fun MainPage(vm: SimplexViewModel) {
TerminalView(vm.chatModel)
} }

View file

@ -0,0 +1,95 @@
package chat.simplex.app
import android.app.Application
import android.net.LocalServerSocket
import android.util.Log
import chat.simplex.app.model.ChatController
import chat.simplex.app.model.ChatModel
import java.io.BufferedReader
import java.io.InputStreamReader
import java.lang.ref.WeakReference
import java.util.*
import java.util.concurrent.Semaphore
import kotlin.concurrent.thread
// ghc's rts
external fun initHS()
// android-support
external fun pipeStdOutToSocket(socketName: String) : Int
// SimpleX API
typealias Controller = Long
typealias Store = Long
external fun chatInit(filesDir: String): Store
external fun chatGetUser(controller: Store) : String
external fun chatCreateUser(controller: Store, data: String) : String
external fun chatStart(controller: Store) : Controller
external fun chatSendCmd(controller: Controller, msg: String) : String
external fun chatRecvMsg(controller: Controller) : String
class SimplexApp: Application() {
private lateinit var controller: ChatController
override fun onCreate() {
super.onCreate()
val store: Store = chatInit(applicationContext.filesDir.toString())
// create user if needed
if (chatGetUser(store) == "{}") {
chatCreateUser(store, """
{"displayName": "test", "fullName": "android test"}
""".trimIndent())
}
Log.d("SIMPLEX (user)", chatGetUser(store))
controller = ChatController(chatStart(store))
}
val chatModel by lazy {
val m = ChatModel(controller)
controller.setModel(m)
controller.startReceiver()
m
}
companion object {
lateinit var weakActivity: WeakReference<MainActivity>
init {
val socketName = "local.socket.address.listen.native.cmd2"
val s = Semaphore(0)
thread(name="stdout/stderr pipe") {
Log.d("SIMPLEX", "starting server")
val server = LocalServerSocket(socketName)
Log.d("SIMPLEX", "started server")
s.release()
val receiver = server.accept()
Log.d("SIMPLEX", "started receiver")
val logbuffer = FifoQueue<String>(500)
if (receiver != null) {
val inStream = receiver.inputStream
val inStreamReader = InputStreamReader(inStream)
val input = BufferedReader(inStreamReader)
while(true) {
val line = input.readLine() ?: break
Log.d("SIMPLEX (stdout/stderr)", line)
logbuffer.add(line)
}
}
}
System.loadLibrary("app-lib")
s.acquire()
pipeStdOutToSocket(socketName)
initHS()
}
}
}
class FifoQueue<E>(private var capacity: Int) : LinkedList<E>() {
override fun add(element: E): Boolean {
if(size > capacity) removeFirst()
return super.add(element)
}
}

View file

@ -0,0 +1,105 @@
package chat.simplex.app.model
import androidx.compose.runtime.mutableStateListOf
import kotlinx.serialization.Serializable
import java.util.*
class ChatModel(val controller: ChatController) {
val currentUser: User? = null
var terminalItems = mutableStateListOf<TerminalItem>()
companion object {
val sampleData: ChatModel get() {
val m = ChatModel(ChatController.Mock())
m.terminalItems = mutableStateListOf(
TerminalItem.Cmd(CC.ShowActiveUser()),
TerminalItem.Resp(CR.ActiveUser(User.sampleData))
)
return m
}
}
}
enum class ChatType(val type: String) {
Direct("@"),
Group("#"),
ContactRequest("<@")
}
@Serializable
class User (
val userId: Int,
val userContactId: Int,
val localDisplayName: String,
val profile: Profile,
val activeUser: Boolean
) : NamedChat {
override val displayName: String get() = profile.displayName
override val fullName: String get() = profile.fullName
companion object {
val sampleData = User(
userId = 1,
userContactId = 1,
localDisplayName = "alice",
profile = Profile.sampleData,
activeUser = true
)
}
}
typealias ChatId = String
@Serializable
class Contact(
val contactId: Int,
val localDisplayName: String,
val profile: Profile,
val activeConn: Connection,
val viaGroup: Int? = null,
// no serializer for type Date?
// val createdAt: Date
): NamedChat {
val id: ChatId get() = "@$contactId"
val apiId: Int get() = contactId
val ready: Boolean get() = activeConn.connStatus == "ready" || activeConn.connStatus == "snd-ready"
override val displayName: String get() = profile.displayName
override val fullName: String get() = profile.fullName
companion object {
val sampleData = Contact(
contactId = 1,
localDisplayName = "alice",
profile = Profile.sampleData,
activeConn = Connection.sampleData
// createdAt = Date()
)
}
}
@Serializable
class Connection(val connStatus: String) {
companion object {
val sampleData = Connection(connStatus = "ready")
}
}
@Serializable
class Profile(
val displayName: String,
val fullName: String
) {
companion object {
val sampleData = Profile(
displayName = "alice",
fullName = "Alice"
)
}
}
interface NamedChat {
abstract val displayName: String
abstract val fullName: String
val chatViewName: String
get() = displayName + (if (fullName == "" || fullName == displayName) "" else " / $fullName")
}

View file

@ -0,0 +1,161 @@
package chat.simplex.app.model
import android.util.Log
import chat.simplex.app.chatRecvMsg
import chat.simplex.app.chatSendCmd
import kotlinx.serialization.*
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import java.lang.Exception
import java.util.*
import kotlin.concurrent.thread
typealias Controller = Long
open class ChatController(val ctrl: Controller) {
private lateinit var chatModel: ChatModel
fun setModel(m: ChatModel) {
chatModel = m
}
fun startReceiver() {
thread(name="receiver") {
// val chatlog = FifoQueue<String>(500)
while(true) {
val json = chatRecvMsg(ctrl)
Log.d("SIMPLEX chatRecvMsg: ", json)
chatModel.terminalItems.add(TerminalItem.Resp(APIResponse.decodeStr(json)))
}
}
}
fun sendCmd(cmd: String) {
val json = chatSendCmd(ctrl, cmd)
Log.d("SIMPLEX chatSendCmd: ", cmd)
Log.d("SIMPLEX chatSendCmd response: ", json)
chatModel.terminalItems.add(TerminalItem.Resp(APIResponse.decodeStr(json)))
}
class Mock: ChatController(0) {}
}
// ChatCommand
abstract class CC {
abstract val cmdString: String
abstract val cmdType: String
class Console(val cmd: String): CC() {
override val cmdString get() = cmd
override val cmdType get() = "console command"
}
class ShowActiveUser: CC() {
override val cmdString get() = "/u"
override val cmdType get() = "ShowActiveUser"
}
class CreateActiveUser(val profile: Profile): CC() {
override val cmdString get() = "/u ${profile.displayName} ${profile.fullName}"
override val cmdType get() = "CreateActiveUser"
}
class StartChat: CC() {
override val cmdString get() = "/_start"
override val cmdType get() = "StartChat"
}
class ApiGetChats: CC() {
override val cmdString get() = "/_get chats"
override val cmdType get() = "ApiGetChats"
}
companion object {
fun chatRef(type: ChatType, id: String) = "${type}${id}"
}
}
val json = Json {
prettyPrint = true
ignoreUnknownKeys = true
}
@Serializable
class APIResponse(val resp: CR) {
companion object {
fun decodeStr(str: String): CR {
try {
return json.decodeFromString<APIResponse>(str).resp
} catch(e: Exception) {
try {
val data = json.parseToJsonElement(str)
return CR.Response(data.jsonObject["resp"]!!.jsonObject["type"]?.toString() ?: "invalid", json.encodeToString(data))
} catch(e: Exception) {
return CR.Invalid(str)
}
}
}
}
}
// ChatResponse
@Serializable
sealed class CR {
abstract val responseType: String
abstract val details: String
@Serializable
@SerialName("activeUser")
class ActiveUser(val user: User): CR() {
override val responseType get() = "activeUser"
override val details get() = user.toString()
}
@Serializable
@SerialName("contactSubscribed")
class ContactSubscribed(val contact: Contact): CR() {
override val responseType get() = "contactSubscribed"
override val details get() = contact.toString()
}
@Serializable
class Response(val type: String, val json: String): CR() {
override val responseType get() = "* ${type}"
override val details get() = json
}
@Serializable
class Invalid(val str: String): CR() {
override val responseType get() = "* invalid json"
override val details get() = str
}
// {"resp": {"activeUser": {"user": {<user>}}}}
// {"resp": {"anythingElse": <json> }} -> Unknown(type = "anythingElse", json = "<the whole thing including resp>")
// {"type": "activeUser", "user": <user>}
}
abstract class TerminalItem {
val date = Date()
abstract val label: String
abstract val details: String
class Cmd(val cmd: CC): TerminalItem() {
override val label get() = "> ${cmd.cmdString.substring(0, 30)}"
override val details get() = cmd.cmdString
}
class Resp(val resp: CR): TerminalItem() {
override val label get() = "< ${resp.responseType}"
override val details get() = resp.details
}
companion object {
val sampleData = listOf<TerminalItem>(
TerminalItem.Cmd(CC.ShowActiveUser()),
TerminalItem.Resp(CR.ActiveUser(User.sampleData))
)
}
}

View file

@ -0,0 +1,8 @@
package chat.simplex.app.ui.theme
import androidx.compose.ui.graphics.Color
val Purple200 = Color(0xFFBB86FC)
val Purple500 = Color(0xFF6200EE)
val Purple700 = Color(0xFF3700B3)
val Teal200 = Color(0xFF03DAC5)

View file

@ -0,0 +1,11 @@
package chat.simplex.app.ui.theme
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Shapes
import androidx.compose.ui.unit.dp
val Shapes = Shapes(
small = RoundedCornerShape(4.dp),
medium = RoundedCornerShape(4.dp),
large = RoundedCornerShape(0.dp)
)

View file

@ -0,0 +1,44 @@
package chat.simplex.app.ui.theme
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material.MaterialTheme
import androidx.compose.material.darkColors
import androidx.compose.material.lightColors
import androidx.compose.runtime.Composable
private val DarkColorPalette = darkColors(
primary = Purple200,
primaryVariant = Purple700,
secondary = Teal200
)
private val LightColorPalette = lightColors(
primary = Purple500,
primaryVariant = Purple700,
secondary = Teal200
/* Other default colors to override
background = Color.White,
surface = Color.White,
onPrimary = Color.White,
onSecondary = Color.Black,
onBackground = Color.Black,
onSurface = Color.Black,
*/
)
@Composable
fun SimpleXTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) {
val colors = if (darkTheme) {
DarkColorPalette
} else {
LightColorPalette
}
MaterialTheme(
colors = colors,
typography = Typography,
shapes = Shapes,
content = content
)
}

View file

@ -0,0 +1,28 @@
package chat.simplex.app.ui.theme
import androidx.compose.material.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
// Set of Material typography styles to start with
val Typography = Typography(
body1 = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp
)
/* Other default text styles to override
button = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.W500,
fontSize = 14.sp
),
caption = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 12.sp
)
*/
)

View file

@ -0,0 +1,38 @@
package chat.simplex.app.views
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import chat.simplex.app.model.ChatModel
import chat.simplex.app.model.TerminalItem
import chat.simplex.app.ui.theme.SimpleXTheme
import chat.simplex.app.views.chat.SendMsgView
@Composable
fun TerminalView(chatModel: ChatModel) {
Column {
TerminalLog(chatModel.terminalItems)
SendMsgView(chatModel.controller::sendCmd)
}
}
@Composable
fun TerminalLog(terminalItems: List<TerminalItem>) {
LazyColumn {
items(terminalItems) { item ->
Text(item.label)
}
}
}
@Preview
@Composable
fun PreviewSendMsgView() {
SimpleXTheme {
TerminalView(chatModel = ChatModel.sampleData)
}
}

View file

@ -0,0 +1,40 @@
package chat.simplex.app.views.chat
import androidx.compose.foundation.layout.*
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.material.TextField
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import chat.simplex.app.ui.theme.SimpleXTheme
@Composable
fun SendMsgView (sendMessage: (String) -> Unit) {
var cmd by remember { mutableStateOf("") }
Row {
TextField(value = cmd, onValueChange = { cmd = it }, modifier = Modifier.height(60.dp))
Spacer(Modifier.height(10.dp))
Button(
onClick = {
sendMessage(cmd)
cmd = ""
},
modifier = Modifier.width(40.dp).height(60.dp),
enabled = cmd.isNotEmpty()
) {
Text("Go")
}
}
}
@Preview
@Composable
fun PreviewSendMsgView() {
SimpleXTheme {
SendMsgView(
sendMessage = { msg -> println(msg) }
)
}
}

View file

@ -1,49 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<EditText
android:id="@+id/cmdInput"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:autofillHints=""
android:ems="10"
android:imeOptions="actionSend"
android:inputType="textPersonName"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:ignore="SpeakableTextPresentCheck" />
<ScrollView
android:id="@+id/scroller"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
app:layout_constraintBottom_toTopOf="@+id/cmdInput"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/chatlog"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
</ScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,16 +0,0 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Theme.SimpleX" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_200</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/black</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_200</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style>
</resources>

View file

@ -1,16 +1,7 @@
<resources xmlns:tools="http://schemas.android.com/tools"> <?xml version="1.0" encoding="utf-8"?>
<!-- Base application theme. --> <resources>
<style name="Theme.SimpleX" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<!-- Primary brand color. --> <style name="Theme.SimpleX" parent="android:Theme.Material.Light.NoActionBar">
<item name="colorPrimary">@color/purple_500</item> <item name="android:statusBarColor">@color/purple_700</item>
<item name="colorPrimaryVariant">@color/purple_700</item>
<item name="colorOnPrimary">@color/white</item>
<!-- Secondary brand color. -->
<item name="colorSecondary">@color/teal_200</item>
<item name="colorSecondaryVariant">@color/teal_700</item>
<item name="colorOnSecondary">@color/black</item>
<!-- Status bar color. -->
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
<!-- Customize your theme here. -->
</style> </style>
</resources> </resources>

View file

@ -1,16 +1,25 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript { buildscript {
ext {
compose_version = '1.1.0'
}
repositories { repositories {
google() google()
mavenCentral() mavenCentral()
} }
dependencies { dependencies {
classpath "com.android.tools.build:gradle:7.0.4" classpath 'com.android.tools.build:gradle:7.1.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10"
classpath "org.jetbrains.kotlin:kotlin-serialization:1.3.2"
// NOTE: Do not place your application dependencies here; they belong // NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files // in the individual module build.gradle files
} }
}// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
id 'com.android.application' version '7.1.1' apply false
id 'com.android.library' version '7.1.1' apply false
id 'org.jetbrains.kotlin.android' version '1.6.10' apply false
id 'org.jetbrains.kotlin.plugin.serialization' version '1.6.10'
} }
task clean(type: Delete) { task clean(type: Delete) {

View file

@ -15,7 +15,11 @@ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# Android operating system, and which are packaged with your app"s APK # Android operating system, and which are packaged with your app"s APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn # https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true android.useAndroidX=true
# Automatically convert third-party libraries to use AndroidX
android.enableJetifier=true
# Kotlin code style for this project: "official" or "obsolete": # Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official kotlin.code.style=official
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true
# Automatically convert third-party libraries to use AndroidX
android.enableJetifier=true

Binary file not shown.

View file

@ -1,6 +1,6 @@
#Fri Jan 21 23:13:54 GMT 2022 #Mon Feb 14 14:23:51 GMT 2022
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME

View file

@ -1,9 +1,15 @@
pluginManagement {
repositories {
gradlePluginPortal()
google()
mavenCentral()
}
}
dependencyResolutionManagement { dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories { repositories {
google() google()
mavenCentral() mavenCentral()
jcenter() // Warning: this repository is going to shut down soon
} }
} }
rootProject.name = "SimpleX" rootProject.name = "SimpleX"