Accessibility
Learn Android Accessibility via these examples.
Example 1: Kotlin Android AccessibilityService for Clock Application Example
This tutorial will teach you how to create and implement an AccessibilityService
that provides custom feedback for the Clock application that comes by default with Android devices.
It demonstrates the following key features of the Android accessibility APIs:
- Simple demonstration of how to use the accessibility APIs.
- Hands-on example of various ways to utilize the accessibility API for providing alternative and complementary feedback.
- Providing application specific feedback the service handles only accessibility events from the clock application.
- Providing dynamic, context-dependent feedback feedback type changes depending on the ringer state.
Step 1: Dependencies
No special dependencies are needed.
Step 2: Layouts
Design your layout as below:
accessibility_service.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/accessibility_service_instructions"
/>
<ImageButton android:id="@+id/button"
android:background="@drawable/ic_launcher_settings"
android:layout_width="32dip"
android:layout_height="32dip"
android:layout_marginTop="50dip"
android:layout_gravity="center"
android:scaleType="fitCenter"
android:adjustViewBounds="true" />
</LinearLayout>
Step 3: Create the Accessibility Service
Start by adding imports:
import com.example.android.apis.R
import android.accessibilityservice.AccessibilityService
import android.accessibilityservice.AccessibilityServiceInfo
import android.annotation.SuppressLint
import android.annotation.TargetApi
import android.app.Service
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.media.AudioManager
import android.os.Build
import android.os.Handler
import android.os.Message
import android.os.Vibrator
import android.speech.tts.TextToSpeech
import android.util.Log
import android.util.SparseArray
import android.view.accessibility.AccessibilityEvent
Create a class that extends the AccessibilityService
:
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
class ClockBackService : AccessibilityService() {
Define insatance fields:
// Sound pool related member fields.
/**
* Mapping from integers to earcon names - dynamically populated.
*/
private val mEarconNames = SparseArray<String>()
// Auxiliary fields.
/**
* Handle to this service to enable inner classes to access the `Context`.
*/
internal lateinit var mContext: Context
/**
* The feedback this service is currently providing.
*/
internal var mProvidedFeedbackType: Int = 0
/**
* Reusable instance for building utterances.
*/
private val mUtterance = StringBuilder()
// Feedback providing services.
/**
* The `TextToSpeech` used for speaking.
*/
private var mTts: TextToSpeech? = null
/**
* The `AudioManager` for detecting ringer state.
*/
private var mAudioManager: AudioManager? = null
/**
* Vibrator for providing haptic feedback.
*/
private var mVibrator: Vibrator? = null
/**
* Flag if the infrastructure is initialized.
*/
private var isInfrastructureInitialized: Boolean = false
/**
* `Handler` for executing messages on the service main thread.
*/
@Suppress("DEPRECATION")
@SuppressLint("HandlerLeak")
internal var mHandler: Handler = object : Handler() {
We implement this to receive messages. We switch on the what
field of our parameter Message message
:
-
MESSAGE_SPEAK - we initialize
String utterance
with theobj
field ofmessage
(cast toString
), call thespeak
method of our fieldTextToSpeech mTts
to speakutterance
using the queuing strategy QUEUING_MODE_INTERRUPT, then return- MESSAGE_STOP_SPEAK - we call the
stop
method of our fieldTextToSpeech mTts
to interrupt the current utterance and discard all utterances in the queue, then return. - MESSAGE_START_TTS - we initialize our field
TextToSpeech mTts
with a new instance using an anonymous class for theTextToSpeech.OnInitListener
which just registers us as a broadcast receiver, and we then return.
- MESSAGE_STOP_SPEAK - we call the
-
MESSAGE_SHUTDOWN_TTS - we call the
shutdown
method ofmTts
and return. -
MESSAGE_PLAY_EARCON - we initialize
int resourceId
with thearg1
field of our argumentmessage
, call our methodplayEarcon
with it to play the earcon with that id (an earcon is a brief, distinctive sound used to represent a specific event or convey other information), then we return. -
MESSAGE_STOP_PLAY_EARCON - we call the
stop
method ofmTts
and return. -
MESSAGE_VIBRATE - we initialize
int key
with thearg1
field of our argumentmessage
, initializelong[] pattern
with the array stored at positionkey
inSparseArray<long[]> sVibrationPatterns
, and if that is not null we call thevibrate
method ofVibrator mVibrator
with that pattern. In either case we return to our caller. -
MESSAGE_STOP_VIBRATE - we call the
cancel
method ofVibrator mVibrator
and return. -
@param message
- AMessage
object
override fun handleMessage(message: Message) {
when (message.what) {
MESSAGE_SPEAK -> {
val utterance = message.obj as String
mTts!!.speak(utterance, QUEUING_MODE_INTERRUPT, null)
return
}
MESSAGE_STOP_SPEAK -> {
mTts!!.stop()
return
}
MESSAGE_START_TTS -> {
/**
* Called to signal the completion of the TextToSpeech engine initialization.
*
* parameter `it` is [TextToSpeech.SUCCESS] or [TextToSpeech.ERROR].
*/
mTts = TextToSpeech(mContext, TextToSpeech.OnInitListener {
// Register here since to add earcons the TTS must be initialized and
// the receiver is called immediately with the current ringer mode.
registerBroadCastReceiver()
})
return
}
MESSAGE_SHUTDOWN_TTS -> {
mTts!!.shutdown()
return
}
MESSAGE_PLAY_EARCON -> {
val resourceId = message.arg1
playEarcon(resourceId)
return
}
MESSAGE_STOP_PLAY_EARCON -> {
mTts!!.stop()
return
}
MESSAGE_VIBRATE -> {
val key = message.arg1
val pattern = sVibrationPatterns.get(key)
if (pattern != null) {
mVibrator!!.vibrate(pattern, -1)
}
return
}
MESSAGE_STOP_VIBRATE -> {
mVibrator!!.cancel()
return
}
}
}
}
BroadcastReceiver
for receiving updates for the actions ACTION_SCREEN_ON
, ACTION_SCREEN_OFF
and AudioManager.RINGER_MODE_CHANGED_ACTION
.
This method is called when the BroadcastReceiver is receiving an Intent broadcast. We initialize String action
with the action of our argument Intent intent
.
If action
is equal to AudioManager.RINGER_MODE_CHANGED_ACTION we initialize int ringerMode
with the extra in intent
stored under the key AudioManager.EXTRA_RINGER_MODE (defaulting to RINGER_MODE_NORMAL) and call our method configureForRingerMode
with ringerMode
to configure our feedback to the new ringer mode. Else if action
equals ACTION_SCREEN_ON, we call our method provideScreenStateChangeFeedback
to provide feedback to announce the screen state change to INDEX_SCREEN_ON. Else if action
equals ACTION_SCREEN_OFF, we call our method provideScreenStateChangeFeedback
to provide feedback to announce the screen state change to INDEX_SCREEN_OFF. If action
is not any of the above we just log a message stating that we do not handle action
.
@param context
- The Context in which the receiver is running.@param intent
- The Intent being received.
override fun onReceive(context: Context, intent: Intent) {
when (val action = intent.action) {
AudioManager.RINGER_MODE_CHANGED_ACTION -> {
val ringerMode = intent.getIntExtra(AudioManager.EXTRA_RINGER_MODE,
AudioManager.RINGER_MODE_NORMAL)
configureForRingerMode(ringerMode)
}
Intent.ACTION_SCREEN_ON -> provideScreenStateChangeFeedback(INDEX_SCREEN_ON)
Intent.ACTION_SCREEN_OFF -> provideScreenStateChangeFeedback(INDEX_SCREEN_OFF)
else -> Log.w(LOG_TAG, "Registered for but not handling action " + action!!)
}
}
Provides feedback to announce the screen state change. Such a change is turning the screen on or off. We switch on our field int mProvidedFeedbackType
:
-
FEEDBACK_SPOKEN - we initialize
String utterance
with the string returned by our methodgenerateScreenOnOrOffUtternace
for our argumentfeedbackIndex
, useHandler mHandler
to load aMessage
with thewhat
field of MESSAGE_SPEAK, and theobj
field ofutterance
then send thisMessage
toHandler mHandler
, and return to our caller. -
FEEDBACK_AUDIBLE - we use
Handler mHandler
to load aMessage
with thewhat
field of MESSAGE_PLAY_EARCON, thearg1
field offeedbackIndex
, and thearg2
field of 0, then send thisMessage
toHandler mHandler
, and return to our caller. -
FEEDBACK_HAPTIC - we use
Handler mHandler
to load aMessage
with thewhat
field of MESSAGE_VIBRATE, thearg1
field offeedbackIndex
, and thearg2
field of 0, then send thisMessage
toHandler mHandler
, and return to our caller. -
default - we throw an IllegalStateException.
-
@param feedbackIndex
- The index of the feedback in the statically mapped feedback resources.
private fun provideScreenStateChangeFeedback(feedbackIndex: Int) {
// We take a specific action depending on the feedback we currently provide.
when (mProvidedFeedbackType) {
AccessibilityServiceInfo.FEEDBACK_SPOKEN -> {
val utterance = generateScreenOnOrOffUtternace(feedbackIndex)
mHandler.obtainMessage(MESSAGE_SPEAK, utterance).sendToTarget()
return
}
AccessibilityServiceInfo.FEEDBACK_AUDIBLE -> {
mHandler.obtainMessage(MESSAGE_PLAY_EARCON, feedbackIndex, 0).sendToTarget()
return
}
AccessibilityServiceInfo.FEEDBACK_HAPTIC -> {
mHandler.obtainMessage(MESSAGE_VIBRATE, feedbackIndex, 0).sendToTarget()
return
}
else -> throw IllegalStateException("Unexpected feedback type $mProvidedFeedbackType")
}
}
}
This method is a part of the AccessibilityService
lifecycle and is called after the system has successfully bound to the service. First we check if we have already initialized our Infrastructure (our flag isInfrastructureInitialized
is true) and if so we return having done nothing. We initialize our field Context mContext
to "this", then send the empty message MESSAGE_START_TTS to Handler mHandler
to start the Text to speech service running. We initialize Vibrator mVibrator
with an instance of the system level service VIBRATOR_SERVICE, and AudioManager mAudioManager
with an instance of the service AUDIO_SERVICE. We use mAudioManager
to fetch the current ringer mode to int ringerMode
and call our method configureForRingerMode
with it to configure the types of feedback which are appropriate for the ringer mode. Finally we set our flag boolean isInfrastructureInitialized
to true.
public override fun onServiceConnected() {
if (isInfrastructureInitialized) {
return
}
mContext = this
// Send a message to start the TTS.
mHandler.sendEmptyMessage(MESSAGE_START_TTS)
// Get the vibrator service.
mVibrator = getSystemService(Service.VIBRATOR_SERVICE) as Vibrator
// Get the AudioManager and configure according the current ring mode.
mAudioManager = getSystemService(Service.AUDIO_SERVICE) as AudioManager
// In Froyo the broadcast receiver for the ringer mode is called back with the
// current state upon registering but in Eclair this is not done so we poll here.
val ringerMode = mAudioManager!!.ringerMode
configureForRingerMode(ringerMode)
// We are in an initialized state now.
isInfrastructureInitialized = true
}
Called when all clients have disconnected from a particular interface published by the service. If our flag boolean isInfrastructureInitialized
is true we send the empty message MESSAGE_SHUTDOWN_TTS to our Handler mHandler
to shut down the text to speech service, and if BroadcastReceiver mBroadcastReceiver
is not null we unregister it. We then set isInfrastructureInitialized
to false. In either case we return false to the caller.
@param intent
- The Intent that was used to bind to this service @return Return true if you would like to have the service's
override fun onUnbind(intent: Intent): Boolean {
if (isInfrastructureInitialized) {
// Stop the TTS service.
mHandler.sendEmptyMessage(MESSAGE_SHUTDOWN_TTS)
// Unregister the intent broadcast receiver.
@Suppress("SENSELESS_COMPARISON")
if (mBroadcastReceiver != null) {
unregisterReceiver(mBroadcastReceiver)
}
// We are not in an initialized state anymore.
isInfrastructureInitialized = false
}
return false
}
Registers the phone state observing broadcast receiver. We initialize IntentFilter filter
with a new instance, add the actions RINGER_MODE_CHANGED_ACTION, ACTION_SCREEN_ON, and ACTION_SCREEN_OFF to it then register BroadcastReceiver mBroadcastReceiver
to receive broadcast intents matching filter
.
private fun registerBroadCastReceiver() {
// Create a filter with the broadcast intents we are interested in.
val filter = IntentFilter()
filter.addAction(AudioManager.RINGER_MODE_CHANGED_ACTION)
filter.addAction(Intent.ACTION_SCREEN_ON)
filter.addAction(Intent.ACTION_SCREEN_OFF)
// Register for broadcasts of interest.
registerReceiver(mBroadcastReceiver, filter, null, null)
}
Generates an utterance for announcing screen on and screen off. If our argument feedbackIndex
is INDEX_SCREEN_ON We initialize our variable int resourceId
to the resource id R.string.template_screen_on ("Screen on. Volume %1$s percent."), otherwise we initialize it to the resource id R.string.template_screen_off ("Screen off. Volume %1$s percent."). We fetch the string that resourceId
references to String template
. We set int currentRingerVolume
to the volume of the stream STREAM_RING that mAudioManager
returns when queried, and int maxRingerVolume
to the maximum value for that stream.
We calculate int volumePercent
to be 100 divided by maxRingerVolume
times currentRingerVolume
(the current volume 0 to 100 percent). We do some math tricks to round volumePercent
to the nearest 5 percent then we return the string formed by formatting volumePercent
using the format in template
.
@param feedbackIndex
- The feedback index for looking up feedback value.@return
- The utterance.
private fun generateScreenOnOrOffUtternace(feedbackIndex: Int): String {
// Get the announce template.
val resourceId = if (feedbackIndex == INDEX_SCREEN_ON)
R.string.template_screen_on
else
R.string.template_screen_off
val template = mContext.getString(resourceId)
// Format the template with the ringer percentage.
val currentRingerVolume = mAudioManager!!.getStreamVolume(AudioManager.STREAM_RING)
val maxRingerVolume = mAudioManager!!.getStreamMaxVolume(AudioManager.STREAM_RING)
var volumePercent = 100 / maxRingerVolume * currentRingerVolume
// Let us round to five so it sounds better.
val adjustment = volumePercent % 10
if (adjustment < 5) {
volumePercent -= adjustment
} else if (adjustment > 5) {
volumePercent += 10 - adjustment
}
return String.format(template, volumePercent)
}
Configures the service according to a ringer mode. Possible configurations:
AudioManager#RINGER_MODE_SILENT
Goal: Provide only custom haptic feedback. Approach: Take over the haptic feedback by configuring this service to provide such and do so. This way the system will not call the default haptic feedback service KickBack. Take over the audible and spoken feedback by configuring this service to provide such feedback but not doing so. This way the system will not call the default spoken feedback service TalkBack and the default audible feedback service SoundBack.
AudioManager#RINGER_MODE_VIBRATE
Goal: Provide custom audible and default haptic feedback. Approach: Take over the audible feedback and provide custom one. Take over the spoken feedback but do not provide such. Let some other service provide haptic feedback (KickBack).
AudioManager#RINGER_MODE_NORMAL
Goal: Provide custom spoken, default audible and default haptic feedback. Approach: Take over the spoken feedback and provide custom one. Let some other services provide audible feedback (SoundBack) and haptic feedback (KickBack).
@param ringerMode
- The device ringer mode.
private fun configureForRingerMode(ringerMode: Int) {
when (ringerMode) {
AudioManager.RINGER_MODE_SILENT -> {
// When the ringer is silent we want to provide only haptic feedback.
mProvidedFeedbackType = AccessibilityServiceInfo.FEEDBACK_HAPTIC
// Take over the spoken and sound feedback so no such feedback is provided.
setServiceInfo(AccessibilityServiceInfo.FEEDBACK_HAPTIC
or AccessibilityServiceInfo.FEEDBACK_SPOKEN
or AccessibilityServiceInfo.FEEDBACK_AUDIBLE)
// Use only an earcon to announce ringer state change.
mHandler.obtainMessage(MESSAGE_PLAY_EARCON, INDEX_RINGER_SILENT, 0).sendToTarget()
}
AudioManager.RINGER_MODE_VIBRATE -> {
// When the ringer is vibrating we want to provide only audible feedback.
mProvidedFeedbackType = AccessibilityServiceInfo.FEEDBACK_AUDIBLE
// Take over the spoken feedback so no spoken feedback is provided.
setServiceInfo(AccessibilityServiceInfo.FEEDBACK_AUDIBLE or AccessibilityServiceInfo.FEEDBACK_SPOKEN)
// Use only an earcon to announce ringer state change.
mHandler.obtainMessage(MESSAGE_PLAY_EARCON, INDEX_RINGER_VIBRATE, 0).sendToTarget()
}
AudioManager.RINGER_MODE_NORMAL -> {
// When the ringer is ringing we want to provide spoken feedback
// overriding the default spoken feedback.
mProvidedFeedbackType = AccessibilityServiceInfo.FEEDBACK_SPOKEN
setServiceInfo(AccessibilityServiceInfo.FEEDBACK_SPOKEN)
// Use only an earcon to announce ringer state change.
mHandler.obtainMessage(MESSAGE_PLAY_EARCON, INDEX_RINGER_NORMAL, 0).sendToTarget()
}
}
}
Sets the AccessibilityServiceInfo
which informs the system how to handle this AccessibilityService
. We initialize AccessibilityServiceInfo info
with a new instance, set its eventTypes
field (the event types an AccessibilityService we are interested in) to TYPES_ALL_MASK (Mask for all types of AccessibilityEvent), and set its feedbackType
field (the type of feedback we want to provide) to our parameter int feedbackType
, the notificationTimeout
field (the timeout after the most recent event of a given type before an AccessibilityService is notified) to our constant EVENT_NOTIFICATION_TIMEOUT_MILLIS (80ms), and its packageNames
field (the package names we are interested in) to our array String[] PACKAGE_NAMES
(which contains the package names of the standard Android clock packages). Finally we call the setServiceInfo
method with info
as its argument to set the AccessibilityServiceInfo that describes this service.
@param feedbackType
- The type of feedback this service will provide. Note: The feedbackType parameter is an bitwise or of all feedback types this service would like to provide.
private fun setServiceInfo(feedbackType: Int) {
val info = AccessibilityServiceInfo()
// We are interested in all types of accessibility events.
info.eventTypes = AccessibilityEvent.TYPES_ALL_MASK
// We want to provide specific type of feedback.
info.feedbackType = feedbackType
// We want to receive events in a certain interval.
info.notificationTimeout = EVENT_NOTIFICATION_TIMEOUT_MILLIS.toLong()
// We want to receive accessibility events only from certain packages.
info.packageNames = PACKAGE_NAMES
serviceInfo = info
}
Callback for AccessibilityEvent
s. First we log the string value of our argument AccessibilityEvent event
. If our field mProvidedFeedbackType
is equal to FEEDBACK_SPOKEN, we obtain and send a Message
to Handler mHandler
with a what
field of MESSAGE_SPEAK, and the String
generated by our method formatUtterance
for event
. Else if field mProvidedFeedbackType
is equal to FEEDBACK_AUDIBLE, we obtain and send a Message
to Handler mHandler
with a what
field of MESSAGE_PLAY_EARCON, the event type of event
as the arg1
field and 0 for the arg2
field. Else if field mProvidedFeedbackType
is equal to FEEDBACK_HAPTIC, we obtain and send a Message
to Handler mHandler
with a what
field of MESSAGE_VIBRATE, the event type of event
as the arg1
field and 0 for the arg2
field. If the mProvidedFeedbackType
is none of the above we throw an IllegalStateException: "Unexpected feedback type ".
@param event
- An event.
override fun onAccessibilityEvent(event: AccessibilityEvent) {
Log.i(LOG_TAG, "$mProvidedFeedbackType $event")
// Here we act according to the feedback type we are currently providing.
when (mProvidedFeedbackType) {
AccessibilityServiceInfo.FEEDBACK_SPOKEN ->
mHandler.obtainMessage(MESSAGE_SPEAK, formatUtterance(event)).sendToTarget()
AccessibilityServiceInfo.FEEDBACK_AUDIBLE ->
mHandler.obtainMessage(MESSAGE_PLAY_EARCON, event.eventType, 0).sendToTarget()
AccessibilityServiceInfo.FEEDBACK_HAPTIC ->
mHandler.obtainMessage(MESSAGE_VIBRATE, event.eventType, 0).sendToTarget()
else -> throw IllegalStateException("Unexpected feedback type $mProvidedFeedbackType")
}
}
Callback for interrupting the accessibility feedback. If our field mProvidedFeedbackType
is equal to FEEDBACK_SPOKEN, we obtain and send a Message
to Handler mHandler
with a what
field of MESSAGE_STOP_SPEAK. Else if our field mProvidedFeedbackType
is equal to FEEDBACK_AUDIBLE, we obtain and send a Message
to Handler mHandler
with a what
field of MESSAGE_STOP_PLAY_EARCON. Else if our field mProvidedFeedbackType
is equal to FEEDBACK_HAPTIC, we obtain and send a Message
to Handler mHandler
with a what
field of MESSAGE_STOP_VIBRATE. If the mProvidedFeedbackType
is none of the above we throw an IllegalStateException: "Unexpected feedback type ".
override fun onInterrupt() {
// Here we act according to the feedback type we are currently providing.
when (mProvidedFeedbackType) {
AccessibilityServiceInfo.FEEDBACK_SPOKEN ->
mHandler.obtainMessage(MESSAGE_STOP_SPEAK).sendToTarget()
AccessibilityServiceInfo.FEEDBACK_AUDIBLE ->
mHandler.obtainMessage(MESSAGE_STOP_PLAY_EARCON).sendToTarget()
AccessibilityServiceInfo.FEEDBACK_HAPTIC ->
mHandler.obtainMessage(MESSAGE_STOP_VIBRATE).sendToTarget()
else -> throw IllegalStateException("Unexpected feedback type $mProvidedFeedbackType")
}
}
Formats an utterance from an AccessibilityEvent
. We initialize StringBuilder utterance
by copying the reference from our field StringBuilder mUtterance
, and clear it by setting its length to 0. We initialize List<CharSequence> eventText
by retrieving the text of the AccessibilityEvent event
. If eventText
is not empty we loop through all the CharSequence subText
in eventText
removing the character '0' if it is the first character, appending subText
to utterance
followed by a SPACE character. When we are done appending all the CharSequence
into utterance
we return the string value of utterance
to the caller. If there is no event text in event
we initialize CharSequence contentDescription
with the content description of the source, and if it is not null we append it to utterance
and return the string value of utterance
to the caller. If there is not event text and no content description in event
we just return the string value of utterance
to the caller.
@param event
- The event from which to format an utterance.@return
- The formatted utterance.
private fun formatUtterance(event: AccessibilityEvent): String {
val utterance = mUtterance
// Clear the utterance before appending the formatted text.
utterance.setLength(0)
val eventText = event.text
// We try to get the event text if such.
if (eventText.isNotEmpty()) {
for (subText in eventText) {
var subTextVar = subText
// Make 01 pronounced as 1
if (subTextVar[0] == '0') {
subTextVar = subTextVar.subSequence(1, subText.length)
}
utterance.append(subTextVar)
utterance.append(SPACE)
}
return utterance.toString()
}
// There is no event text but we try to get the content description which is
// an optional attribute for describing a view (typically used with ImageView).
val contentDescription = event.contentDescription
if (contentDescription != null) {
utterance.append(contentDescription)
return utterance.toString()
}
return utterance.toString()
}
Plays an earcon given its id. We set String earconName
to the string in our field SparseArray<String> mEarconNames
at index earconId
. If earconName
is null we do not know the sound id, hence we need to load the sound. We initialize Integer resourceId
with the resource id at index earconId
in our field SparseArray<Integer> sSoundsResourceIds
. If resourceId
is not equal to null, we set earconName
to the string formed by surrounding the string value of earconId
with '[' and ']' characters, call the addEarcon
method of TextToSpeech mTts
to add a mapping between the string of text earconName
, our package name, and the sound resource resourceId
. We then store earconName
under the key earconId
in SparseArray<String> mEarconNames
. Finally we call the playEarcon
method of mTts
to play the Earcon earconName
using QUEUING_MODE_INTERRUPT as the queueing mode.
@param earconId
- The id of the earcon to be played.
private fun playEarcon(earconId: Int) {
var earconName: String? = mEarconNames.get(earconId)
if (earconName == null) {
// We do not know the sound id, hence we need to load the sound.
val resourceId = sSoundsResourceIds.get(earconId)
if (resourceId != null) {
earconName = "[$earconId]"
mTts!!.addEarcon(earconName, packageName, resourceId)
mEarconNames.put(earconId, earconName)
}
}
@Suppress("DEPRECATION")
mTts!!.playEarcon(earconName, QUEUING_MODE_INTERRUPT, null)
}
companion object {
/**
* Tag for logging from this service.
*/
private const val LOG_TAG = "ClockBackService"
// Fields for configuring how the system handles this accessibility service.
/**
* Minimal timeout between accessibility events we want to receive.
*/
private const val EVENT_NOTIFICATION_TIMEOUT_MILLIS = 80
/**
* Packages we are interested in.
*
*
* **
* Note: This code sample will work only on devices shipped with the
* default Clock application.
** *
*
* This works with AlarmClock and Clock whose package name changes in different releases
*/
private val PACKAGE_NAMES = arrayOf(
"com.android.alarmclock",
"com.google.android.deskclock",
"com.android.deskclock"
)
// Message types we are passing around.
/**
* Speak.
*/
private const val MESSAGE_SPEAK = 1
/**
* Stop speaking.
*/
private const val MESSAGE_STOP_SPEAK = 2
/**
* Start the TTS service.
*/
private const val MESSAGE_START_TTS = 3
/**
* Stop the TTS service.
*/
private const val MESSAGE_SHUTDOWN_TTS = 4
/**
* Play an earcon.
*/
private const val MESSAGE_PLAY_EARCON = 5
/**
* Stop playing an earcon.
*/
private const val MESSAGE_STOP_PLAY_EARCON = 6
/**
* Vibrate a pattern.
*/
private const val MESSAGE_VIBRATE = 7
/**
* Stop vibrating.
*/
private const val MESSAGE_STOP_VIBRATE = 8
// Screen state broadcast related constants.
/**
* Feedback mapping index used as a key for the screen-on broadcast.
*/
private const val INDEX_SCREEN_ON = 0x00000100
/**
* Feedback mapping index used as a key for the screen-off broadcast.
*/
private const val INDEX_SCREEN_OFF = 0x00000200
// Ringer mode change related constants.
/**
* Feedback mapping index used as a key for normal ringer mode.
*/
private const val INDEX_RINGER_NORMAL = 0x00000400
/**
* Feedback mapping index used as a key for vibration ringer mode.
*/
private const val INDEX_RINGER_VIBRATE = 0x00000800
/**
* Feedback mapping index used as a key for silent ringer mode.
*/
private const val INDEX_RINGER_SILENT = 0x00001000
// Speech related constants.
/**
* The queuing mode we are using - interrupt a spoken utterance before
* speaking another one.
*/
private const val QUEUING_MODE_INTERRUPT = 2
/**
* The space string constant.
*/
private const val SPACE = " "
/**
* Mapping from integers to vibration patterns for haptic feedback.
*/
private val sVibrationPatterns = SparseArray<LongArray>()
/**
* Initializes our `sVibrationPatterns` `SparseArray`.
*/
init {
sVibrationPatterns.put(AccessibilityEvent.TYPE_VIEW_CLICKED, longArrayOf(0L, 100L))
sVibrationPatterns.put(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED, longArrayOf(0L, 100L))
sVibrationPatterns.put(AccessibilityEvent.TYPE_VIEW_SELECTED, longArrayOf(0L, 15L, 10L, 15L))
sVibrationPatterns.put(AccessibilityEvent.TYPE_VIEW_FOCUSED, longArrayOf(0L, 15L, 10L, 15L))
sVibrationPatterns.put(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED, longArrayOf(0L, 25L, 50L, 25L, 50L, 25L))
sVibrationPatterns.put(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER, longArrayOf(0L, 15L, 10L, 15L, 15L, 10L))
sVibrationPatterns.put(INDEX_SCREEN_ON, longArrayOf(0L, 10L, 10L, 20L, 20L, 30L))
sVibrationPatterns.put(INDEX_SCREEN_OFF, longArrayOf(0L, 30L, 20L, 20L, 10L, 10L))
}
/**
* Mapping from integers to raw sound resource ids.
*/
@SuppressLint("UseSparseArrays")
private val sSoundsResourceIds = SparseArray<Int>()
/**
* Initializes our `sSoundsResourceIds` `SparseArray`.
*/
init {
sSoundsResourceIds.put(AccessibilityEvent.TYPE_VIEW_CLICKED, R.raw.sound_view_clicked)
sSoundsResourceIds.put(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED, R.raw.sound_view_clicked)
sSoundsResourceIds.put(AccessibilityEvent.TYPE_VIEW_SELECTED, R.raw.sound_view_focused_or_selected)
sSoundsResourceIds.put(AccessibilityEvent.TYPE_VIEW_FOCUSED, R.raw.sound_view_focused_or_selected)
sSoundsResourceIds.put(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED, R.raw.sound_window_state_changed)
sSoundsResourceIds.put(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER, R.raw.sound_view_hover_enter)
sSoundsResourceIds.put(INDEX_SCREEN_ON, R.raw.sound_screen_on)
sSoundsResourceIds.put(INDEX_SCREEN_OFF, R.raw.sound_screen_off)
sSoundsResourceIds.put(INDEX_RINGER_SILENT, R.raw.sound_ringer_silent)
sSoundsResourceIds.put(INDEX_RINGER_VIBRATE, R.raw.sound_ringer_vibrate)
sSoundsResourceIds.put(INDEX_RINGER_NORMAL, R.raw.sound_ringer_normal)
}
}
}
Here is the full code:
TaskBackService.kt
import com.example.android.apis.R
import android.accessibilityservice.AccessibilityService
import android.accessibilityservice.AccessibilityServiceInfo
import android.annotation.SuppressLint
import android.annotation.TargetApi
import android.app.Service
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.media.AudioManager
import android.os.Build
import android.os.Handler
import android.os.Message
import android.os.Vibrator
import android.speech.tts.TextToSpeech
import android.util.Log
import android.util.SparseArray
import android.view.accessibility.AccessibilityEvent
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
class ClockBackService : AccessibilityService() {
// Sound pool related member fields.
/**
* Mapping from integers to earcon names - dynamically populated.
*/
private val mEarconNames = SparseArray<String>()
// Auxiliary fields.
/**
* Handle to this service to enable inner classes to access the `Context`.
*/
internal lateinit var mContext: Context
/**
* The feedback this service is currently providing.
*/
internal var mProvidedFeedbackType: Int = 0
/**
* Reusable instance for building utterances.
*/
private val mUtterance = StringBuilder()
// Feedback providing services.
/**
* The `TextToSpeech` used for speaking.
*/
private var mTts: TextToSpeech? = null
/**
* The `AudioManager` for detecting ringer state.
*/
private var mAudioManager: AudioManager? = null
/**
* Vibrator for providing haptic feedback.
*/
private var mVibrator: Vibrator? = null
/**
* Flag if the infrastructure is initialized.
*/
private var isInfrastructureInitialized: Boolean = false
/**
* `Handler` for executing messages on the service main thread.
*/
@Suppress("DEPRECATION")
@SuppressLint("HandlerLeak")
internal var mHandler: Handler = object : Handler() {
override fun handleMessage(message: Message) {
when (message.what) {
MESSAGE_SPEAK -> {
val utterance = message.obj as String
mTts!!.speak(utterance, QUEUING_MODE_INTERRUPT, null)
return
}
MESSAGE_STOP_SPEAK -> {
mTts!!.stop()
return
}
MESSAGE_START_TTS -> {
/**
* Called to signal the completion of the TextToSpeech engine initialization.
*
* parameter `it` is [TextToSpeech.SUCCESS] or [TextToSpeech.ERROR].
*/
mTts = TextToSpeech(mContext, TextToSpeech.OnInitListener {
// Register here since to add earcons the TTS must be initialized and
// the receiver is called immediately with the current ringer mode.
registerBroadCastReceiver()
})
return
}
MESSAGE_SHUTDOWN_TTS -> {
mTts!!.shutdown()
return
}
MESSAGE_PLAY_EARCON -> {
val resourceId = message.arg1
playEarcon(resourceId)
return
}
MESSAGE_STOP_PLAY_EARCON -> {
mTts!!.stop()
return
}
MESSAGE_VIBRATE -> {
val key = message.arg1
val pattern = sVibrationPatterns.get(key)
if (pattern != null) {
mVibrator!!.vibrate(pattern, -1)
}
return
}
MESSAGE_STOP_VIBRATE -> {
mVibrator!!.cancel()
return
}
}
}
}
private val mBroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
when (val action = intent.action) {
AudioManager.RINGER_MODE_CHANGED_ACTION -> {
val ringerMode = intent.getIntExtra(AudioManager.EXTRA_RINGER_MODE,
AudioManager.RINGER_MODE_NORMAL)
configureForRingerMode(ringerMode)
}
Intent.ACTION_SCREEN_ON -> provideScreenStateChangeFeedback(INDEX_SCREEN_ON)
Intent.ACTION_SCREEN_OFF -> provideScreenStateChangeFeedback(INDEX_SCREEN_OFF)
else -> Log.w(LOG_TAG, "Registered for but not handling action " + action!!)
}
}
private fun provideScreenStateChangeFeedback(feedbackIndex: Int) {
// We take a specific action depending on the feedback we currently provide.
when (mProvidedFeedbackType) {
AccessibilityServiceInfo.FEEDBACK_SPOKEN -> {
val utterance = generateScreenOnOrOffUtternace(feedbackIndex)
mHandler.obtainMessage(MESSAGE_SPEAK, utterance).sendToTarget()
return
}
AccessibilityServiceInfo.FEEDBACK_AUDIBLE -> {
mHandler.obtainMessage(MESSAGE_PLAY_EARCON, feedbackIndex, 0).sendToTarget()
return
}
AccessibilityServiceInfo.FEEDBACK_HAPTIC -> {
mHandler.obtainMessage(MESSAGE_VIBRATE, feedbackIndex, 0).sendToTarget()
return
}
else -> throw IllegalStateException("Unexpected feedback type $mProvidedFeedbackType")
}
}
}
public override fun onServiceConnected() {
if (isInfrastructureInitialized) {
return
}
mContext = this
// Send a message to start the TTS.
mHandler.sendEmptyMessage(MESSAGE_START_TTS)
// Get the vibrator service.
mVibrator = getSystemService(Service.VIBRATOR_SERVICE) as Vibrator
// Get the AudioManager and configure according the current ring mode.
mAudioManager = getSystemService(Service.AUDIO_SERVICE) as AudioManager
// In Froyo the broadcast receiver for the ringer mode is called back with the
// current state upon registering but in Eclair this is not done so we poll here.
val ringerMode = mAudioManager!!.ringerMode
configureForRingerMode(ringerMode)
// We are in an initialized state now.
isInfrastructureInitialized = true
}
/**
* Called when all clients have disconnected from a particular interface published by the service.
* If our flag `boolean isInfrastructureInitialized` is true we send the empty message
* MESSAGE_SHUTDOWN_TTS to our `Handler mHandler` to shut down the text to speech service,
* and if `BroadcastReceiver mBroadcastReceiver` is not null we unregister it. We then set
* `isInfrastructureInitialized` to false. In either case we return false to the caller.
*
* @param intent The Intent that was used to bind to this service
* @return Return true if you would like to have the service's
*/
override fun onUnbind(intent: Intent): Boolean {
if (isInfrastructureInitialized) {
// Stop the TTS service.
mHandler.sendEmptyMessage(MESSAGE_SHUTDOWN_TTS)
// Unregister the intent broadcast receiver.
@Suppress("SENSELESS_COMPARISON")
if (mBroadcastReceiver != null) {
unregisterReceiver(mBroadcastReceiver)
}
// We are not in an initialized state anymore.
isInfrastructureInitialized = false
}
return false
}
/**
* Registers the phone state observing broadcast receiver. We initialize `IntentFilter filter`
* with a new instance, add the actions RINGER_MODE_CHANGED_ACTION, ACTION_SCREEN_ON, and
* ACTION_SCREEN_OFF to it then register `BroadcastReceiver mBroadcastReceiver` to receive
* broadcast intents matching `filter`.
*/
private fun registerBroadCastReceiver() {
// Create a filter with the broadcast intents we are interested in.
val filter = IntentFilter()
filter.addAction(AudioManager.RINGER_MODE_CHANGED_ACTION)
filter.addAction(Intent.ACTION_SCREEN_ON)
filter.addAction(Intent.ACTION_SCREEN_OFF)
// Register for broadcasts of interest.
registerReceiver(mBroadcastReceiver, filter, null, null)
}
private fun generateScreenOnOrOffUtternace(feedbackIndex: Int): String {
// Get the announce template.
val resourceId = if (feedbackIndex == INDEX_SCREEN_ON)
R.string.template_screen_on
else
R.string.template_screen_off
val template = mContext.getString(resourceId)
// Format the template with the ringer percentage.
val currentRingerVolume = mAudioManager!!.getStreamVolume(AudioManager.STREAM_RING)
val maxRingerVolume = mAudioManager!!.getStreamMaxVolume(AudioManager.STREAM_RING)
var volumePercent = 100 / maxRingerVolume * currentRingerVolume
// Let us round to five so it sounds better.
val adjustment = volumePercent % 10
if (adjustment < 5) {
volumePercent -= adjustment
} else if (adjustment > 5) {
volumePercent += 10 - adjustment
}
return String.format(template, volumePercent)
}
private fun configureForRingerMode(ringerMode: Int) {
when (ringerMode) {
AudioManager.RINGER_MODE_SILENT -> {
// When the ringer is silent we want to provide only haptic feedback.
mProvidedFeedbackType = AccessibilityServiceInfo.FEEDBACK_HAPTIC
// Take over the spoken and sound feedback so no such feedback is provided.
setServiceInfo(AccessibilityServiceInfo.FEEDBACK_HAPTIC
or AccessibilityServiceInfo.FEEDBACK_SPOKEN
or AccessibilityServiceInfo.FEEDBACK_AUDIBLE)
// Use only an earcon to announce ringer state change.
mHandler.obtainMessage(MESSAGE_PLAY_EARCON, INDEX_RINGER_SILENT, 0).sendToTarget()
}
AudioManager.RINGER_MODE_VIBRATE -> {
// When the ringer is vibrating we want to provide only audible feedback.
mProvidedFeedbackType = AccessibilityServiceInfo.FEEDBACK_AUDIBLE
// Take over the spoken feedback so no spoken feedback is provided.
setServiceInfo(AccessibilityServiceInfo.FEEDBACK_AUDIBLE or AccessibilityServiceInfo.FEEDBACK_SPOKEN)
// Use only an earcon to announce ringer state change.
mHandler.obtainMessage(MESSAGE_PLAY_EARCON, INDEX_RINGER_VIBRATE, 0).sendToTarget()
}
AudioManager.RINGER_MODE_NORMAL -> {
// When the ringer is ringing we want to provide spoken feedback
// overriding the default spoken feedback.
mProvidedFeedbackType = AccessibilityServiceInfo.FEEDBACK_SPOKEN
setServiceInfo(AccessibilityServiceInfo.FEEDBACK_SPOKEN)
// Use only an earcon to announce ringer state change.
mHandler.obtainMessage(MESSAGE_PLAY_EARCON, INDEX_RINGER_NORMAL, 0).sendToTarget()
}
}
}
private fun setServiceInfo(feedbackType: Int) {
val info = AccessibilityServiceInfo()
// We are interested in all types of accessibility events.
info.eventTypes = AccessibilityEvent.TYPES_ALL_MASK
// We want to provide specific type of feedback.
info.feedbackType = feedbackType
// We want to receive events in a certain interval.
info.notificationTimeout = EVENT_NOTIFICATION_TIMEOUT_MILLIS.toLong()
// We want to receive accessibility events only from certain packages.
info.packageNames = PACKAGE_NAMES
serviceInfo = info
}
override fun onAccessibilityEvent(event: AccessibilityEvent) {
Log.i(LOG_TAG, "$mProvidedFeedbackType $event")
// Here we act according to the feedback type we are currently providing.
when (mProvidedFeedbackType) {
AccessibilityServiceInfo.FEEDBACK_SPOKEN ->
mHandler.obtainMessage(MESSAGE_SPEAK, formatUtterance(event)).sendToTarget()
AccessibilityServiceInfo.FEEDBACK_AUDIBLE ->
mHandler.obtainMessage(MESSAGE_PLAY_EARCON, event.eventType, 0).sendToTarget()
AccessibilityServiceInfo.FEEDBACK_HAPTIC ->
mHandler.obtainMessage(MESSAGE_VIBRATE, event.eventType, 0).sendToTarget()
else -> throw IllegalStateException("Unexpected feedback type $mProvidedFeedbackType")
}
}
override fun onInterrupt() {
// Here we act according to the feedback type we are currently providing.
when (mProvidedFeedbackType) {
AccessibilityServiceInfo.FEEDBACK_SPOKEN ->
mHandler.obtainMessage(MESSAGE_STOP_SPEAK).sendToTarget()
AccessibilityServiceInfo.FEEDBACK_AUDIBLE ->
mHandler.obtainMessage(MESSAGE_STOP_PLAY_EARCON).sendToTarget()
AccessibilityServiceInfo.FEEDBACK_HAPTIC ->
mHandler.obtainMessage(MESSAGE_STOP_VIBRATE).sendToTarget()
else -> throw IllegalStateException("Unexpected feedback type $mProvidedFeedbackType")
}
}
private fun formatUtterance(event: AccessibilityEvent): String {
val utterance = mUtterance
// Clear the utterance before appending the formatted text.
utterance.setLength(0)
val eventText = event.text
// We try to get the event text if such.
if (eventText.isNotEmpty()) {
for (subText in eventText) {
var subTextVar = subText
// Make 01 pronounced as 1
if (subTextVar[0] == '0') {
subTextVar = subTextVar.subSequence(1, subText.length)
}
utterance.append(subTextVar)
utterance.append(SPACE)
}
return utterance.toString()
}
// There is no event text but we try to get the content description which is
// an optional attribute for describing a view (typically used with ImageView).
val contentDescription = event.contentDescription
if (contentDescription != null) {
utterance.append(contentDescription)
return utterance.toString()
}
return utterance.toString()
}
private fun playEarcon(earconId: Int) {
var earconName: String? = mEarconNames.get(earconId)
if (earconName == null) {
// We do not know the sound id, hence we need to load the sound.
val resourceId = sSoundsResourceIds.get(earconId)
if (resourceId != null) {
earconName = "[$earconId]"
mTts!!.addEarcon(earconName, packageName, resourceId)
mEarconNames.put(earconId, earconName)
}
}
@Suppress("DEPRECATION")
mTts!!.playEarcon(earconName, QUEUING_MODE_INTERRUPT, null)
}
companion object {
/**
* Tag for logging from this service.
*/
private const val LOG_TAG = "ClockBackService"
// Fields for configuring how the system handles this accessibility service.
/**
* Minimal timeout between accessibility events we want to receive.
*/
private const val EVENT_NOTIFICATION_TIMEOUT_MILLIS = 80
/**
* Packages we are interested in.
*
*
* **
* Note: This code sample will work only on devices shipped with the
* default Clock application.
** *
*
* This works with AlarmClock and Clock whose package name changes in different releases
*/
private val PACKAGE_NAMES = arrayOf(
"com.android.alarmclock",
"com.google.android.deskclock",
"com.android.deskclock"
)
// Message types we are passing around.
/**
* Speak.
*/
private const val MESSAGE_SPEAK = 1
/**
* Stop speaking.
*/
private const val MESSAGE_STOP_SPEAK = 2
/**
* Start the TTS service.
*/
private const val MESSAGE_START_TTS = 3
/**
* Stop the TTS service.
*/
private const val MESSAGE_SHUTDOWN_TTS = 4
/**
* Play an earcon.
*/
private const val MESSAGE_PLAY_EARCON = 5
/**
* Stop playing an earcon.
*/
private const val MESSAGE_STOP_PLAY_EARCON = 6
/**
* Vibrate a pattern.
*/
private const val MESSAGE_VIBRATE = 7
/**
* Stop vibrating.
*/
private const val MESSAGE_STOP_VIBRATE = 8
// Screen state broadcast related constants.
/**
* Feedback mapping index used as a key for the screen-on broadcast.
*/
private const val INDEX_SCREEN_ON = 0x00000100
/**
* Feedback mapping index used as a key for the screen-off broadcast.
*/
private const val INDEX_SCREEN_OFF = 0x00000200
// Ringer mode change related constants.
/**
* Feedback mapping index used as a key for normal ringer mode.
*/
private const val INDEX_RINGER_NORMAL = 0x00000400
/**
* Feedback mapping index used as a key for vibration ringer mode.
*/
private const val INDEX_RINGER_VIBRATE = 0x00000800
/**
* Feedback mapping index used as a key for silent ringer mode.
*/
private const val INDEX_RINGER_SILENT = 0x00001000
// Speech related constants.
/**
* The queuing mode we are using - interrupt a spoken utterance before
* speaking another one.
*/
private const val QUEUING_MODE_INTERRUPT = 2
/**
* The space string constant.
*/
private const val SPACE = " "
/**
* Mapping from integers to vibration patterns for haptic feedback.
*/
private val sVibrationPatterns = SparseArray<LongArray>()
/**
* Initializes our `sVibrationPatterns` `SparseArray`.
*/
init {
sVibrationPatterns.put(AccessibilityEvent.TYPE_VIEW_CLICKED, longArrayOf(0L, 100L))
sVibrationPatterns.put(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED, longArrayOf(0L, 100L))
sVibrationPatterns.put(AccessibilityEvent.TYPE_VIEW_SELECTED, longArrayOf(0L, 15L, 10L, 15L))
sVibrationPatterns.put(AccessibilityEvent.TYPE_VIEW_FOCUSED, longArrayOf(0L, 15L, 10L, 15L))
sVibrationPatterns.put(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED, longArrayOf(0L, 25L, 50L, 25L, 50L, 25L))
sVibrationPatterns.put(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER, longArrayOf(0L, 15L, 10L, 15L, 15L, 10L))
sVibrationPatterns.put(INDEX_SCREEN_ON, longArrayOf(0L, 10L, 10L, 20L, 20L, 30L))
sVibrationPatterns.put(INDEX_SCREEN_OFF, longArrayOf(0L, 30L, 20L, 20L, 10L, 10L))
}
/**
* Mapping from integers to raw sound resource ids.
*/
@SuppressLint("UseSparseArrays")
private val sSoundsResourceIds = SparseArray<Int>()
/**
* Initializes our `sSoundsResourceIds` `SparseArray`.
*/
init {
sSoundsResourceIds.put(AccessibilityEvent.TYPE_VIEW_CLICKED, R.raw.sound_view_clicked)
sSoundsResourceIds.put(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED, R.raw.sound_view_clicked)
sSoundsResourceIds.put(AccessibilityEvent.TYPE_VIEW_SELECTED, R.raw.sound_view_focused_or_selected)
sSoundsResourceIds.put(AccessibilityEvent.TYPE_VIEW_FOCUSED, R.raw.sound_view_focused_or_selected)
sSoundsResourceIds.put(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED, R.raw.sound_window_state_changed)
sSoundsResourceIds.put(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER, R.raw.sound_view_hover_enter)
sSoundsResourceIds.put(INDEX_SCREEN_ON, R.raw.sound_screen_on)
sSoundsResourceIds.put(INDEX_SCREEN_OFF, R.raw.sound_screen_off)
sSoundsResourceIds.put(INDEX_RINGER_SILENT, R.raw.sound_ringer_silent)
sSoundsResourceIds.put(INDEX_RINGER_VIBRATE, R.raw.sound_ringer_vibrate)
sSoundsResourceIds.put(INDEX_RINGER_NORMAL, R.raw.sound_ringer_normal)
}
}
}
Step 4: Create the Activity
This is the activity that demonstrates how to implement our AccessibilityService
, namely the ClockBackService.
Start by adding imports:
import android.content.Intent
import android.os.Bundle
import android.provider.Settings
import android.widget.ImageButton
import androidx.appcompat.app.AppCompatActivity
import com.example.android.apis.R
Extend the AppCompatActivity
:
Called when the activity is starting. First we call through to our super's implementation of onCreate
, then we set our content view to our layout file R.layout.accessibility_service.
We initialize our variable ImageButton button
by finding the view with the id R.id.button and set its OnClickListener
to lambda which starts the system settings activity.
@param savedInstanceState
- we do not overrideonSaveInstanceState
so do not use.
public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.accessibility_service)
// Add a shortcut to the accessibility settings.
val button = findViewById<ImageButton>(R.id.button)
/*
* Called when the button with id R.id.button is clicked, we just launch the settings
* activity.
*
* v View that was clicked
*/
button.setOnClickListener {
startActivity(sSettingsIntent)
}
}
companion object {
/**
* An intent for launching the system settings.
*/
private val sSettingsIntent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)
}
}
Run
- Copy the code above into your android studio project
- Build and run
Kotlin Android Accessibility - How to Query Window Content
This tutorial will teach you how to query window content to improve the feedback given to the user from an accessibility service.
Step 1: Dependencies
No special dependencies are needed.
Step 2: Layouts
This is a simple class and includes no layouts.
Step 3: Write Code
Start by adding imports:
import android.accessibilityservice.AccessibilityService
import android.speech.tts.TextToSpeech
import android.speech.tts.TextToSpeech.OnInitListener
import android.text.TextUtils
import android.util.Log
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo
import com.example.android.apis.R
import java.util.Locale
Create a TaskBackService
class. Extend the AccessibilityService
class and implement the OnInitListener
interface:
Create two instance fields:
/** Flag whether Text-To-Speech is initialized. */
private var mTextToSpeechInitialized: Boolean = false
/** Handle to the Text-To-Speech engine. */
private var mTts: TextToSpeech? = null
Override the onServiceConnected()
function:
Inside the above callback, initialize the Text-To-Speech engine as soon as the service is connected:
Processes an AccessibilityEvent, by traversing the View's tree and putting together a message to speak to the user.
override fun onAccessibilityEvent(event: AccessibilityEvent) {
if (!mTextToSpeechInitialized) {
Log.e(LOG_TAG, "Text-To-Speech engine not ready. Bailing out.")
return
}
This AccessibilityNodeInfo
represents the view that fired the AccessibilityEvent. The following code will use it to traverse the view hierarchy, using this node as a starting point.
NOTE: Every method that returns an AccessibilityNodeInfo may return null, because the explored window is in another process and the corresponding View might be gone by the time your request reaches the view hierarchy.
Grab the parent of the view that fired the event.
Using this parent, get references to both child nodes, the label and the checkbox.
val labelNode = rowNode.getChild(0)
if (labelNode == null) {
rowNode.recycle()
return
}
val completeNode = rowNode.getChild(1)
if (completeNode == null) {
rowNode.recycle()
return
}
Here is the full code:
TaskBackService.kt
import android.accessibilityservice.AccessibilityService
import android.speech.tts.TextToSpeech
import android.speech.tts.TextToSpeech.OnInitListener
import android.text.TextUtils
import android.util.Log
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo
import com.example.android.apis.R
import java.util.Locale
class TaskBackService : AccessibilityService(), OnInitListener {
/** Flag whether Text-To-Speech is initialized. */
private var mTextToSpeechInitialized: Boolean = false
/** Handle to the Text-To-Speech engine. */
private var mTts: TextToSpeech? = null
public override fun onServiceConnected() {
// Initializes the Text-To-Speech engine as soon as the service is connected.
mTts = TextToSpeech(applicationContext, this)
}
override fun onAccessibilityEvent(event: AccessibilityEvent) {
if (!mTextToSpeechInitialized) {
Log.e(LOG_TAG, "Text-To-Speech engine not ready. Bailing out.")
return
}
val source = event.source ?: return
// Grab the parent of the view that fired the event.
val rowNode = getListItemNodeInfo(source) ?: return
// Using this parent, get references to both child nodes, the label and the checkbox.
val labelNode = rowNode.getChild(0)
if (labelNode == null) {
rowNode.recycle()
return
}
val completeNode = rowNode.getChild(1)
if (completeNode == null) {
rowNode.recycle()
return
}
// Determine what the task is and whether or not it's complete, based on
// the text inside the label, and the state of the check-box.
if (rowNode.childCount < 2 || !rowNode.getChild(1).isCheckable) {
rowNode.recycle()
return
}
val taskLabel = labelNode.text
val isComplete = completeNode.isChecked
val completeStr: String
completeStr = if (isComplete) {
getString(R.string.task_complete)
} else {
getString(R.string.task_not_complete)
}
val taskStr = getString(R.string.task_complete_template, taskLabel, completeStr)
val utterance = StringBuilder(taskStr)
// The custom ListView added extra context to the event by adding an
// AccessibilityRecord to it. Extract that from the event and read it.
val records = event.recordCount
for (i in 0 until records) {
val record = event.getRecord(i)
val contentDescription = record.contentDescription
if (!TextUtils.isEmpty(contentDescription)) {
utterance.append(SEPARATOR)
utterance.append(contentDescription)
}
}
// Announce the utterance.
@Suppress("DEPRECATION")
mTts!!.speak(utterance.toString(), TextToSpeech.QUEUE_FLUSH, null)
Log.d(LOG_TAG, utterance.toString())
}
private fun getListItemNodeInfo(source: AccessibilityNodeInfo): AccessibilityNodeInfo? {
var current = source
while (true) {
val parent = current.parent ?: return null
if (TASK_LIST_VIEW_CLASS_NAME.contentEquals(parent.className)) {
return current
}
// NOTE: Recycle the infos.
val oldCurrent = current
current = parent
oldCurrent.recycle()
}
}
/**
* {@inheritDoc}
*/
override fun onInterrupt() {
/* do nothing */
}
/**
* {@inheritDoc}
*/
override fun onInit(status: Int) {
// Set a flag so that the TaskBackService knows that the Text-To-Speech
// engine has been initialized, and can now handle speaking requests.
if (status == TextToSpeech.SUCCESS) {
mTts!!.language = Locale.US
mTextToSpeechInitialized = true
}
}
/**
* {@inheritDoc}
*/
override fun onDestroy() {
super.onDestroy()
if (mTextToSpeechInitialized) {
mTts!!.shutdown()
}
}
companion object {
/** Tag for logging. */
private const val LOG_TAG = "TaskBackService"
/** Comma separator. */
private const val SEPARATOR = ", "
/** The class name of TaskListView - for simplicity we speak only its items. */
private const val TASK_LIST_VIEW_CLASS_NAME = "com.example.android.apis.accessibility.TaskListView"
}
}
Step 4: Run
- Copy the above class into your project.
- Run.