<|start_filename|>app/src/main/java/nemutui/com/github/nemu_droid/ConnectToApiActivity.kt<|end_filename|>
package nemutui.com.github.nemu_droid

import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.widget.TextView
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView

class ConnectToApiActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_connect_to_api)

        val api_conn = intent.getStringExtra(EXTRA_NEMU_API_LOCATION)
        val api_port = intent.getStringExtra(EXTRA_NEMU_API_PORT)
        val api_pass = intent.getStringExtra(EXTRA_NEMU_API_PASSWORD)
        val api_trust = intent.getBooleanExtra(EXTRA_CHECK_CERTIFICATE, false)

        val nemu_client = NemuApiClient(api_conn, api_port, api_pass, api_trust)
        val auth_res = nemu_client.checkAuth()

        nemu_api = nemu_client

        val tv = findViewById<TextView>(R.id.api_location)

        if (!auth_res) {
            tv.apply {
                this.text = getString(R.string.nemu_info, api_conn, api_port, nemu_client.getErr())
            }
        } else {
            val ver = nemu_client.nemuVersion()

            if (ver) {
                tv.apply {
                    this.text = getString(R.string.nemu_info, api_conn, api_port, nemu_client.getVersion())
                }
            } else {
                tv.apply {
                    this.text = getString(R.string.nemu_info, api_conn, api_port, nemu_client.getErr())
                }
            }

            if (!nemu_client.getVmList()) {
                Log.e("nemu-droid", "cannot get VM list")
                //kill app?
            }
            val rv = findViewById<RecyclerView>(R.id.vm_list_rv)
            val rv_adapter =  VmListAadapter(nemu_client.vmlist, nemu_client)
            rec_view = rv_adapter

            rv.apply {
                setHasFixedSize(true)
                adapter = rv_adapter
                layoutManager = LinearLayoutManager(this.context)
            }
        }
    }

    override fun onCreateOptionsMenu(menu: Menu): Boolean {
        // Inflate the menu to use in the action bar
        val inflater = menuInflater
        inflater.inflate(R.menu.menu_main, menu)

        if (nemu_api.checkAuth()) {
            val refresh_butt = menu.findItem(R.id.refresh_butt)
            refresh_butt.setVisible(true)
        }

        return super.onCreateOptionsMenu(menu)
    }

    fun refreshData(menu: MenuItem) {
        nemu_api.getVmList()
        rec_view.vmlist = nemu_api.vmlist.toList()
        rec_view.notifyDataSetChanged()
        /*
         list.remove(position);
         recycler.removeViewAt(position);
         mAdapter.notifyItemRemoved(position);
         mAdapter.notifyItemRangeChanged(position, list.size());
         */
    }

    fun editSettings(nemu: MenuItem) {
        val intent = Intent(this, SettingsActivity::class.java)
        startActivity(intent)
    }

    lateinit private var rec_view : VmListAadapter
    lateinit private var nemu_api : NemuApiClient
}

<|start_filename|>app/src/main/java/nemutui/com/github/nemu_droid/SettingsFragment.kt<|end_filename|>
package nemutui.com.github.nemu_droid

import android.os.Bundle
import android.text.InputType
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.EditTextPreference

class SettingsFragment : PreferenceFragmentCompat() {
    override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
        addPreferencesFromResource(R.xml.pref)
        val port_pref: EditTextPreference? = findPreference("ssh_port")
        val pass_pref: EditTextPreference? = findPreference("ssh_pass")

        port_pref?.setOnBindEditTextListener { editText ->
            editText.inputType = InputType.TYPE_CLASS_NUMBER
        }

        pass_pref?.setOnBindEditTextListener { editText ->
            editText.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
        }
    }
}

<|start_filename|>app/src/main/java/nemutui/com/github/nemu_droid/VmListAadapter.kt<|end_filename|>
package nemutui.com.github.nemu_droid

import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.PopupMenu
import android.widget.TextView
import android.widget.Toast
import androidx.recyclerview.widget.RecyclerView

