diff --git a/.gitignore b/.gitignore index 24a8dc895..78889d5e8 100644 --- a/.gitignore +++ b/.gitignore @@ -17,4 +17,5 @@ gen/ /build /captures .externalNativeBuild +app/src/main/assets/bluetooth-server.txt .vagrant diff --git a/app/.gitignore b/app/.gitignore index 796b96d1c..b56b55e98 100644 --- a/app/.gitignore +++ b/app/.gitignore @@ -1 +1,2 @@ /build +/src/main/assets/bluetooth-server.txt diff --git a/app/build.gradle b/app/build.gradle index 0afbfcaa3..8ccebca80 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -92,6 +92,9 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'org.connectbot:sshlib:2.2.9' + implementation 'com.github.kbiakov:CodeView-Android:1.3.2' + + def lifecycle_version = "2.2.0" implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version" diff --git a/app/src/main/kotlin/io/treehouses/remote/Fragments/SettingsFragment.kt b/app/src/main/kotlin/io/treehouses/remote/Fragments/SettingsFragment.kt index ceacf0170..e186179dc 100644 --- a/app/src/main/kotlin/io/treehouses/remote/Fragments/SettingsFragment.kt +++ b/app/src/main/kotlin/io/treehouses/remote/Fragments/SettingsFragment.kt @@ -1,6 +1,7 @@ package io.treehouses.remote.Fragments import android.app.AlertDialog +import android.content.Context import android.content.DialogInterface import android.content.SharedPreferences.OnSharedPreferenceChangeListener import android.os.Bundle @@ -15,11 +16,13 @@ import androidx.core.content.ContextCompat import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import io.treehouses.remote.R +import io.treehouses.remote.callback.HomeInteractListener import io.treehouses.remote.utils.KeyUtils import io.treehouses.remote.utils.SaveUtils class SettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferenceClickListener { private var preferenceChangeListener: OnSharedPreferenceChangeListener? = null + private lateinit var listener : HomeInteractListener override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { setPreferencesFromResource(R.xml.app_preferences, rootKey) @@ -30,6 +33,7 @@ class SettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferenceClic val clearServices = findPreference("clear_services") val clearSSHHosts = findPreference("ssh_hosts") val clearSSHKeys = findPreference("ssh_keys") + val showBluetoothFile = findPreference("bluetooth_file") setClickListener(clearCommandsList) setClickListener(resetCommandsList) @@ -38,6 +42,7 @@ class SettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferenceClic setClickListener(clearServices) setClickListener(clearSSHHosts) setClickListener(clearSSHKeys) + setClickListener(showBluetoothFile) preferenceChangeListener = OnSharedPreferenceChangeListener { sharedPreferences, key -> if (key == "dark_mode") { @@ -87,6 +92,7 @@ class SettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferenceClic "reactivate_tutorials" -> reactivateTutorialsPrompt() "ssh_hosts" -> clearSSHHosts() "ssh_keys" -> clearSSHKeys() + "bluetooth_file" -> openBluetoothFile() } return false } @@ -109,6 +115,9 @@ class SettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferenceClic private fun clearSSHKeys() = createAlertDialog("Clear All SSH Keys", "Would you like to delete all SSH Keys?", "Clear", CLEAR_SSH_KEYS) + private fun openBluetoothFile() { + listener.openCallFragment(ShowBluetoothFile()) + } private fun createAlertDialog(title: String, message: String, positive: String, ID: Int) { val dialog = AlertDialog.Builder(ContextThemeWrapper(context, R.style.CustomAlertDialogStyle)) .setTitle(title) @@ -172,6 +181,12 @@ class SettingsFragment : PreferenceFragmentCompat(), Preference.OnPreferenceClic Toast.makeText(context, "Tutorials reactivated", Toast.LENGTH_LONG).show() } + override fun onAttach(context: Context) { + super.onAttach(context) + listener = if (context is HomeInteractListener) context + else throw Exception("Context does not implement HomeInteractListener") + } + companion object { private const val CLEAR_COMMANDS_ID = 1 private const val RESET_COMMANDS_ID = 2 diff --git a/app/src/main/kotlin/io/treehouses/remote/Fragments/ShowBluetoothFile.kt b/app/src/main/kotlin/io/treehouses/remote/Fragments/ShowBluetoothFile.kt new file mode 100644 index 000000000..8f37b2e8b --- /dev/null +++ b/app/src/main/kotlin/io/treehouses/remote/Fragments/ShowBluetoothFile.kt @@ -0,0 +1,57 @@ +package io.treehouses.remote.Fragments + +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope +import io.github.kbiakov.codeview.CodeView +import io.github.kbiakov.codeview.adapters.Options +import io.github.kbiakov.codeview.highlight.ColorTheme +import io.treehouses.remote.databinding.CodeViewBinding +import io.treehouses.remote.databinding.FragmentShowBluetoothFileBinding +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class ShowBluetoothFile : Fragment() { + private lateinit var bind : FragmentShowBluetoothFileBinding + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + bind = FragmentShowBluetoothFileBinding.inflate(inflater, container, false) + return bind.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + bind.pbar.visibility = View.VISIBLE + lifecycleScope.launch(Dispatchers.IO) { + val code = context?.assets?.open("bluetooth-server.txt")?.bufferedReader().use { it?.readText() } + Log.e("GOT CODE", code) + withContext(Dispatchers.Main) { + if (code == null) { + bind.fileNotFound.visibility = View.VISIBLE + } else { + withContext(Dispatchers.Default) { + val codeView = createCodeView(code) + withContext(Dispatchers.Main) { + bind.scriptContainer.addView(codeView, ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)) + bind.pbar.visibility = View.GONE + } + } + } + } + } + } + + fun createCodeView(code : String) : CodeView { + val codeView = CodeViewBinding.inflate(layoutInflater).root + codeView.setOptions(Options.Default.get(requireContext()) + .withLanguage("python") + .withCode(code) + .withTheme(ColorTheme.MONOKAI)) + return codeView + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/io/treehouses/remote/ui/home/BaseHomeFragment.kt b/app/src/main/kotlin/io/treehouses/remote/ui/home/BaseHomeFragment.kt index 8f99bcf84..17066de81 100644 --- a/app/src/main/kotlin/io/treehouses/remote/ui/home/BaseHomeFragment.kt +++ b/app/src/main/kotlin/io/treehouses/remote/ui/home/BaseHomeFragment.kt @@ -18,7 +18,12 @@ import io.treehouses.remote.Network.ParseDbService import io.treehouses.remote.bases.BaseFragment import io.treehouses.remote.callback.SetDisconnect import io.treehouses.remote.utils.LogUtils +import io.treehouses.remote.utils.Matcher +import io.treehouses.remote.utils.SaveUtils import io.treehouses.remote.utils.SaveUtils.Screens +import io.treehouses.remote.utils.Utils +import okhttp3.internal.Util +import java.nio.charset.Charset import java.util.* open class BaseHomeFragment : BaseFragment() { @@ -198,4 +203,30 @@ open class BaseHomeFragment : BaseFragment() { private fun CreateAlertDialog(context: Context?, id:Int, title: String): AlertDialog.Builder { return AlertDialog.Builder(ContextThemeWrapper(context, id)).setTitle(title) } + + protected fun syncBluetooth(serverHash: String) { + val inputStream = context?.assets?.open("bluetooth-server.txt") + val localString = inputStream?.bufferedReader().use { it?.readText() } + inputStream?.close() + val hashed = Utils.hashString(localString!!) + Log.e("HASHED", serverHash) + if (Matcher.isError(serverHash)) { + CreateAlertDialog(requireContext(), R.style.CustomAlertDialogStyle, "Upgrade Bluetooth").setMessage("There is a new version of bluetooth available. Please upgrade to receive the latest changes.") + .setPositiveButton("Upgrade") { _, _ -> + listener.sendMessage("treehouses upgrade bluetooth\n") + } + .setNegativeButton("Cancel") {dialog, _ -> dialog.dismiss()}.create().show() + } + else if (hashed.trim() != serverHash.trim()) { + CreateAlertDialog(context, R.style.CustomAlertDialogStyle, "Re-sync Bluetooth Server") + .setMessage("The bluetooth server on the Raspberry Pi does not match the one on your device. Would you like to update the CLI bluetooth server?") + .setPositiveButton("Upgrade") { _, _ -> + Log.e("ENCODED", Utils.compressString(localString)) + listener.sendMessage("remotesync ${Utils.compressString(localString).replace("\n","" )} cnysetomer\n") + Toast.makeText(requireContext(), "Bluetooth Upgraded. Please restart Bluetooth to apply the changes.", Toast.LENGTH_LONG).show() + }.setNegativeButton("Cancel") { dialog: DialogInterface, _: Int -> + dialog.dismiss() + }.show() + } + } } \ No newline at end of file diff --git a/app/src/main/kotlin/io/treehouses/remote/ui/home/HomeFragment.kt b/app/src/main/kotlin/io/treehouses/remote/ui/home/HomeFragment.kt index 9b3fd62af..5746a1fa7 100644 --- a/app/src/main/kotlin/io/treehouses/remote/ui/home/HomeFragment.kt +++ b/app/src/main/kotlin/io/treehouses/remote/ui/home/HomeFragment.kt @@ -9,6 +9,7 @@ import android.content.DialogInterface import android.content.Intent import android.net.Uri import android.os.* +import androidx.preference.PreferenceManager import android.view.ContextThemeWrapper import android.view.LayoutInflater import android.view.View @@ -18,7 +19,6 @@ import android.widget.Toast import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider -import androidx.preference.PreferenceManager import io.treehouses.remote.* import io.treehouses.remote.Constants.REQUEST_ENABLE_BT import io.treehouses.remote.Fragments.AboutFragment @@ -43,6 +43,7 @@ class HomeFragment : BaseHomeFragment(), SetDisconnect { private var selectedLed = 0 private var checkVersionSent = false private var internetSent = false + private var hashSent = false private var connectionState = false private var testConnectionResult = false private var networkSsid = "" @@ -174,12 +175,13 @@ class HomeFragment : BaseHomeFragment(), SetDisconnect { showLogDialog(preferences!!) transition(true) connectionState = true - checkVersionSent = true - listener.sendMessage(getString(R.string.TREEHOUSES_REMOTE_VERSION, BuildConfig.VERSION_CODE)) + listener.sendMessage("remotehash") + hashSent = true Tutorials.homeTutorials(bind, requireActivity()) } else { transition(false) connectionState = false + hashSent = false MainApplication.logSent = false } mChatService.updateHandler(mHandler) @@ -243,11 +245,15 @@ class HomeFragment : BaseHomeFragment(), SetDisconnect { private fun readMessage(output: String) { notificationListener = try { context as NotificationCallback? - } catch (e: ClassCastException) { - throw ClassCastException("Activity must implement NotificationListener") - } + } catch (e: ClassCastException) { throw ClassCastException("Activity must implement NotificationListener") } val s = match(output) when { + hashSent -> { + syncBluetooth(output) + hashSent = false + checkVersionSent = true + listener.sendMessage(getString(R.string.TREEHOUSES_REMOTE_VERSION, BuildConfig.VERSION_CODE)) + } s == RESULTS.ERROR && !output.toLowerCase(Locale.ROOT).contains("error") -> { showUpgradeCLI() internetSent = false diff --git a/app/src/main/kotlin/io/treehouses/remote/utils/CommandManager.kt b/app/src/main/kotlin/io/treehouses/remote/utils/CommandManager.kt index 578d50d86..b89aba8a6 100644 --- a/app/src/main/kotlin/io/treehouses/remote/utils/CommandManager.kt +++ b/app/src/main/kotlin/io/treehouses/remote/utils/CommandManager.kt @@ -54,7 +54,7 @@ object Matcher { private fun toLC(string: String) : String {return string.toLowerCase(Locale.ROOT).trim(); } fun isError(output: String): Boolean { - val keys = listOf("error ", "unknown command", "usage: ", "not a valid option", "error: ") + val keys = listOf("error ", "unknown command", "usage: ", "not a valid option", "error: ", "not found") if (output.contains("{") || output.contains("}")) return false for (k in keys) if (toLC(output).contains(k)) return true return false diff --git a/app/src/main/kotlin/io/treehouses/remote/utils/Utils.kt b/app/src/main/kotlin/io/treehouses/remote/utils/Utils.kt index 7edfded5b..9845c2e1a 100644 --- a/app/src/main/kotlin/io/treehouses/remote/utils/Utils.kt +++ b/app/src/main/kotlin/io/treehouses/remote/utils/Utils.kt @@ -3,9 +3,15 @@ package io.treehouses.remote.utils import android.content.ClipData import android.content.ClipboardManager import android.content.Context +import android.util.Base64 import android.widget.Toast +import java.io.ByteArrayOutputStream import java.net.NetworkInterface +import java.nio.charset.Charset +import java.security.MessageDigest import java.util.* +import java.util.zip.DeflaterOutputStream + object Utils { fun Context.copyToClipboard(clickedData: String) { @@ -50,4 +56,26 @@ object Utils { fun Context?.toast(s: String, duration: Int = Toast.LENGTH_LONG): Toast { return Toast.makeText(this, s, duration).apply { show() } } + + fun hashString(toHash: String) : String { + val digest = MessageDigest.getInstance("SHA-256") + val hash = digest.digest(toHash.toByteArray(Charset.forName("UTF-8"))) + + val hexString = StringBuffer() + for (c in hash) { + val hex = Integer.toHexString(0xff and c.toInt()) + if (hex.length == 1) hexString.append('0') + hexString.append(hex) + } + return hexString.toString() + } + + fun compressString(toCompress: String) : String{ + val baos = ByteArrayOutputStream() + val dos = DeflaterOutputStream(baos) + dos.write(toCompress.toByteArray(Charset.forName("UTF-8"))) + dos.flush() + dos.close() + return Base64.encodeToString(baos.toByteArray(), Base64.DEFAULT) + } } \ No newline at end of file diff --git a/app/src/main/res/layout/code_view.xml b/app/src/main/res/layout/code_view.xml new file mode 100644 index 000000000..346056810 --- /dev/null +++ b/app/src/main/res/layout/code_view.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_show_bluetooth_file.xml b/app/src/main/res/layout/fragment_show_bluetooth_file.xml new file mode 100644 index 000000000..125752623 --- /dev/null +++ b/app/src/main/res/layout/fragment_show_bluetooth_file.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/app_preferences.xml b/app/src/main/res/xml/app_preferences.xml index 8acbd47bf..ef02fdf62 100644 --- a/app/src/main/res/xml/app_preferences.xml +++ b/app/src/main/res/xml/app_preferences.xml @@ -131,5 +131,13 @@ + + + + diff --git a/build.gradle b/build.gradle index eec53aabd..5a2c171df 100644 --- a/build.gradle +++ b/build.gradle @@ -13,6 +13,11 @@ buildscript { // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } + def assets = new File('app/src/main/assets') + assets.mkdir() + def f = new File('app/src/main/assets/bluetooth-server.txt') + f.createNewFile() + new URL('https://raw.githubusercontent.com/treehouses/control/master/server.py').withInputStream{ i -> f.withOutputStream{ it << i }} } allprojects {