Compare commits

..

7 Commits

5 changed files with 346 additions and 248 deletions

View File

@@ -1,63 +1,31 @@
# This file is a template, and might need editing before it works on your project.
# To contribute improvements to CI/CD templates, please follow the Development guide at:
# https://docs.gitlab.com/ee/development/cicd/templates.html
# This specific template is located at:
# https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Android.gitlab-ci.yml
# Read more about this script on this blog post https://about.gitlab.com/2018/10/24/setting-up-gitlab-ci-for-android-projects/, by Jason Lenny
# If you are interested in using Android with FastLane for publishing take a look at the Android-Fastlane template.
image: eclipse-temurin:17-jdk-jammy image: eclipse-temurin:17-jdk-jammy
variables: variables:
# ANDROID_COMPILE_SDK is the version of Android you're compiling with.
# It should match compileSdkVersion.
ANDROID_COMPILE_SDK: "34" ANDROID_COMPILE_SDK: "34"
# ANDROID_BUILD_TOOLS is the version of the Android build tools you are using.
# It should match buildToolsVersion.
ANDROID_BUILD_TOOLS: "33.0.2" ANDROID_BUILD_TOOLS: "33.0.2"
# It's what version of the command line tools we're going to download from the official site.
# Official Site-> https://developer.android.com/studio/index.html
# There, look down below at the cli tools only, sdk tools package is of format:
# commandlinetools-os_type-ANDROID_SDK_TOOLS_latest.zip
# when the script was last modified for latest compileSdkVersion, it was which is written down below
ANDROID_SDK_TOOLS: "9477386" ANDROID_SDK_TOOLS: "9477386"
# Keystore credentials stored as GitLab CI/CD variables
KEYSTORE_PASSWORD: $KS_PASSWORD
KEY_ALIAS: $KS_ALIAS
KEY_PASSWORD: $KS_KEY_PASSWORD
# Packages installation before running script
before_script: before_script:
- apt-get --quiet update --yes - apt-get --quiet update --yes
- apt-get --quiet install --yes wget unzip - apt-get --quiet install --yes wget unzip
# Setup path as android_home for moving/exporting the downloaded sdk into it
- export ANDROID_HOME="${PWD}/android-sdk-root" - export ANDROID_HOME="${PWD}/android-sdk-root"
# Create a new directory at specified location
- install -d $ANDROID_HOME - install -d $ANDROID_HOME
# Here we are installing androidSDK tools from official source,
# (the key thing here is the url from where you are downloading these sdk tool for command line, so please do note this url pattern there and here as well)
# after that unzipping those tools and
# then running a series of SDK manager commands to install necessary android SDK packages that'll allow the app to build
- wget --no-verbose --output-document=$ANDROID_HOME/cmdline-tools.zip https://dl.google.com/android/repository/commandlinetools-linux-${ANDROID_SDK_TOOLS}_latest.zip - wget --no-verbose --output-document=$ANDROID_HOME/cmdline-tools.zip https://dl.google.com/android/repository/commandlinetools-linux-${ANDROID_SDK_TOOLS}_latest.zip
- unzip -q -d "$ANDROID_HOME/cmdline-tools" "$ANDROID_HOME/cmdline-tools.zip" - unzip -q -d "$ANDROID_HOME/cmdline-tools" "$ANDROID_HOME/cmdline-tools.zip"
- mv -T "$ANDROID_HOME/cmdline-tools/cmdline-tools" "$ANDROID_HOME/cmdline-tools/tools" - mv -T "$ANDROID_HOME/cmdline-tools/cmdline-tools" "$ANDROID_HOME/cmdline-tools/tools"
- export PATH=$PATH:$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/cmdline-tools/tools/bin - export PATH=$PATH:$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/cmdline-tools/tools/bin
# Nothing fancy here, just checking sdkManager version
- sdkmanager --version - sdkmanager --version
# use yes to accept all licenses
- yes | sdkmanager --licenses > /dev/null || true - yes | sdkmanager --licenses > /dev/null || true
- sdkmanager "platforms;android-${ANDROID_COMPILE_SDK}" - sdkmanager "platforms;android-${ANDROID_COMPILE_SDK}"
- sdkmanager "platform-tools" - sdkmanager "platform-tools"
- sdkmanager "build-tools;${ANDROID_BUILD_TOOLS}" - sdkmanager "build-tools;${ANDROID_BUILD_TOOLS}"
# Not necessary, but just for surity
- chmod +x ./gradlew - chmod +x ./gradlew
# Basic android and gradle stuff
# Check linting
lintDebug: lintDebug:
interruptible: true interruptible: true
stage: build stage: build
@@ -69,7 +37,6 @@ lintDebug:
expose_as: "lint-report" expose_as: "lint-report"
when: always when: always
# Make Project
assembleDebug: assembleDebug:
interruptible: true interruptible: true
stage: build stage: build
@@ -78,8 +45,68 @@ assembleDebug:
artifacts: artifacts:
paths: paths:
- app/build/outputs/ - app/build/outputs/
# Job for building signed release APK for tags containing "release" on any branch
assembleRelease:
stage: build
script:
- |
if [[ "$CI_COMMIT_TAG" =~ release ]]; then
echo "Decoding keystore file from Base64"
# Debug: Print the first few characters of BASE64_KEYSTORE
echo "First 20 characters of BASE64_KEYSTORE: ${BASE64_KEYSTORE:0:20}..."
# Check if BASE64_KEYSTORE is a file path
if [[ "$BASE64_KEYSTORE" == /* ]] && [[ -f "$BASE64_KEYSTORE" ]]; then
echo "BASE64_KEYSTORE appears to be a file path. Reading content..."
BASE64_CONTENT=$(cat "$BASE64_KEYSTORE")
else
echo "BASE64_KEYSTORE is not a file path. Using as-is."
BASE64_CONTENT="$BASE64_KEYSTORE"
fi
# Remove any potential whitespace or newline characters
CLEANED_KEYSTORE=$(echo "$BASE64_CONTENT" | tr -d '[:space:]')
# Attempt to decode and save to a file
if echo "$CLEANED_KEYSTORE" | base64 -d > "$CI_PROJECT_DIR/app/keystore.jks" 2>/tmp/base64_error; then
echo "Keystore file decoded successfully"
else
echo "Error decoding keystore file:"
cat /tmp/base64_error
echo "First 20 characters of cleaned content: ${CLEANED_KEYSTORE:0:20}..."
exit 1
fi
# Check if the keystore file was created and has content
if [ -s "$CI_PROJECT_DIR/app/keystore.jks" ]; then
echo "Keystore file created successfully"
# Print file size for verification
ls -l "$CI_PROJECT_DIR/app/keystore.jks"
else
echo "Error: Keystore file is empty or not created"
exit 1
fi
echo "Building signed release APK"
./gradlew assembleRelease \
-Pandroid.injected.signing.store.file="$CI_PROJECT_DIR/app/keystore.jks" \
-Pandroid.injected.signing.store.password="$KEYSTORE_PASSWORD" \
-Pandroid.injected.signing.key.alias="$KEY_ALIAS" \
-Pandroid.injected.signing.key.password="$KEY_PASSWORD"
else
echo "Tag '$CI_COMMIT_TAG' does not contain 'release'. Skipping release build."
fi
artifacts:
paths:
- app/build/outputs/
expire_in: never
rules:
- if: $CI_COMMIT_TAG =~ /release/
when: always
- when: never
# Run all tests, if any fails, interrupt the pipeline(fail it)
debugTests: debugTests:
needs: [lintDebug, assembleDebug] needs: [lintDebug, assembleDebug]
interruptible: true interruptible: true

View File

@@ -21,7 +21,7 @@ android {
minSdk 21 minSdk 21
targetSdk 34 targetSdk 34
versionCode 130 versionCode 130
versionName "2.1.130" versionName "2.1.130 -usb"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
} }
@@ -30,6 +30,7 @@ android {
release { release {
minifyEnabled false minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.debug
} }
} }
compileOptions { compileOptions {

View File

@@ -55,7 +55,6 @@ class UsbTerminalActivity : AppCompatActivity() {
private var mConnection: UsbDeviceConnection? = null private var mConnection: UsbDeviceConnection? = null
lateinit var mService: UsbService lateinit var mService: UsbService
private var deviceId = "" private var deviceId = ""
private var executedCommands: MutableList<String> = mutableListOf()
private var isResuming = false private var isResuming = false
private val TAG = "Calibration" private val TAG = "Calibration"
@@ -64,38 +63,39 @@ class UsbTerminalActivity : AppCompatActivity() {
synchronized(this) { synchronized(this) {
val device: UsbDevice? = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE) val device: UsbDevice? = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE)
if (UsbManager.ACTION_USB_DEVICE_DETACHED == intent.action) { when (intent.action) {
// Handle USB disconnection UsbManager.ACTION_USB_DEVICE_DETACHED -> {
Log.d(TAG, "USB device detached") Log.d(TAG, "USB device detached")
cleanupUsb() cleanupUsb()
DataHolder.usbConnected.postValue(false) DataHolder.usbConnected.postValue(false)
} }
UsbManager.ACTION_USB_DEVICE_ATTACHED -> {
if (UsbManager.ACTION_USB_DEVICE_ATTACHED == intent.action) { Log.d(TAG, "USB device attached")
// Handle USB reconnection isResuming = true
Log.d(TAG, "USB device attached") connectUsb(false)
connectUsb(false) // Attempt reconnection }
} else if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) { else -> {
// Handle USB permission granted if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) {
device?.let { device?.let {
connectUsb(true) connectUsb(true)
DataHolder.usbConnected.postValue(true) DataHolder.usbConnected.postValue(true)
}
} else {
Log.e(TAG, "USB permission denied")
DataHolder.usbConnected.postValue(false)
}
} }
} else {
Log.e(TAG, "USB permission denied")
DataHolder.usbConnected.postValue(false)
} }
} }
} }
} }
private val connection = object : ServiceConnection { private val connection = object : ServiceConnection {
override fun onServiceConnected(className: ComponentName, service: IBinder) { override fun onServiceConnected(className: ComponentName, service: IBinder) {
val binder = service as UsbService.UsbServiceBinder val binder = service as UsbService.UsbServiceBinder
mService = binder.getService() mService = binder.getService()
usbTerminalViewModel.isServiceConnected = true usbTerminalViewModel.isServiceConnected = true
mConnection.let { mService.connect(mDriver, mConnection!!) } mConnection?.let { mService.connect(mDriver, it) }
moveToNext() moveToNext()
} }
@@ -104,15 +104,6 @@ class UsbTerminalActivity : AppCompatActivity() {
} }
} }
fun getExecutedCommands(): List<String> {
return executedCommands
}
fun addExecutedCommand(command: String) {
if (!executedCommands.contains(command)) {
executedCommands.add(command)
}
}
override fun attachBaseContext(newBase: Context?) { override fun attachBaseContext(newBase: Context?) {
val languageCode = LanguageManager.getSavedLanguage(newBase!!) val languageCode = LanguageManager.getSavedLanguage(newBase!!)
LanguageManager.setLocale(newBase, languageCode) LanguageManager.setLocale(newBase, languageCode)
@@ -123,31 +114,34 @@ class UsbTerminalActivity : AppCompatActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
binding = ActivityUsbTerminalBinding.inflate(layoutInflater) binding = ActivityUsbTerminalBinding.inflate(layoutInflater)
setContentView(binding.root) setContentView(binding.root)
sharedPreferences = getSharedPreferences("HEMOCUBE", Context.MODE_PRIVATE)
val filter = IntentFilter() val filter = IntentFilter().apply {
filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED) addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED)
filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED) addAction(UsbManager.ACTION_USB_DEVICE_DETACHED)
addAction(Constants.HEMOCUBE_USB_PERMISSION)
}
registerReceiver(broadcastReceiver, filter) registerReceiver(broadcastReceiver, filter)
doBoth()
}
fun doBoth(){
setupListener() setupListener()
connectUsb(false) connectUsb(false)
} }
override fun onResume() {
super.onResume()
isResuming = true
connectUsb(false)
}
private fun setupListener() { private fun setupListener() {
DataHolder.usbConnected.observe(this) { isConnected ->
DataHolder.usbConnected.observe(this) { Log.d("USB OBSERVE", "HemoCube called -> $isConnected")
Log.d("USB OBSERVE", "HemoCube called -> $it") myMenu?.get(0)?.icon = ContextCompat.getDrawable(
if (it) { this,
myMenu?.get(0)?.icon = if (isConnected) R.drawable.ic_baseline_usb_24
ContextCompat.getDrawable(this, R.drawable.ic_baseline_usb_24) else R.drawable.ic_baseline_usb_off_24
)
} else { if (!isConnected) {
myMenu?.get(0)?.icon =
ContextCompat.getDrawable(this, R.drawable.ic_baseline_usb_off_24)
Toast.makeText(this, "Device is Disconnected", Toast.LENGTH_SHORT).show() Toast.makeText(this, "Device is Disconnected", Toast.LENGTH_SHORT).show()
} }
} }
@@ -155,61 +149,78 @@ class UsbTerminalActivity : AppCompatActivity() {
open fun connectUsb(permissionGranted: Boolean) { open fun connectUsb(permissionGranted: Boolean) {
Log.d(TAG, "connectUsb() called, permission variable = $permissionGranted") Log.d(TAG, "connectUsb() called, permission variable = $permissionGranted")
cleanupUsb() // Clean up before reinitializing
val manager = getSystemService(Context.USB_SERVICE) as UsbManager try {
val availableDrivers = UsbSerialProber.getDefaultProber().findAllDrivers(manager) val manager = getSystemService(Context.USB_SERVICE) as UsbManager
val availableDrivers = UsbSerialProber.getDefaultProber().findAllDrivers(manager)
if (availableDrivers.isEmpty()) {
onErrorReported("No Device Connected")
DataHolder.usbConnected.postValue(false)
return
}
if (availableDrivers.isEmpty()) {
onErrorReported("No Device Connected")
DataHolder.usbConnected.postValue(false)
} else {
mDriver = availableDrivers[0] mDriver = availableDrivers[0]
mConnection = manager.openDevice(mDriver.device) mConnection = manager.openDevice(mDriver.device)
if (mConnection == null) {
requestUserPermission(manager, mDriver.device) when {
} else { mConnection == null -> requestUserPermission(manager, mDriver.device)
setupService() else -> {
DataHolder.usbConnected.postValue(true) setupService()
DataHolder.usbConnected.postValue(true)
// Ensure fragment is loaded and state is restored
Handler(Looper.getMainLooper()).postDelayed({
(supportFragmentManager.findFragmentById(binding.fgDevice.id) as? UsbTerminalFragment)?.let {
it.loadCommandState()
it.resumeExecution()
}
}, 1000) // Slightly longer delay to ensure stability
isResuming = false
}
} }
} catch (e: Exception) {
Log.e(TAG, "USB Connection Error: ${e.message}")
onErrorReported("USB Connection Failed: ${e.localizedMessage}")
DataHolder.usbConnected.postValue(false)
}
}
private fun moveToNext() {
if (!supportFragmentManager.isDestroyed) {
supportFragmentManager.beginTransaction()
.replace(binding.fgDevice.id, UsbTerminalFragment())
.commit()
} }
} }
fun onErrorReported(msg: String) {
Toast.makeText(this, msg, Toast.LENGTH_SHORT).show()
}
private fun moveToNext() {
if (supportFragmentManager.isDestroyed) return
supportFragmentManager.beginTransaction().replace(binding.fgDevice.id, UsbTerminalFragment())
.commit()
}
@SuppressLint("MutableImplicitPendingIntent") @SuppressLint("MutableImplicitPendingIntent")
private fun requestUserPermission(manager: UsbManager, device: UsbDevice) { private fun requestUserPermission(manager: UsbManager, device: UsbDevice) {
val mPendingIntent: PendingIntent val mPendingIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) { PendingIntent.getBroadcast(
mPendingIntent = PendingIntent.getBroadcast( this, 0,
this, 0, Intent(Constants.HEMOCUBE_USB_PERMISSION), PendingIntent.FLAG_MUTABLE Intent(Constants.HEMOCUBE_USB_PERMISSION),
PendingIntent.FLAG_MUTABLE
) )
} else { } else {
mPendingIntent = PendingIntent.getBroadcast( PendingIntent.getBroadcast(
this, this, 0,
0,
Intent(Constants.HEMOCUBE_USB_PERMISSION), Intent(Constants.HEMOCUBE_USB_PERMISSION),
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE
) )
} }
val filter = IntentFilter(Constants.HEMOCUBE_USB_PERMISSION) registerReceiver(
registerReceiver(broadcastReceiver, filter) broadcastReceiver,
IntentFilter(Constants.HEMOCUBE_USB_PERMISSION)
)
manager.requestPermission(device, mPendingIntent) manager.requestPermission(device, mPendingIntent)
} }
fun onErrorReported(msg: String) {
Toast.makeText(this, msg, Toast.LENGTH_SHORT).show()
}
fun setupService() { fun setupService() {
val intent = Intent(this, UsbService::class.java) val intent = Intent(this, UsbService::class.java)
bindService(intent, connection, Context.BIND_AUTO_CREATE) bindService(intent, connection, Context.BIND_AUTO_CREATE)
@@ -217,10 +228,10 @@ class UsbTerminalActivity : AppCompatActivity() {
override fun onCreateOptionsMenu(menu: Menu?): Boolean { override fun onCreateOptionsMenu(menu: Menu?): Boolean {
menuInflater.inflate(R.menu.my_menu, menu) menuInflater.inflate(R.menu.my_menu, menu)
myMenu = menu
return true return true
} }
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
unregisterReceiver(broadcastReceiver) unregisterReceiver(broadcastReceiver)
@@ -232,17 +243,16 @@ class UsbTerminalActivity : AppCompatActivity() {
cleanupUsb() cleanupUsb()
} }
private fun cleanupUsb() { private fun cleanupUsb() {
try { try {
mService.disconnect() if (usbTerminalViewModel.isServiceConnected) {
unbindService(connection) mService.disconnect()
unbindService(connection)
}
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Error during USB cleanup: ${e.message}") Log.e(TAG, "Error during USB cleanup: ${e.message}")
} }
mConnection?.close() mConnection?.close()
mConnection = null mConnection = null
} }
} }

View File

@@ -18,7 +18,10 @@ package com.example.hpostesting.presentation.usb_teminal
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.os.Bundle import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.text.method.ScrollingMovementMethod import android.text.method.ScrollingMovementMethod
import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@@ -27,7 +30,7 @@ import androidx.fragment.app.activityViewModels
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import com.example.hpostesting.presentation.utils.UsbServiceListener import com.example.hpostesting.presentation.utils.UsbServiceListener
import `in`.sminnovations.hpostesting.databinding.FragmentUsbTerminalBinding import `in`.sminnovations.hpostesting.databinding.FragmentUsbTerminalBinding
// UsbTerminalFragment.kt
class UsbTerminalFragment : Fragment() { class UsbTerminalFragment : Fragment() {
private lateinit var binding: FragmentUsbTerminalBinding private lateinit var binding: FragmentUsbTerminalBinding
private val usbTerminalViewModel: UsbTerminalViewModel by activityViewModels() private val usbTerminalViewModel: UsbTerminalViewModel by activityViewModels()
@@ -36,193 +39,250 @@ class UsbTerminalFragment : Fragment() {
private var resultData = StringBuilder() private var resultData = StringBuilder()
private var commandQueue: MutableList<String> = mutableListOf() private var commandQueue: MutableList<String> = mutableListOf()
private var currentCommand: String? = null private var currentCommand: String? = null
var executedCommands: MutableList<String> = mutableListOf() private var isExecuting = false
companion object {
private const val PREF_COMMAND_QUEUE = "command_queue"
private const val PREF_CURRENT_COMMAND = "current_command"
private const val PREF_RESULT_DATA = "result_data"
private const val PREF_EXECUTION_STATE = "execution_state"
}
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?, inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View { ): View {
binding = FragmentUsbTerminalBinding.inflate(inflater, container, false) binding = FragmentUsbTerminalBinding.inflate(inflater, container, false)
sharedPreferences = requireContext().getSharedPreferences("HEMOCUBE", Context.MODE_PRIVATE) sharedPreferences = requireContext().getSharedPreferences("HEMOCUBE", Context.MODE_PRIVATE)
setupClickListeners()
return binding.root return binding.root
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
val pastCommands = (activity as? UsbTerminalActivity)?.getExecutedCommands() ?: emptyList()
if (pastCommands.isNotEmpty()) {
resumeCommands(pastCommands)
}
initViews() initViews()
loadCommandState()
} }
private fun setupClickListeners() {
binding.clearQueueButton.setOnClickListener { clearQueue() }
binding.sendCommand.setOnClickListener {
binding.etCommands.text.toString().trim().let { commands ->
if (commands.isNotEmpty()) {
addToQueue(commands)
binding.etCommands.text.clear()
}
}
}
}
private fun initViews() { private fun initViews() {
binding.messageCommand.movementMethod = ScrollingMovementMethod() binding.messageCommand.apply {
movementMethod = ScrollingMovementMethod()
}
// Observe messages with proper formatting
usbTerminalViewModel.messages.observe(viewLifecycleOwner) { message -> usbTerminalViewModel.messages.observe(viewLifecycleOwner) { message ->
binding.messageCommand.text = message binding.messageCommand.text = message
scrollToBottom() scrollToBottom()
} }
binding.sendCommand.setOnClickListener {
val commands = binding.etCommands.text.toString().trim()
if (commands.isNotEmpty()) {
// Process entire command string
addToQueue(listOf(commands))
binding.etCommands.text.clear()
}
}
binding.button.setOnClickListener{
}
listenToHemoCube() listenToHemoCube()
callCommand("I") callCommand("I")
} }
private fun scrollToBottom() { private fun addToQueue(commandStr: String) {
binding.messageCommand.post { val commands = if (commandStr.contains(" ")) {
val scrollAmount = binding.messageCommand.layout.getLineTop(binding.messageCommand.lineCount) - commandStr.split("").filter { it.isNotBlank() }
binding.messageCommand.height } else {
if (scrollAmount > 0) { listOf(commandStr)
binding.messageCommand.scrollTo(0, scrollAmount)
}
} }
}
private fun addToQueue(commands: List<String>) {
commandQueue.clear()
currentCommand = null
commandQueue.addAll(commands) commandQueue.addAll(commands)
saveCommandState()
// Track commands in the activity appendFormattedMessage("\nAdded commands to queue: ${commands.joinToString(" ")}")
(activity as? UsbTerminalActivity)?.let { activity ->
commands.forEach { command ->
activity.addExecutedCommand(command)
}
}
appendFormattedMessage("\nNew priority commands: ${commands.joinToString(" ")}")
processNextCommand() processNextCommand()
} }
private fun appendFormattedMessage(message: String) { fun loadCommandState() {
resultData.append("\n").append(message) val prefs = sharedPreferences
usbTerminalViewModel.messages.postValue(resultData.toString()) prefs.apply {
scrollToBottom() // Restore command queue
val savedQueue = getStringSet(PREF_COMMAND_QUEUE, setOf())?.toMutableList() ?: mutableListOf()
commandQueue.clear()
commandQueue.addAll(savedQueue)
// Restore current command
currentCommand = getString(PREF_CURRENT_COMMAND, null)
// Restore result data
getString(PREF_RESULT_DATA, null)?.let {
resultData = StringBuilder(it)
usbTerminalViewModel.messages.postValue(it)
}
// Restore execution state
isExecuting = getBoolean(PREF_EXECUTION_STATE, false)
}
}
fun resumeExecution() {
try {
// Force re-establish connection and resume
if (currentCommand != null || commandQueue.isNotEmpty()) {
// Slight delay to ensure USB connection is stable
Handler(Looper.getMainLooper()).postDelayed({
processNextCommand()
}, 500)
}
} catch (e: Exception) {
Log.e("UsbTerminalFragment", "Error resuming execution: ${e.message}")
appendFormattedMessage("Error resuming: ${e.localizedMessage}")
}
} }
private fun processNextCommand() { private fun processNextCommand() {
if (currentCommand == null && commandQueue.isNotEmpty()) { if (!isExecuting && currentCommand == null && commandQueue.isNotEmpty()) {
currentCommand = commandQueue.removeAt(0) currentCommand = commandQueue.removeAt(0)
isExecuting = true
saveCommandState()
callCommand(currentCommand!!) callCommand(currentCommand!!)
} }
} }
private fun callCommand(command: String) { private fun callCommand(command: String) {
// Don't process if command was cleared
if (currentCommand == null) {
return
}
usbTerminalViewModel.progressBar.postValue(true)
appendFormattedMessage("Sending command: $command \n")
try { try {
(activity as UsbTerminalActivity).mService.sendAndListenToHemoCubeTxt( val usbActivity = activity as? UsbTerminalActivity
command, usbActivity?.mService?.let { service ->
object : UsbServiceListener { usbTerminalViewModel.progressBar.postValue(true)
override fun onUsbRead(data: ByteArray?) { appendFormattedMessage("Sending command: $command \n")
// Keep empty as per original code
}
override fun onUsbError(e: Exception?) { service.sendAndListenToHemoCubeTxt(
activity?.runOnUiThread { command,
object : UsbServiceListener {
override fun onUsbRead(data: ByteArray?) {
// Optional: Handle read if needed
}
appendFormattedMessage("Error sending command $command: ${e?.message ?: "Unknown error"}") override fun onUsbError(e: Exception?) {
usbTerminalViewModel.progressBar.postValue(false) activity?.runOnUiThread {
currentCommand = null handleCommandError(e)
}
processNextCommand()
} }
} }
} )
) } ?: run {
// If service is not available, log and handle error
Log.e("UsbTerminalFragment", "USB Service not available")
handleCommandError(Exception("USB Service not available"))
}
} catch (e: Exception) { } catch (e: Exception) {
appendFormattedMessage("Error: ${e.localizedMessage}") handleCommandError(e)
usbTerminalViewModel.progressBar.postValue(false)
currentCommand = null
processNextCommand()
} }
} }
private fun handleCommandError(e: Exception?) {
val errorMessage = e?.localizedMessage ?: "Unknown error"
Log.e("UsbTerminalFragment", "Command error: $errorMessage")
appendFormattedMessage("Error: $errorMessage")
// Ensure we don't completely stop execution
usbTerminalViewModel.progressBar.postValue(false)
isExecuting = false
// Important: Do NOT clear the current command or queue
saveCommandState()
// Try to process next command
processNextCommand()
}
private fun listenToHemoCube() { private fun listenToHemoCube() {
startListening.postValue(true) startListening.postValue(true)
try { try {
(activity as UsbTerminalActivity).mService.listenToHemoCube(object : UsbServiceListener { (activity as? UsbTerminalActivity)?.mService?.listenToHemoCube(
override fun onUsbRead(data: ByteArray?) { object : UsbServiceListener {
data?.let { override fun onUsbRead(data: ByteArray?) {
val stringData = String(it) data?.let { handleUsbData(String(it)) }
resultData.append(stringData) }
override fun onUsbError(e: Exception?) {
activity?.runOnUiThread { activity?.runOnUiThread {
usbTerminalViewModel.messages.postValue(resultData.toString()) Log.e("UsbTerminalFragment", "USB Listen Error: ${e?.message}")
scrollToBottom() // Do not completely stop execution
appendFormattedMessage("USB Listen Error: ${e?.localizedMessage}")
// Check for command completion
if (currentCommand != null && stringData.contains("${currentCommand}C")) {
currentCommand = null
usbTerminalViewModel.progressBar.postValue(false)
// If there are no more commands in queue, clear completion flags
if (commandQueue.isEmpty()) {
appendFormattedMessage("\nAll commands completed")
} else {
processNextCommand()
}
}
} }
} }
} }
)
override fun onUsbError(e: Exception?) {
activity?.runOnUiThread {
appendFormattedMessage("Error: ${e?.localizedMessage ?: "Unknown error"}")
usbTerminalViewModel.progressBar.postValue(false)
currentCommand = null
processNextCommand()
}
}
})
} catch (e: Exception) { } catch (e: Exception) {
appendFormattedMessage("Error: ${e.localizedMessage}") Log.e("UsbTerminalFragment", "Error setting up USB listener: ${e.message}")
appendFormattedMessage("Error setting up USB listener: ${e.localizedMessage}")
} }
} }
fun resumeCommands(pastCommands: List<String>) {
appendFormattedMessage("Resuming execution...") private fun handleUsbData(stringData: String) {
pastCommands.forEach { command -> resultData.append(stringData)
if (!commandQueue.contains(command)) {
commandQueue.add(command) activity?.runOnUiThread {
usbTerminalViewModel.messages.postValue(resultData.toString())
scrollToBottom()
if (currentCommand != null && stringData.contains("${currentCommand}C")) {
isExecuting = false
currentCommand = null
if (commandQueue.isEmpty()) {
sharedPreferences.edit().clear().apply()
appendFormattedMessage("\nAll commands completed")
} else {
processNextCommand()
}
saveCommandState()
} }
} }
processNextCommand()
} }
fun clearQueue() {
commandQueue.clear()
currentCommand = null
isExecuting = false
saveCommandState()
appendFormattedMessage("\nCommand queue cleared")
}
private fun scrollToBottom() {
binding.messageCommand.post {
(binding.messageCommand.layout.getLineTop(binding.messageCommand.lineCount) -
binding.messageCommand.height).takeIf { it > 0 }?.let {
binding.messageCommand.scrollTo(0, it)
}
}
}
private fun appendFormattedMessage(message: String) {
resultData.append("\n").append(message)
usbTerminalViewModel.messages.postValue(resultData.toString())
scrollToBottom()
}
private fun saveCommandState() {
sharedPreferences.edit().apply {
putStringSet(PREF_COMMAND_QUEUE, commandQueue.toSet())
putString(PREF_CURRENT_COMMAND, currentCommand)
putString(PREF_RESULT_DATA, resultData.toString())
putBoolean(PREF_EXECUTION_STATE, isExecuting)
apply()
}
}
override fun onDestroyView() { override fun onDestroyView() {
super.onDestroyView() super.onDestroyView()
startListening.value = false startListening.value = false
resultData.clear() clearQueue()
commandQueue.clear()
currentCommand = null
} }
} }

View File

@@ -18,7 +18,7 @@
android:padding="16dp"> android:padding="16dp">
<Button <Button
android:id="@+id/button" android:id="@+id/clearQueueButton"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="Clar All Commands" /> android:text="Clar All Commands" />