class VmListAadapter(private val vms: MutableMap<String, Boolean>, api: NemuApiClient) :
    RecyclerView.Adapter<VmListAadapter.VmListHolder>() {
    class VmListHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
        var vm_name: TextView? = null
        var vm_status: ImageView? = null
        var vm_options: TextView? = null

        init {
            vm_name = itemView.findViewById(R.id.vmitem_name)
            vm_status = itemView.findViewById(R.id.vmitem_status)
            vm_options = itemView.findViewById(R.id.vmitem_options)
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VmListHolder {
        val itemView = LayoutInflater.from(parent.context)
                .inflate(R.layout.vmlist_item, parent, false)
        return VmListHolder(itemView)
    }

    @SuppressLint("IntentReset")
    override fun onBindViewHolder(holder: VmListHolder, position: Int) {
        holder.vm_name?.text = vmlist[position].first
        if (vmlist[position].second) {
            holder.vm_status?.setImageResource(R.drawable.ic_round_power_on_18)
        } else {
            holder.vm_status?.setImageResource(R.drawable.ic_round_power_off_18)
        }

        holder.vm_options?.setOnClickListener(View.OnClickListener {
            val menu = PopupMenu(holder.itemView.context, holder.vm_options)
            val name = vmlist[position].first
            val status = vmlist[position].second

            menu.inflate(R.menu.menu_options)
            menu.setOnMenuItemClickListener {
                when (it.itemId) {
                    R.id.menu_start -> {
                        if (!status) {
                            nemu_api.startVm(name)
                            Toast.makeText(holder.itemView.context, name + " started",
                                    Toast.LENGTH_SHORT).show()

                        } else {
                            Toast.makeText(holder.itemView.context, name + " already started",
                                    Toast.LENGTH_SHORT).show()
                        }
                        true
                    }
                    R.id.menu_stop -> {
                        if (status) {
                            nemu_api.stopVm(name)
                            Toast.makeText(holder.itemView.context, name + " stopped",
                                    Toast.LENGTH_SHORT).show()
                        } else {
                            Toast.makeText(holder.itemView.context, name + " already stopped",
                                    Toast.LENGTH_SHORT).show()
                        }
                        true
                    }
                    R.id.menu_force_stop -> {
                        if (status) {
                            nemu_api.forceStopVm(name)
                            Toast.makeText(holder.itemView.context, name + " force stopped",
                                    Toast.LENGTH_SHORT).show()
                        } else {
                            Toast.makeText(holder.itemView.context, name + " already stopped",
                                    Toast.LENGTH_SHORT).show()
                        }
                        true
                    }
                    R.id.menu_connect -> {
                        if (status) {
                            val aspice_intent = holder.itemView.context.
                            getPackageManager().getLaunchIntentForPackage(
                                    "com.iiordanov.freeaSPICE")

                            if (aspice_intent == null) {
                                Toast.makeText(holder.itemView.context, "install aSPICE",
                                        Toast.LENGTH_SHORT).show()
                            } else {
                                if (nemu_api.connectPort(name)) {
                                    val preferences = holder.itemView.context.getSharedPreferences(
                                        holder.itemView.context.getPackageName() + "_preferences", Context.MODE_PRIVATE)
                                    val intent = Intent(Intent.ACTION_VIEW)
                                    val use_ssh = preferences.getBoolean("ssh_proxy", false)
                                    val ssh_host = preferences.getString("ssh_host", nemu_api.getApiAddr())
                                    val ssh_port = preferences.getString("ssh_port", "22")
                                    val ssh_user = preferences.getString("ssh_user", "user")
                                    val ssh_pass = preferences.getString("ssh_pass", "not set")
                                    var uri = "spice://" + nemu_api.getApiAddr() + ":" +
                                           nemu_api.getConnectPort() + "?ConnectionName=" + name

                                    if (use_ssh) {
                                        uri += "&SpicePassword&SshHost=" + ssh_host + "&SshPort=" +
                                            ssh_port + "&SshUsername=" + ssh_user + "&SecurityType=24"
                                    }

                                    if (use_ssh && !ssh_pass.equals("not set")) {
                                        uri += "&SshPassword=" + ssh_pass
                                    }

                                    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
                                    intent.setType("application/vnd.spice")
                                    intent.setData(Uri.parse(uri))
                                    holder.itemView.context.startActivity(intent)
                                } else {
                                    Toast.makeText(holder.itemView.context, name + "cannot get connect port",
                                            Toast.LENGTH_SHORT).show()
                                }
                            }
                        } else {
                            Toast.makeText(holder.itemView.context, name + " not running",
                                    Toast.LENGTH_SHORT).show()
                        }
                        true
                    }
                    else -> false
                }
            }
            menu.show()
        })
    }

    override fun getItemCount() = vms.count()

    var vmlist: List<Pair<String, Boolean>> = vms.toList()
    private var nemu_api: NemuApiClient = api
}

<|start_filename|>app/src/main/java/nemutui/com/github/nemu_droid/MainActivity.kt<|end_filename|>
package nemutui.com.github.nemu_droid

import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.content.SharedPreferences
import android.content.Context
import android.os.Bundle
import android.os.StrictMode
import android.text.Editable
import android.text.TextWatcher
import android.view.View
import android.widget.Button
import android.widget.EditText
import androidx.appcompat.widget.SwitchCompat

const val EXTRA_NEMU_API_LOCATION = "nemutui.com.github.nemu_droid.EXTRA_NEMU_API_LOCATION"
const val EXTRA_NEMU_API_PASSWORD = "<PASSWORD>NEMU_API_PASSWORD"
const val EXTRA_NEMU_API_PORT     = "nemutui.com.github.nemu_droid.EXTRA_NEMU_API_PORT"
const val EXTRA_CHECK_CERTIFICATE = "nemutui.com.github.nemu_droid.EXTRA_CHECK_CERTIFICATE"

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        /* This is bad I know, but I'm noob in Kotlin and Android */
        val policy = StrictMode.ThreadPolicy.Builder().permitAll().build()
        StrictMode.setThreadPolicy(policy)

        api_conn_et = findViewById<EditText>(R.id.nemu_api_location)
        api_port_et = findViewById<EditText>(R.id.nemu_api_port)
        api_pass_et = findViewById<EditText>(R.id.nemu_api_password)
        api_check_cert_et = findViewById<SwitchCompat>(R.id.trust_all_sw)

        api_conn_et.addTextChangedListener(connect_text_watcher)
        api_port_et.addTextChangedListener(connect_text_watcher)
        api_pass_et.addTextChangedListener(connect_text_watcher)

        preferences = getSharedPreferences(
            getPackageName() + "_preferences", Context.MODE_PRIVATE)

        api_conn_et.setText(preferences.getString("api_conn", ""))
        api_port_et.setText(preferences.getString("api_port", ""))
        api_check_cert_et.setChecked(preferences.getBoolean("api_check_cert", false))
    }

    fun connectToApi(view: View) {
        val api_conn = api_conn_et.text.toString()
        val api_port = api_port_et.text.toString()
        val api_pass = api_pass_et.text.toString()
        val check_cert = api_check_cert_et.isChecked()

        val editor = preferences.edit()
        editor.putString("api_conn", api_conn)
        editor.putString("api_port", api_port)
        editor.putBoolean("api_check_cert", check_cert)
        editor.apply();

        val intent = Intent(this, ConnectToApiActivity::class.java).apply {
            putExtra(EXTRA_NEMU_API_LOCATION, api_conn)
            putExtra(EXTRA_NEMU_API_PASSWORD, <PASSWORD>)
            putExtra(EXTRA_NEMU_API_PORT,
                if (api_port.isNullOrEmpty()) getString(R.string.nemu_default_port) else api_port)
            putExtra(EXTRA_CHECK_CERTIFICATE, check_cert)
        }

        startActivity(intent)
    }

    private val connect_text_watcher = object : TextWatcher {
        override fun afterTextChanged(s: Editable?) {}
        override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
        override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
            val connect_button = findViewById<Button>(R.id.connect_button)
            val api_conn = api_conn_et.text.toString()
            val api_pass = api_pass_et.text.toString()
            val state = !api_conn.isEmpty() && !api_pass.isEmpty()

            connect_button.isEnabled = state
        }
    }

    private lateinit var api_conn_et : EditText
    private lateinit var api_port_et : EditText
    private lateinit var api_pass_et : EditText
    private lateinit var api_check_cert_et : SwitchCompat
    private lateinit var preferences : SharedPreferences
}

