Skip to content

Commit

Permalink
Merge pull request #205 from enaboapps/iss203
Browse files Browse the repository at this point in the history
Persist scan receiver state
  • Loading branch information
enaboapps authored Mar 8, 2024
2 parents 677a5fd + 2ed9281 commit 57cef0f
Show file tree
Hide file tree
Showing 11 changed files with 179 additions and 63 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class PreferenceManager(context: Context) {
const val PREFERENCE_KEY_PAUSE_SCAN_ON_SWITCH_HOLD = "pause_scan_on_switch_hold"
const val PREFERENCE_KEY_SWITCH_IGNORE_REPEAT = "switch_ignore_repeat"
const val PREFERENCE_KEY_SWITCH_IGNORE_REPEAT_DELAY = "switch_ignore_repeat_delay"
const val PREFERENCE_KEY_SCAN_RECEIVER = "scan_receiver"
private const val PREFERENCE_FILE_NAME = "switchify_preferences"
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,61 +1,88 @@
package com.enaboapps.switchify.service

import android.accessibilityservice.AccessibilityService
import android.util.Log
import android.view.KeyEvent
import android.view.accessibility.AccessibilityEvent
import com.enaboapps.switchify.preferences.PreferenceManager
import com.enaboapps.switchify.service.gestures.GestureManager
import com.enaboapps.switchify.service.nodes.NodeExaminer
import com.enaboapps.switchify.service.scanning.ScanReceiver
import com.enaboapps.switchify.service.scanning.ScanningManager
import com.enaboapps.switchify.service.selection.AutoSelectionHandler
import com.enaboapps.switchify.service.switches.SwitchListener
import com.enaboapps.switchify.service.utils.KeyboardInfo

/**
* This is the main service class for the Switchify application.
* It extends the AccessibilityService class to provide accessibility features.
*/
class SwitchifyAccessibilityService : AccessibilityService() {

private val TAG = "SwitchifyAccessibilityService"
// ScanningManager instance for managing scanning operations
private lateinit var scanningManager: ScanningManager

private var scanningManager: ScanningManager? = null
// SwitchListener instance for listening to switch events
private lateinit var switchListener: SwitchListener

private var switchListener: SwitchListener? = null
/**
* This function is called when the service is destroyed.
* It shuts down the scanning manager.
*/
override fun onDestroy() {
super.onDestroy()
scanningManager.shutdown()
}

/**
* This method is called when an AccessibilityEvent is fired.
* It updates the keyboard state and finds nodes in the active window.
*/
override fun onAccessibilityEvent(event: AccessibilityEvent?) {
Log.d(TAG, "onAccessibilityEvent: ${event?.eventType}")

val rootNode = rootInActiveWindow
if (rootNode != null) {
rootInActiveWindow?.let { rootNode ->
NodeExaminer.findNodes(rootNode, this)
}

KeyboardInfo.updateKeyboardState(windows)
}

override fun onInterrupt() {
Log.d(TAG, "onInterrupt")
}

/**
* This method is called when the service is interrupted.
* Currently, it does nothing.
*/
override fun onInterrupt() {}

/**
* This method is called when the service is connected.
* It sets up the scanning manager, switch listener, gesture manager, and auto selection handler.
* It also finds nodes in the active window and updates the keyboard state.
*/
override fun onServiceConnected() {
Log.d(TAG, "onServiceConnected")
super.onServiceConnected()

scanningManager = ScanningManager(this, this)
ScanReceiver.preferenceManager = PreferenceManager(this)

scanningManager?.setup()
scanningManager = ScanningManager(this, this)
scanningManager.setup()

switchListener = SwitchListener(this, scanningManager!!)
switchListener = SwitchListener(this, scanningManager)

GestureManager.getInstance().setup(this)

AutoSelectionHandler.init(this)
}

rootInActiveWindow?.let { rootNode ->
NodeExaminer.findNodes(rootNode, this)
}
KeyboardInfo.updateKeyboardState(windows)
}

/**
* This method is called when a key event is fired.
* It handles switch press and release events.
*/
override fun onKeyEvent(event: KeyEvent?): Boolean {
if (event?.action == KeyEvent.ACTION_DOWN) {
return switchListener?.onSwitchPressed(event.keyCode) ?: false
} else if (event?.action == KeyEvent.ACTION_UP) {
return switchListener?.onSwitchReleased(event.keyCode) ?: false
return when (event?.action) {
KeyEvent.ACTION_DOWN -> switchListener.onSwitchPressed(event.keyCode)
KeyEvent.ACTION_UP -> switchListener.onSwitchReleased(event.keyCode)
else -> true
}
return true
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -585,4 +585,9 @@ class CursorManager(private val context: Context) : ScanStateInterface, GestureP
private fun performTapAction() {
GestureManager.getInstance().performTap()
}

fun cleanup() {
cursorUI.reset()
scanningScheduler.shutdown()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,7 @@ class GestureManager {
dragStartPoint = GesturePoint.getPoint()
isDragging = true

ScanReceiver.state = ScanReceiver.ReceiverState.CURSOR
ScanReceiver.setState(ScanReceiver.ReceiverState.CURSOR)

ServiceMessageHUD.instance.showMessage(
"Select where to drag to",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ object GesturePoint {
*/
fun setReselect(reselect: Boolean) {
if (reselect) {
ScanReceiver.state = ScanReceiver.ReceiverState.CURSOR
ScanReceiver.setState(ScanReceiver.ReceiverState.CURSOR)
listener?.onGesturePointReselect()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ class MenuManager {
/**
* The state of the scan receiver when the menu was activated
*/
var scanReceiverState: ScanReceiver.ReceiverState = ScanReceiver.ReceiverState.CURSOR
var scanReceiverState: Int = ScanReceiver.ReceiverState.CURSOR

/**
* The menu hierarchy
Expand All @@ -67,7 +67,7 @@ class MenuManager {
* This function sets the scan receiver state back to the state that activated the menu
*/
fun resetScanReceiverState() {
ScanReceiver.state = scanReceiverState
ScanReceiver.setState(scanReceiverState)
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,41 +11,82 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

/**
* This class is responsible for scanning the nodes
* NodeScanner is a class that handles the scanning of nodes.
* It implements the NodeUpdateDelegate interface.
*
* @property job Job instance used for managing coroutines.
* @property coroutineScope CoroutineScope instance used for launching coroutines.
* @property scanTree ScanTree instance used for managing the scanning process.
* @property nodes List of Node instances that are currently being managed.
*/
class NodeScanner(context: Context) : NodeUpdateDelegate {
class NodeScanner private constructor(context: Context) : NodeUpdateDelegate {
private val job = Job()
private val coroutineScope =
CoroutineScope(Dispatchers.Main + job) // Use Main dispatcher for UI operations.
CoroutineScope(Dispatchers.Main + job)

val scanTree =
ScanTree(context, stopScanningOnSelect = true, individualHighlightingItemsInRow = false)

private var nodes: List<Node> = emptyList()

/**
* Initialization block for the NodeScanner class.
* Sets the nodeUpdateDelegate of the NodeExaminer to this instance and starts the timeout.
*/
init {
NodeExaminer.nodeUpdateDelegate = this
startTimeoutToRevertToCursor()
}

/**
* Starts a timeout that resets the scanTree and changes the state of the ScanReceiver
* if the state is ITEM_SCAN and there are no nodes after 5 seconds.
*/
fun startTimeoutToRevertToCursor() {
coroutineScope.launch {
delay(5000)
if (ScanReceiver.getState() == ScanReceiver.ReceiverState.ITEM_SCAN && this@NodeScanner.nodes.isEmpty()) {
scanTree.reset()
ScanReceiver.setState(ScanReceiver.ReceiverState.CURSOR)
}
}
}

/**
* Updates the nodes and rebuilds the scanTree when new nodes are detected.
* If nodes are present, it cancels any children of the job.
* If no nodes are present, it starts the timeout.
*
* @param nodes List of new Node instances.
*/
override fun onNodesUpdated(nodes: List<Node>) {
this.nodes = nodes

scanTree.buildTree(nodes)

if (nodes.isEmpty()) {
coroutineScope.launch {
delay(5000) // Non-blocking delay for 5 seconds
if (ScanReceiver.state == ScanReceiver.ReceiverState.ITEM_SCAN && this@NodeScanner.nodes.isEmpty()) {
scanTree.reset()
ScanReceiver.state = ScanReceiver.ReceiverState.CURSOR
}
}
if (nodes.isNotEmpty()) {
job.cancelChildren()
} else {
job.cancelChildren() // Cancel any ongoing coroutine tasks if nodes are found
startTimeoutToRevertToCursor()
}
}

fun clear() {
job.cancel() // Cancel all coroutines when the scanner is no longer needed.
/**
* Cleans up the NodeScanner by cancelling the job and shutting down the scanTree.
*/
fun cleanup() {
job.cancel()
scanTree.shutdown()
}

companion object {
@Volatile
private var INSTANCE: NodeScanner? = null

fun getInstance(context: Context): NodeScanner {
return INSTANCE ?: synchronized(this) {
INSTANCE ?: NodeScanner(context).also { INSTANCE = it }
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package com.enaboapps.switchify.service.scanning

import com.enaboapps.switchify.preferences.PreferenceManager

object ScanReceiver {
var state: ReceiverState = ReceiverState.CURSOR
var preferenceManager: PreferenceManager? = null

/**
* This variable is used to determine if the scanning is in the menu
Expand All @@ -11,16 +13,44 @@ object ScanReceiver {
/**
* This enum represents the state of the scanning receiver
*/
enum class ReceiverState {
object ReceiverState {
/**
* This state represents the cursor
*/
CURSOR,
const val CURSOR = 0

/**
* This state represents the item scan
* Sequentially scanning the items on the screen
*/
ITEM_SCAN
const val ITEM_SCAN = 1
}

/**
* This function is used to get the state of the scanning receiver
*/
fun getState(): Int {
preferenceManager?.let { preferenceManager ->
val storedState = preferenceManager.getIntegerValue(
PreferenceManager.PREFERENCE_KEY_SCAN_RECEIVER
)
println("Stored state: $storedState")
return if (storedState == ReceiverState.CURSOR || storedState == ReceiverState.ITEM_SCAN) {
storedState
} else {
ReceiverState.CURSOR
}
}
return ReceiverState.CURSOR
}

/**
* This function is used to set the state of the scanning receiver
*/
fun setState(value: Int) {
preferenceManager?.setIntegerValue(
PreferenceManager.PREFERENCE_KEY_SCAN_RECEIVER,
value
)
}
}
Loading

0 comments on commit 57cef0f

Please sign in to comment.