diff --git a/app/src/main/kotlin/io/treehouses/remote/BaseInitialActivity.kt b/app/src/main/kotlin/io/treehouses/remote/BaseInitialActivity.kt index 69c48b49f..6afb29d5f 100644 --- a/app/src/main/kotlin/io/treehouses/remote/BaseInitialActivity.kt +++ b/app/src/main/kotlin/io/treehouses/remote/BaseInitialActivity.kt @@ -26,6 +26,7 @@ import io.treehouses.remote.databinding.ActivityInitial2Binding import io.treehouses.remote.ui.system.SystemFragment import io.treehouses.remote.ui.home.HomeFragment import io.treehouses.remote.ui.network.NetworkFragment +import io.treehouses.remote.ui.sshconfig.SSHConfigFragment import io.treehouses.remote.ui.services.ServicesFragment import io.treehouses.remote.ui.status.StatusFragment import io.treehouses.remote.utils.LogUtils diff --git a/app/src/main/kotlin/io/treehouses/remote/bases/BaseSSHConfig.kt b/app/src/main/kotlin/io/treehouses/remote/bases/BaseSSHConfig.kt deleted file mode 100644 index 129b83bac..000000000 --- a/app/src/main/kotlin/io/treehouses/remote/bases/BaseSSHConfig.kt +++ /dev/null @@ -1,103 +0,0 @@ -package io.treehouses.remote.bases - -import android.content.* -import android.os.Bundle -import android.os.IBinder -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.recyclerview.widget.RecyclerView -import io.treehouses.remote.fragments.dialogfragments.EditHostDialogFragment -import io.treehouses.remote.ssh.terminal.TerminalManager -import io.treehouses.remote.ssh.beans.HostBean -import io.treehouses.remote.ssh.interfaces.OnHostStatusChangedListener -import io.treehouses.remote.views.RecyclerViewClickListener -import io.treehouses.remote.adapter.ViewHolderSSHRow -import io.treehouses.remote.callback.RVButtonClickListener -import io.treehouses.remote.databinding.DialogSshBinding -import io.treehouses.remote.databinding.RowSshBinding -import io.treehouses.remote.utils.SaveUtils -import io.treehouses.remote.utils.logD -import java.lang.Exception -import java.util.regex.Pattern - -open class BaseSSHConfig: BaseFragment(), RVButtonClickListener, OnHostStatusChangedListener { - protected val sshPattern = Pattern.compile("^(.+)@(([0-9a-z.-]+)|(\\[[a-f:0-9]+\\]))(:(\\d+))?$", Pattern.CASE_INSENSITIVE) - protected lateinit var bind: DialogSshBinding - protected lateinit var pastHosts: List - protected lateinit var adapter : RecyclerView.Adapter - protected var bound : TerminalManager? = null - protected val connection: ServiceConnection = object : ServiceConnection { - override fun onServiceConnected(className: ComponentName, service: IBinder) { - bound = (service as TerminalManager.TerminalBinder).service - // update our listview binder to find the service - setUpAdapter() - if (!bound?.hostStatusChangedListeners?.contains(this@BaseSSHConfig)!!) { - bound?.hostStatusChangedListeners?.add(this@BaseSSHConfig) - } - } - - override fun onServiceDisconnected(className: ComponentName) { - bound?.hostStatusChangedListeners?.remove(this@BaseSSHConfig) - bound = null - setUpAdapter() - } - } - - protected fun setUpAdapter() { - pastHosts = SaveUtils.getAllHosts(requireContext()).reversed() - if (!isVisible) return - if (pastHosts.isEmpty()) { - bind.noHosts.visibility = View.VISIBLE - bind.pastHosts.visibility = View.GONE - } - adapter = object : RecyclerView.Adapter() { - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolderSSHRow { - val holderBinding = RowSshBinding.inflate(LayoutInflater.from(parent.context), parent, false) - return ViewHolderSSHRow(holderBinding, this@BaseSSHConfig) - } - - override fun getItemCount(): Int { return pastHosts.size } - - override fun onBindViewHolder(holder: ViewHolderSSHRow, position: Int) { - val host = pastHosts[position] - holder.bind(host) - if (bound?.mHostBridgeMap?.get(host)?.get() != null) holder.setConnected(true) else holder.setConnected(false) - } - } - bind.pastHosts.adapter = adapter - addItemTouchListener() - } - - private fun addItemTouchListener() { - val listener = RecyclerViewClickListener(requireContext(), bind.pastHosts, object : RecyclerViewClickListener.ClickListener { - override fun onClick(view: View?, position: Int) { - val clicked = pastHosts[position] - bind.sshTextInput.setText(clicked.getPrettyFormat()) - } - override fun onLongClick(view: View?, position: Int) {} - }) - bind.pastHosts.addOnItemTouchListener(listener) - } - - override fun onAttach(context: Context) { - super.onAttach(context) - activity?.bindService(Intent(context, TerminalManager::class.java), connection, Context.BIND_AUTO_CREATE) - } - - override fun onStop() { - super.onStop() - try {activity?.unbindService(connection)} catch (e: Exception) {logD("SSHConfig $e")} - } - - override fun onButtonClick(position: Int) { - val edit = EditHostDialogFragment() - edit.setOnDismissListener(DialogInterface.OnDismissListener { setUpAdapter() }) - edit.arguments = Bundle().apply { putString(EditHostDialogFragment.SELECTED_HOST_URI, pastHosts[position].uri.toString())} - edit.show(childFragmentManager, "EditHost") - } - - override fun onHostStatusChanged() { - if (context != null) setUpAdapter() - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/io/treehouses/remote/fragments/SSHConfigFragment.kt b/app/src/main/kotlin/io/treehouses/remote/fragments/SSHConfigFragment.kt deleted file mode 100644 index 88608fbd0..000000000 --- a/app/src/main/kotlin/io/treehouses/remote/fragments/SSHConfigFragment.kt +++ /dev/null @@ -1,134 +0,0 @@ -package io.treehouses.remote.fragments - -import android.content.Intent -import android.net.Uri -import android.os.Bundle -import android.os.Message -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.core.widget.addTextChangedListener -import androidx.fragment.app.FragmentActivity -import io.treehouses.remote.Constants -import io.treehouses.remote.fragments.dialogfragments.SSHAllKeyFragment -import io.treehouses.remote.fragments.dialogfragments.SSHKeyGenFragment -import io.treehouses.remote.R -import io.treehouses.remote.Tutorials -import io.treehouses.remote.ssh.beans.HostBean -import io.treehouses.remote.sshconsole.SSHConsole -import io.treehouses.remote.bases.BaseSSHConfig -import io.treehouses.remote.databinding.DialogSshBinding -import io.treehouses.remote.utils.KeyUtils -import io.treehouses.remote.utils.KeyUtils.getOpenSSH -import io.treehouses.remote.utils.SaveUtils -import io.treehouses.remote.utils.Utils.toast -import io.treehouses.remote.utils.logD - - -class SSHConfigFragment : BaseSSHConfig() { - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - bind = DialogSshBinding.inflate(inflater, container, false) - if (listener.getChatService().state == Constants.STATE_CONNECTED) { - listener.sendMessage(getString(R.string.TREEHOUSES_NETWORKMODE_INFO)) - listener.getChatService().updateHandler(mHandler) - } - return bind.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - setEnabled(false) - addTextValidation() - Tutorials.sshTutorial(bind, requireActivity()) - bind.connectSsh.setOnClickListener { - var uriString = bind.sshTextInput.text.toString() - connect(uriString, false) - } - setUpAdapter() - bind.generateKeys.setOnClickListener { SSHKeyGenFragment().show(childFragmentManager, "GenerateKey") } - bind.smartConnect.setOnClickListener { - val shouldConnect = checkForSmartConnectKey() - var uriString = bind.sshTextInput.text.toString() - if (shouldConnect) connect(uriString, true) - } - bind.showKeys.setOnClickListener { SSHAllKeyFragment().show(childFragmentManager, "AllKeys") } - } - - private fun checkForSmartConnectKey(): Boolean { - if (!KeyUtils.getAllKeyNames(requireContext()).contains("SmartConnectKey")) { - if (listener?.getChatService()?.state == Constants.STATE_CONNECTED) { - val key = KeyUtils.createSmartConnectKey(requireContext()) - listener?.sendMessage(getString(R.string.TREEHOUSES_SSHKEY_ADD, getOpenSSH(key))) - } else { - context.toast("Bluetooth not connected. Could not send key to Pi.") - return false - } - } - return true - } - - private fun connect(uriStr: String, isSmartConnect: Boolean) { - var uriString = uriStr - if (!uriString.startsWith("ssh://")) uriString = "ssh://$uriString" - val host = HostBean() - host.setHostFromUri(Uri.parse(uriString)) - if (isSmartConnect) { - host.keyName = "SmartConnectKey" - host.fontSize = 7 - } - SaveUtils.updateHostList(requireContext(), host) - logD("HOST URI " + host.uri.toString()) - launchSSH(requireActivity(), host) - } - - private fun addTextValidation() { - bind.sshTextInput.addTextChangedListener { - if (sshPattern.matcher(it.toString()).matches()) { - bind.sshTextInput.error = null - setEnabled(true) - } else { - bind.sshTextInput.error = "Unknown Format" - setEnabled(false) - } - } - } - - fun setEnabled(bool: Boolean) { - bind.connectSsh.isEnabled = bool - bind.connectSsh.isClickable = bool - bind.smartConnect.isEnabled = bool - bind.smartConnect.isClickable = bool - } - - private fun launchSSH(activity: FragmentActivity, host: HostBean) { - val contents = Intent(Intent.ACTION_VIEW, host.uri) - contents.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP - contents.setClass(activity, SSHConsole::class.java) - activity.startActivity(contents) - } - - private fun getIP(s: String) { - if (s.contains("eth0")) { - val ipAddress = s.substringAfterLast("ip: ").trim() - val hostAddress = "pi@$ipAddress" - bind.sshTextInput.setText(hostAddress) - logD("GOT IP $ipAddress") - } else if (s.contains("ip") || s.startsWith("essid")) { - val ipString = s.split(", ")[1] - val ipAddress = ipString.substring(4) - val hostAddress = "pi@$ipAddress" - bind.sshTextInput.setText(hostAddress) - logD("GOT IP $ipAddress") - } - } - - override fun getMessage(msg: Message) { - when (msg.what) { - Constants.MESSAGE_READ -> { - val output = msg.obj as String - if (output.isNotEmpty()) getIP(output) - } - } - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/io/treehouses/remote/ui/sshconfig/SSHConfigFragment.kt b/app/src/main/kotlin/io/treehouses/remote/ui/sshconfig/SSHConfigFragment.kt new file mode 100644 index 000000000..8c62e9359 --- /dev/null +++ b/app/src/main/kotlin/io/treehouses/remote/ui/sshconfig/SSHConfigFragment.kt @@ -0,0 +1,202 @@ +package io.treehouses.remote.ui.sshconfig + +import android.content.* +import android.net.Uri +import android.os.Bundle +import android.os.IBinder +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.widget.addTextChangedListener +import androidx.fragment.app.FragmentActivity +import androidx.fragment.app.viewModels +import androidx.lifecycle.Observer +import androidx.recyclerview.widget.RecyclerView +import io.treehouses.remote.fragments.dialogfragments.SSHAllKeyFragment +import io.treehouses.remote.fragments.dialogfragments.SSHKeyGenFragment +import io.treehouses.remote.Tutorials +import io.treehouses.remote.adapter.ViewHolderSSHRow +import io.treehouses.remote.bases.BaseFragment +import io.treehouses.remote.ssh.beans.HostBean +import io.treehouses.remote.sshconsole.SSHConsole +import io.treehouses.remote.callback.RVButtonClickListener +import io.treehouses.remote.databinding.DialogSshBinding +import io.treehouses.remote.databinding.RowSshBinding +import io.treehouses.remote.fragments.dialogfragments.EditHostDialogFragment +import io.treehouses.remote.ssh.interfaces.OnHostStatusChangedListener +import io.treehouses.remote.ssh.terminal.TerminalManager +import io.treehouses.remote.utils.SaveUtils +import io.treehouses.remote.utils.logD +import io.treehouses.remote.views.RecyclerViewClickListener +import java.lang.Exception +import java.util.regex.Pattern + + +class SSHConfigFragment : BaseFragment(), RVButtonClickListener, OnHostStatusChangedListener { + + protected val sshPattern = Pattern.compile("^(.+)@(([0-9a-z.-]+)|(\\[[a-f:0-9]+\\]))(:(\\d+))?$", Pattern.CASE_INSENSITIVE) + protected val viewModel: SSHConfigViewModel by viewModels(ownerProducer = { this }) + private lateinit var bind: DialogSshBinding + + protected lateinit var pastHosts: List + protected lateinit var adapter : RecyclerView.Adapter + protected var bound : TerminalManager? = null + protected val connection: ServiceConnection = object : ServiceConnection { + override fun onServiceConnected(className: ComponentName, service: IBinder) { + bound = (service as TerminalManager.TerminalBinder).service + // update our listview binder to find the service + setUpAdapter() + if (!bound?.hostStatusChangedListeners?.contains(this@SSHConfigFragment)!!) { + bound?.hostStatusChangedListeners?.add(this@SSHConfigFragment) + } + } + + override fun onServiceDisconnected(className: ComponentName) { + bound?.hostStatusChangedListeners?.remove(this@SSHConfigFragment) + bound = null + setUpAdapter() + } + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + bind = DialogSshBinding.inflate(inflater, container, false) + viewModel.createView() + loadObservers1() + loadObservers2() + return bind.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + viewModel.setEnabled(false) + setListeners() + Tutorials.sshTutorial(bind, requireActivity()) + setUpAdapter() + } + + fun setListeners(){ + bind.sshTextInput.addTextChangedListener { + viewModel.sshTextChangedListener(sshPattern, it.toString()) + } + bind.connectSsh.setOnClickListener { + var uriString = bind.sshTextInput.text.toString() + connect(uriString, false) + } + bind.generateKeys.setOnClickListener { SSHKeyGenFragment().show(childFragmentManager, "GenerateKey") } + bind.smartConnect.setOnClickListener { + val shouldConnect = viewModel.checkForSmartConnectKey() + var uriString = bind.sshTextInput.text.toString() + if (shouldConnect) connect(uriString, true) + } + bind.showKeys.setOnClickListener { SSHAllKeyFragment().show(childFragmentManager, "AllKeys") } + } + + fun loadObservers1(){ + viewModel.sshTextInputText.observe(viewLifecycleOwner, Observer { + bind.sshTextInput.setText(it.toString()) + }) + viewModel.sshTextInputError.observe(viewLifecycleOwner, Observer { + bind.sshTextInput.error = it + }) + viewModel.noHostsVisibility.observe(viewLifecycleOwner, Observer { + bind.noHosts.visibility = if (!it) View.GONE else View.VISIBLE + }) + viewModel.pastsHostsVisibility.observe(viewLifecycleOwner, Observer { + var view = if (it) View.VISIBLE else View.GONE + bind.pastHosts.visibility = view + }) + viewModel.pastHostsList.observe(viewLifecycleOwner, Observer { + pastHosts = it + }) + } + + fun loadObservers2(){ + viewModel.connectSshEnabled.observe(viewLifecycleOwner, Observer { + bind.connectSsh.isEnabled = it + }) + viewModel.connectSshClickable.observe(viewLifecycleOwner, Observer { + bind.connectSsh.isClickable = it + }) + viewModel.smartConnectEnabled.observe(viewLifecycleOwner, Observer { + bind.smartConnect.isEnabled = it + }) + viewModel.smartConnectClickable.observe(viewLifecycleOwner, Observer { + bind.smartConnect.isClickable = it + }) + } + + private fun connect(uriStr: String, isSmartConnect: Boolean) { + var uriString = uriStr + if (!uriString.startsWith("ssh://")) uriString = "ssh://$uriString" + val host = HostBean() + host.setHostFromUri(Uri.parse(uriString)) + if (isSmartConnect) { + host.keyName = "SmartConnectKey" + host.fontSize = 7 + } + SaveUtils.updateHostList(requireContext(), host) + logD("HOST URI " + host.uri.toString()) + launchSSH(requireActivity(), host) + } + + private fun launchSSH(activity: FragmentActivity, host: HostBean) { + val contents = Intent(Intent.ACTION_VIEW, host.uri) + contents.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP + contents.setClass(activity, SSHConsole::class.java) + activity.startActivity(contents) + } + + fun setUpAdapter() { + viewModel.getPastHost() + if (!isVisible) return + viewModel.setNoHostPastHost() + adapter = object : RecyclerView.Adapter() { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolderSSHRow { + val holderBinding = RowSshBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return ViewHolderSSHRow(holderBinding, this@SSHConfigFragment) + } + + override fun getItemCount(): Int { return pastHosts.size } + + override fun onBindViewHolder(holder: ViewHolderSSHRow, position: Int) { + val host = pastHosts[position] + holder.bind(host) + if (bound?.mHostBridgeMap?.get(host)?.get() != null) holder.setConnected(true) else holder.setConnected(false) + } + } + bind.pastHosts.adapter = adapter + addItemTouchListener() + } + + fun addItemTouchListener() { + val listener = RecyclerViewClickListener(requireContext(), bind.pastHosts, object : RecyclerViewClickListener.ClickListener { + override fun onClick(view: View?, position: Int) { + val clicked = pastHosts[position] + viewModel.recylerOnClick(clicked.getPrettyFormat()) + } + override fun onLongClick(view: View?, position: Int) {} + }) + bind.pastHosts.addOnItemTouchListener(listener) + } + + override fun onAttach(context: Context) { + super.onAttach(context) + activity?.bindService(Intent(context, TerminalManager::class.java), connection, Context.BIND_AUTO_CREATE) + } + + override fun onStop() { + super.onStop() + try {activity?.unbindService(connection)} catch (e: Exception) {logD("SSHConfig $e")} + } + + override fun onButtonClick(position: Int) { + val edit = EditHostDialogFragment() + edit.setOnDismissListener(DialogInterface.OnDismissListener { setUpAdapter() }) + edit.arguments = Bundle().apply { putString(EditHostDialogFragment.SELECTED_HOST_URI, pastHosts[position].uri.toString())} + edit.show(childFragmentManager, "EditHost") + } + + override fun onHostStatusChanged() { + if (context != null) setUpAdapter() + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/io/treehouses/remote/ui/sshconfig/SSHConfigViewModel.kt b/app/src/main/kotlin/io/treehouses/remote/ui/sshconfig/SSHConfigViewModel.kt new file mode 100644 index 000000000..7b3b451a5 --- /dev/null +++ b/app/src/main/kotlin/io/treehouses/remote/ui/sshconfig/SSHConfigViewModel.kt @@ -0,0 +1,102 @@ +package io.treehouses.remote.ui.sshconfig + +import android.app.Application +import androidx.lifecycle.MutableLiveData +import io.treehouses.remote.Constants +import io.treehouses.remote.MainApplication +import io.treehouses.remote.R +import io.treehouses.remote.ssh.beans.HostBean +import io.treehouses.remote.bases.FragmentViewModel +import io.treehouses.remote.utils.KeyUtils +import io.treehouses.remote.utils.SaveUtils +import io.treehouses.remote.utils.Utils.toast +import io.treehouses.remote.utils.logD +import java.util.regex.Pattern + +open class SSHConfigViewModel(application: Application) : FragmentViewModel(application) { + private val context = getApplication().applicationContext + var sshTextInputText: MutableLiveData = MutableLiveData() //bind.sshTextInput.setText + var sshTextInputError: MutableLiveData = MutableLiveData() //bind.sshTextInput.error + var noHostsVisibility: MutableLiveData = MutableLiveData() //bind.noHosts.visibility + var pastsHostsVisibility: MutableLiveData = MutableLiveData() //bind.pastHosts.visibility + var pastHostsList: MutableLiveData> = MutableLiveData() //pastHosts + var connectSshEnabled: MutableLiveData = MutableLiveData() //bind.connectSsh.isEnabled + var connectSshClickable: MutableLiveData = MutableLiveData() //bind.connectSsh.isClickable = bool + var smartConnectEnabled: MutableLiveData = MutableLiveData() //bind.smartConnect.isEnabled = bool + var smartConnectClickable: MutableLiveData = MutableLiveData() //bind.smartConnect.isClickable = bool + + + override fun onRead(output: String) { + super.onRead(output) + if (output.isNotEmpty()) getIP(output) + + } + + fun createView(){ + if (mChatService.state == Constants.STATE_CONNECTED) { + sendMessage(getString(R.string.TREEHOUSES_NETWORKMODE_INFO)) + mChatService.updateHandler(mHandler) + } + } + + fun sshTextChangedListener(sshPattern: Pattern, sshText: String){ + if (sshPattern.matcher(sshText).matches()) { + sshTextInputError.value = null + setEnabled(true) + } else { + sshTextInputError.value = "Unknown Format" + setEnabled(false) + } + } + + fun checkForSmartConnectKey(): Boolean { + if (!KeyUtils.getAllKeyNames(context).contains("SmartConnectKey")) { + if (mChatService.state == Constants.STATE_CONNECTED) { + val key = KeyUtils.createSmartConnectKey(context) + sendMessage(getString(R.string.TREEHOUSES_SSHKEY_ADD, KeyUtils.getOpenSSH(key))) + } else { + context.toast("Bluetooth not connected. Could not send key to Pi.") + return false + } + } + return true + } + + private fun getIP(s: String) { + if (s.contains("eth0")) { + val ipAddress = s.substringAfterLast("ip: ").trim() + val hostAddress = "pi@$ipAddress" + sshTextInputText.value = hostAddress + logD("GOT IP $ipAddress") + } else if (s.contains("ip") || s.startsWith("essid")) { + val ipString = s.split(", ")[1] + val ipAddress = ipString.substring(4) + val hostAddress = "pi@$ipAddress" + sshTextInputText.value = hostAddress + logD("GOT IP $ipAddress") + } + } + + fun setEnabled(bool: Boolean) { + connectSshEnabled.value = bool + connectSshClickable.value = bool + smartConnectEnabled.value = bool + smartConnectClickable.value = bool + } + + fun getPastHost(){ + pastHostsList.value = SaveUtils.getAllHosts(context).reversed() + } + + fun setNoHostPastHost(){ + if (pastHostsList.value!!.isEmpty()) { + noHostsVisibility.value = true + pastsHostsVisibility.value = false + } + } + + fun recylerOnClick(prettyFormat: String){ + sshTextInputText.value = prettyFormat + } + +} \ No newline at end of file