<|start_filename|>app/src/main/java/nemutui/com/github/nemu_droid/NemuApiClient.kt<|end_filename|>
package nemutui.com.github.nemu_droid

import android.annotation.SuppressLint
import android.util.Log
import io.ktor.network.selector.*
import io.ktor.network.sockets.*
import io.ktor.network.tls.*
import io.ktor.utils.io.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import org.json.JSONObject
import java.net.InetSocketAddress
import java.security.cert.X509Certificate
import javax.net.ssl.X509TrustManager

class X509TrustAllManager : X509TrustManager {
    @SuppressLint("TrustAllX509TrustManager")
    override fun checkClientTrusted(chain: Array<out X509Certificate>?, authType: String?) {}
    @SuppressLint("TrustAllX509TrustManager")
    override fun checkServerTrusted(chain: Array<out X509Certificate>?, authType: String?) {}
    override fun getAcceptedIssuers(): Array<X509Certificate?> = arrayOfNulls<X509Certificate>(0)
}

class NemuApiClient(addr: String?, port: String?, pass: String?, trust: Boolean) {
    fun checkAuth(): Boolean {
        val request = "{\"exec\":\"auth\", \"auth\":\"" + api_pass + "\"}"
        if (this.send_request(request)) {
            val json_reply = JSONObject(reply)
            val verdict = json_reply.getString("return")

            if (verdict == "ok") {
                return true
            }

            err_msg = json_reply.getString("error")
            return false
        }

        return false
    }

