Skip to content

Commit

Permalink
Merge pull request #16 from Irineu333/release/v0.1.0
Browse files Browse the repository at this point in the history
[Release/v0.1.0] Prototype
  • Loading branch information
Irineu333 authored Mar 9, 2023
2 parents 9d7b6d1 + 0040a09 commit a5d446a
Show file tree
Hide file tree
Showing 50 changed files with 1,039 additions and 12 deletions.
2 changes: 2 additions & 0 deletions .idea/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions .idea/gradle.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 7 additions & 5 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ plugins {
}

android {
namespace 'com.neo.screenreader'
namespace 'com.neo.speaktouch'
compileSdk 33

defaultConfig {
applicationId "com.neo.screenreader"
applicationId "com.neo.speaktouch"
minSdk 22
targetSdk 33
versionCode 1
versionName "1.0"
versionName "0.1.0"

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
Expand All @@ -24,8 +24,8 @@ android {
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = '1.8'
Expand All @@ -37,6 +37,8 @@ dependencies {
implementation 'androidx.core:core-ktx:1.9.0'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.8.0'
implementation 'com.jakewharton.timber:timber:5.0.1'

testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.neo.screenreader
package com.neo.speaktouch

import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
Expand Down
31 changes: 29 additions & 2 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,41 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<queries>
<intent>
<action android:name="android.intent.action.TTS_SERVICE" />
</intent>
</queries>

<application
android:name=".SpeakTouchApplication"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/Theme.ScreenReader"
tools:targetApi="31" />
android:theme="@style/Theme.SpeakTouch"
tools:targetApi="31">

<service
android:name=".service.SpeakTouchService"
android:exported="true"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">

<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService" />

<category android:name="android.accessibilityservice.category.FEEDBACK_AUDIBLE" />
<category android:name="android.accessibilityservice.category.FEEDBACK_HAPTIC" />
<category android:name="android.accessibilityservice.category.FEEDBACK_SPOKEN" />
</intent-filter>

<meta-data
android:name="android.accessibilityservice"
android:resource="@xml/accessibility_service_config" />
</service>

</application>

</manifest>
14 changes: 14 additions & 0 deletions app/src/main/java/com/neo/speaktouch/SpeakTouchApplication.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.neo.speaktouch

import android.app.Application
import timber.log.Timber

class SpeakTouchApplication : Application() {
override fun onCreate() {
super.onCreate()

if (BuildConfig.DEBUG) {
Timber.plant(Timber.DebugTree())
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.neo.speaktouch.intercepter

import android.view.accessibility.AccessibilityEvent
import com.neo.speaktouch.intercepter.interfece.Interceptor
import com.neo.speaktouch.utils.extensions.*
import timber.log.Timber

class FocusInterceptor : Interceptor {

override fun handler(event: AccessibilityEvent) {

val node = NodeInfo.wrap(event.source ?: return)

Timber.d(node.getLog())

if (node.isAccessibilityFocused) return

when (event.eventType) {
AccessibilityEvent.TYPE_VIEW_HOVER_ENTER -> {
Timber.d("event: TYPE_VIEW_HOVER_ENTER")
handlerAccessibilityNode(node)
}

AccessibilityEvent.TYPE_VIEW_FOCUSED -> {
Timber.d("event: TYPE_VIEW_FOCUSED")
handlerAccessibilityNode(node)
}

AccessibilityEvent.TYPE_VIEW_CLICKED -> {
Timber.d("event: TYPE_VIEW_CLICKED")
handlerAccessibilityNode(node)
}

else -> Unit
}
}

private fun handlerAccessibilityNode(node: NodeInfo) {
getFocusableNode(node)?.run {
Timber.i("performAction: $className")
performAction(NodeInfo.ACTION_ACCESSIBILITY_FOCUS)
}
}

private fun getFocusableNode(node: NodeInfo): NodeInfo? {

if (node.isRequiredFocus) {
return node
}

val nearestAncestor = node.getNearestAncestor {
it.isRequiredFocus
}

return nearestAncestor ?: when {
node.isReadable -> node
else -> null
}
}
}
127 changes: 127 additions & 0 deletions app/src/main/java/com/neo/speaktouch/intercepter/SpeechInterceptor.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package com.neo.speaktouch.intercepter

import android.content.Context
import android.speech.tts.TextToSpeech
import android.view.accessibility.AccessibilityEvent
import com.neo.speaktouch.R
import com.neo.speaktouch.intercepter.interfece.Interceptor
import com.neo.speaktouch.model.Type
import com.neo.speaktouch.utils.extensions.*
import timber.log.Timber

class SpeechInterceptor(
private val textToSpeech: TextToSpeech,
private val context: Context
) : Interceptor {

override fun handler(event: AccessibilityEvent) {
if (event.eventType == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED) {
speak(NodeInfo.wrap(event.source ?: return))
}
}

fun speak(text: CharSequence) {

Timber.i("speak: $text")

textToSpeech.speak(
text,
TextToSpeech.QUEUE_FLUSH,
null,
null
)
}

private fun speak(node: NodeInfo) {
speak(getContent(node))
}

fun shutdown() {

Timber.i("shutdown")

textToSpeech.shutdown()
}

private fun getType(
node: NodeInfo
): String {

fun getState() = context.getText(node.isChecked)

return when (Type.get(node)) {
Type.NONE -> ""
Type.IMAGE -> context.getString(R.string.text_image_type)
Type.SWITCH -> context.getString(R.string.text_switch_type, getState())
Type.TOGGLE -> context.getString(R.string.text_toggle_type, getState())
Type.RADIO -> context.getString(R.string.text_radio_type, getState())
Type.CHECKBOX -> context.getString(R.string.text_checkbox_type, getState())
Type.CHECKABLE -> context.getString(R.string.text_checkable_type, getState())
Type.BUTTON -> context.getString(R.string.text_button_type)
Type.EDITABLE -> context.getString(R.string.text_editable_type)
Type.OPTIONS -> context.getString(R.string.text_options_type)
Type.LIST -> context.getString(R.string.text_list_type)
Type.TITLE -> context.getString(R.string.text_title_type)
}
}

private fun getChildrenContent(
node: NodeInfo
): String {
return buildList {
for (index in 0 until node.childCount) {
val nodeChild = node.getChild(index)

if (nodeChild.isAvailableForAccessibility) {
add(getContent(nodeChild))
}
}
}.joinToString(", ")
}

private fun getContent(
node: NodeInfo
) = with(node) {
Timber.d("getContent\n${node.getLog()}")

val content = contentDescription.ifEmptyOrNull {
text.ifEmptyOrNull {
hintText.ifEmptyOrNull {
getChildrenContent(node)
}
}
}

listOf(
content,
getType(node),
hintText?.takeIf { it != content },
error
).filterNotNullOrEmpty()
.joinToString(", ")
}

companion object {

fun getInstance(context: Context): SpeechInterceptor {

var speechInterceptor: SpeechInterceptor? = null

speechInterceptor = SpeechInterceptor(
textToSpeech = TextToSpeech(context) { status ->
if (status == TextToSpeech.SUCCESS) {
speechInterceptor!!.speak(
"Speak Touch ${context.getText(true)}"
)
} else {
error(message = "TTS_INITIALIZATION_ERROR")
}
},
context = context
)

return speechInterceptor
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.neo.speaktouch.intercepter.interfece

import android.view.accessibility.AccessibilityEvent

interface Interceptor {
fun handler(event: AccessibilityEvent)
}
76 changes: 76 additions & 0 deletions app/src/main/java/com/neo/speaktouch/model/Type.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package com.neo.speaktouch.model

import android.widget.*
import com.neo.speaktouch.utils.extensions.NodeInfo
import com.neo.speaktouch.utils.extensions.instanceOf

enum class Type {
NONE,
IMAGE,
SWITCH,
TOGGLE,
RADIO,
CHECKBOX,
CHECKABLE,
BUTTON,
EDITABLE,
OPTIONS,
LIST,
TITLE;

companion object {
fun get(node: NodeInfo): Type {
val className = node.className ?: return NONE

/* ImageView */

// View->ImageView->ImageButton
if (className instanceOf ImageButton::class.java) return BUTTON

// View->ImageView
if (className instanceOf ImageView::class.java) return IMAGE

/* TextView */

// View->TextView->EditText
if (className instanceOf EditText::class.java) return EDITABLE

/* Button */

// View->TextView->Button->CompoundButton->Switch
if (className instanceOf Switch::class.java) return SWITCH

// View->TextView->Button->CompoundButton->ToggleButton
if (className instanceOf ToggleButton::class.java) return TOGGLE

// View->TextView->Button->CompoundButton->RadioButton
if (className instanceOf RadioButton::class.java) return RADIO

// View->TextView->Button->CompoundButton->CheckBox
if (className instanceOf CheckBox::class.java) return CHECKBOX

// View->TextView->Button
if (className instanceOf Button::class.java) return BUTTON

/* AdapterView */

// View->ViewGroup->AdapterView->AbsListView
if (className instanceOf AbsListView::class.java) return LIST

// View->ViewGroup->AdapterView->AbsSpinner
if (className instanceOf AbsSpinner::class.java) return OPTIONS

/* Additional */

if (node.isCheckable) return CHECKABLE

if (node.isEditable) return EDITABLE

if (node.collectionInfo != null) return LIST

if (node.isHeading) return TITLE

return NONE
}
}
}
Loading

0 comments on commit a5d446a

Please sign in to comment.