    fun startVm(name: String) {
        val request = "{\"exec\":\"vm_start\", \"name\":\"" + name +
                "\", \"auth\":\"" + api_pass + "\"}"

        if (this.send_request(request)) {
            return
        }
    }

    fun stopVm(name: String) {
        val request = "{\"exec\":\"vm_stop\", \"name\":\"" + name +
                "\", \"auth\":\"" + api_pass + "\"}"

        if (this.send_request(request)) {
            return
        }
    }

    fun forceStopVm(name: String) {
        val request = "{\"exec\":\"vm_force_stop\", \"name\":\"" + name +
                "\", \"auth\":\"" + api_pass + "\"}"

        if (this.send_request(request)) {
            return
        }
    }

    fun getVmList() : Boolean {
        val request = "{\"exec\":\"vm_list\", \"auth\":\"" + api_pass + "\"}"
        if (::vmlist.isInitialized) {
            vmlist.clear()
        } else {
            vmlist = mutableMapOf()
        }

        if (this.send_request(request)) {
            val json_reply = JSONObject(reply)
            val vms = json_reply.getJSONArray("return")
            val vm_count = vms.length()

            for (i in 0 .. (vm_count - 1)) {
                val vm = vms.getJSONObject(i)
                val name = vm.getString("name")
                val status = vm.getBoolean("status")

                vmlist.put(name, status)
            }

            return true
        }

        return false
    }

    fun nemuVersion() : Boolean {
        val request = "{\"exec\":\"nemu_version\", \"auth\":\"" + api_pass + "\"}"
        if (this.send_request(request)) {
            val json_reply = JSONObject(reply)
            if (!json_reply.isNull("error")) {
                val err = json_reply.getString("error")
                err_msg = err
                return false
            }

            val ver = json_reply.getString("return")
            version = ver
            return true
        }

        return false
    }

    fun connectPort(name: String) : Boolean {
        val request = "{\"exec\":\"vm_get_connect_port\", \"name\":\"" + name +
                "\", \"auth\":\"" + api_pass + "\"}"
        if (this.send_request(request)) {
            val json_reply = JSONObject(reply)
            if (!json_reply.isNull("error")) {
                val err = json_reply.getString("error")
                err_msg = err
                return false
            }

            val port = json_reply.getString("return")
            connect_port = port
            return true
        }

        return false
    }

    fun getErr() : String {
        return this.err_msg
    }

    fun getVersion() : String {
        return this.version
    }

    fun getApiAddr() : String? {
        return this.api_addr
    }

    fun getConnectPort() : String {
        return this.connect_port
    }

    private fun send_request(request: String): Boolean {
        var rc = true
        if (api_addr == null || api_port == null || api_pass == null) {
            this.err_msg = "null input params"
            return false
        }

        runBlocking {
            try {
                    val socket = aSocket(ActorSelectorManager(Dispatchers.IO)).tcp()
                            .connect(InetSocketAddress(api_addr, api_port.toInt()))
                            .tls(Dispatchers.IO, if (!api_trust) X509TrustAllManager() else null,
                                "SHA1PRNG")
                    val sw = socket.openWriteChannel(autoFlush = false)
                    sw.writeStringUtf8(request)
                    sw.flush()
                    val sr = socket.openReadChannel()
                    reply = sr.readUTF8Line().toString()
                    socket.close()
                } catch (ex: Exception) {
                    Log.e("nemu-droid", "something goes wrong", ex)
                    err_msg = ex.message.toString()
                    rc = false
                }
        }

        return rc
    }

    private val api_addr = addr
    private val api_port = port
    private val api_pass = <PASSWORD>
    private val api_trust = trust
    private var err_msg : String = ""
    private lateinit var reply : String
    private lateinit var version : String
    private lateinit var connect_port : String

    lateinit var vmlist: MutableMap<String, Boolean>
}