blob: 5746716e1f786b629f2d8136c26777d6e29f381f [file] [log] [blame]
/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.clockback;
import android.accessibilityservice.AccessibilityService;
import android.accessibilityservice.AccessibilityServiceInfo;
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.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;
import java.util.List;
/**
* This class is an {@link 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:
* <ol>
* <li>
* Simple demonstration of how to use the accessibility APIs.
* </li>
* <li>
* Hands-on example of various ways to utilize the accessibility API for
* providing alternative and complementary feedback.
* </li>
* <li>
* Providing application specific feedback &mdash; the service handles only
* accessibility events from the clock application.
* </li>
* <li>
* Providing dynamic, context-dependent feedback &mdash; feedback type changes
* depending on the ringer state.
* </li>
* <li>
* Application specific UI enhancement - application domain knowledge is
* utilized to enhance the provided feedback.
* </li>
* </ol>
* <p>
* <strong>
* Note: This code sample will work only on devices shipped with the default Clock
* application. If you are running Android 1.6 of Android 2.0 you should enable first
* ClockBack and then TalkBack since in these releases accessibility services are
* notified in the order of registration.
* </strong>
* </p>
*/
public class ClockBackService extends AccessibilityService {
/** Tag for logging from this service. */
private static final String LOG_TAG = "ClockBackService";
// Fields for configuring how the system handles this accessibility service.
/** Minimal timeout between accessibility events we want to receive. */
private static final int EVENT_NOTIFICATION_TIMEOUT_MILLIS = 80;
/** Packages we are interested in.
* <p>
* <strong>
* Note: This code sample will work only on devices shipped with the
* default Clock application.
* </strong>
* </p>
*/
// This works with AlarmClock and Clock whose package name changes in different releases
private static final String[] PACKAGE_NAMES = new String[] {
"com.android.alarmclock", "com.google.android.deskclock", "com.android.deskclock"
};
// Message types we are passing around.
/** Speak. */
private static final int MESSAGE_SPEAK = 1;
/** Stop speaking. */
private static final int MESSAGE_STOP_SPEAK = 2;
/** Start the TTS service. */
private static final int MESSAGE_START_TTS = 3;
/** Stop the TTS service. */
private static final int MESSAGE_SHUTDOWN_TTS = 4;
/** Play an earcon. */
private static final int MESSAGE_PLAY_EARCON = 5;
/** Stop playing an earcon. */
private static final int MESSAGE_STOP_PLAY_EARCON = 6;
/** Vibrate a pattern. */
private static final int MESSAGE_VIBRATE = 7;
/** Stop vibrating. */
private static final int MESSAGE_STOP_VIBRATE = 8;
// Screen state broadcast related constants.
/** Feedback mapping index used as a key for the screen-on broadcast. */
private static final int INDEX_SCREEN_ON = 0x00000100;
/** Feedback mapping index used as a key for the screen-off broadcast. */
private static final int INDEX_SCREEN_OFF = 0x00000200;
// Ringer mode change related constants.
/** Feedback mapping index used as a key for normal ringer mode. */
private static final int INDEX_RINGER_NORMAL = 0x00000400;
/** Feedback mapping index used as a key for vibration ringer mode. */
private static final int INDEX_RINGER_VIBRATE = 0x00000800;
/** Feedback mapping index used as a key for silent ringer mode. */
private static final int INDEX_RINGER_SILENT = 0x00001000;
// Speech related constants.
/**
* The queuing mode we are using - interrupt a spoken utterance before
* speaking another one.
*/
private static final int QUEUING_MODE_INTERRUPT = 2;
/** The space string constant. */
private static final String SPACE = " ";
/**
* The class name of the number picker buttons with no text we want to
* announce in the Clock application.
*/
private static final String CLASS_NAME_NUMBER_PICKER_BUTTON_CLOCK = "android.widget.NumberPickerButton";
/**
* The class name of the number picker buttons with no text we want to
* announce in the AlarmClock application.
*/
private static final String CLASS_NAME_NUMBER_PICKER_BUTTON_ALARM_CLOCK = "com.android.internal.widget.NumberPickerButton";
/**
* The class name of the edit text box for hours and minutes we want to
* better announce.
*/
private static final String CLASS_NAME_EDIT_TEXT = "android.widget.EditText";
/**
* Mapping from integer to string resource id where the keys are generated
* from the {@link AccessibilityEvent#getText()},
* {@link AccessibilityEvent#getItemCount()} and
* {@link AccessibilityEvent#getCurrentItemIndex()} properties.
* <p>
* Note: In general, computing these mappings includes the widget position on
* the screen. This is fragile and should be used as a last resort since
* changing the layout could potentially change the widget position. This is
* a workaround since the widgets of interest are image buttons that do not
* have contentDescription attribute set (plus/minus buttons) or no other
* information in the accessibility event is available to distinguish them
* aside of their positions on the screen (hour/minute inputs).<br/>
* If you are owner of the target application (Clock in this case) you
* should add contentDescription attribute to all image buttons such that a
* screen reader knows how to speak them. For input fields (while not
* applicable for the hour and minute inputs since they are not empty) a
* hint text should be set to enable better announcement.
* </p>
*/
private static final SparseArray<Integer> sEventDataMappedStringResourceIds = new SparseArray<Integer>();
static {
sEventDataMappedStringResourceIds.put(110, R.string.value_increase_hours);
sEventDataMappedStringResourceIds.put(1140, R.string.value_increase_minutes);
sEventDataMappedStringResourceIds.put(1120, R.string.value_decrease_hours);
sEventDataMappedStringResourceIds.put(1160, R.string.value_decrease_minutes);
sEventDataMappedStringResourceIds.put(1111, R.string.value_hour);
sEventDataMappedStringResourceIds.put(1110, R.string.value_hours);
sEventDataMappedStringResourceIds.put(1151, R.string.value_minute);
sEventDataMappedStringResourceIds.put(1150, R.string.value_minutes);
}
/** Mapping from integers to vibration patterns for haptic feedback. */
private static final SparseArray<long[]> sVibrationPatterns = new SparseArray<long[]>();
static {
sVibrationPatterns.put(AccessibilityEvent.TYPE_VIEW_CLICKED, new long[] {
0L, 100L
});
sVibrationPatterns.put(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED, new long[] {
0L, 100L
});
sVibrationPatterns.put(AccessibilityEvent.TYPE_VIEW_SELECTED, new long[] {
0L, 15L, 10L, 15L
});
sVibrationPatterns.put(AccessibilityEvent.TYPE_VIEW_FOCUSED, new long[] {
0L, 15L, 10L, 15L
});
sVibrationPatterns.put(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED, new long[] {
0L, 25L, 50L, 25L, 50L, 25L
});
sVibrationPatterns.put(INDEX_SCREEN_ON, new long[] {
0L, 10L, 10L, 20L, 20L, 30L
});
sVibrationPatterns.put(INDEX_SCREEN_OFF, new long[] {
0L, 30L, 20L, 20L, 10L, 10L
});
}
/** Mapping from integers to raw sound resource ids. */
private static SparseArray<Integer> sSoundsResourceIds = new SparseArray<Integer>();
static {
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(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);
}
// Sound pool related member fields.
/** Mapping from integers to earcon names - dynamically populated. */
private final SparseArray<String> mEarconNames = new SparseArray<String>();
// Auxiliary fields.
/**
* Handle to this service to enable inner classes to access the {@link Context}.
*/
Context mContext;
/** The feedback this service is currently providing. */
int mProvidedFeedbackType;
/** Reusable instance for building utterances. */
private final StringBuilder mUtterance = new StringBuilder();
// Feedback providing services.
/** The {@link TextToSpeech} used for speaking. */
private TextToSpeech mTts;
/** The {@link AudioManager} for detecting ringer state. */
private AudioManager mAudioManager;
/** Vibrator for providing haptic feedback. */
private Vibrator mVibrator;
/** Flag if the infrastructure is initialized. */
private boolean isInfrastructureInitialized;
/** {@link Handler} for executing messages on the service main thread. */
Handler mHandler = new Handler() {
@Override
public void handleMessage(Message message) {
switch (message.what) {
case MESSAGE_SPEAK:
String utterance = (String) message.obj;
mTts.speak(utterance, QUEUING_MODE_INTERRUPT, null);
return;
case MESSAGE_STOP_SPEAK:
mTts.stop();
return;
case MESSAGE_START_TTS:
mTts = new TextToSpeech(mContext, new TextToSpeech.OnInitListener() {
public void onInit(int status) {
// Register here since to add earcons the TTS must be initialized and
// the receiver is called immediately with the current ringer mode.
registerBroadCastReceiver();
}
});
return;
case MESSAGE_SHUTDOWN_TTS:
mTts.shutdown();
return;
case MESSAGE_PLAY_EARCON:
int resourceId = message.arg1;
playEarcon(resourceId);
return;
case MESSAGE_STOP_PLAY_EARCON:
mTts.stop();
return;
case MESSAGE_VIBRATE:
int key = message.arg1;
long[] pattern = sVibrationPatterns.get(key);
mVibrator.vibrate(pattern, -1);
return;
case MESSAGE_STOP_VIBRATE:
mVibrator.cancel();
return;
}
}
};
/**
* {@link BroadcastReceiver} for receiving updates for our context - device
* state.
*/
private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (AudioManager.RINGER_MODE_CHANGED_ACTION.equals(action)) {
int ringerMode = intent.getIntExtra(AudioManager.EXTRA_RINGER_MODE,
AudioManager.RINGER_MODE_NORMAL);
configureForRingerMode(ringerMode);
} else if (Intent.ACTION_SCREEN_ON.equals(action)) {
provideScreenStateChangeFeedback(INDEX_SCREEN_ON);
} else if (Intent.ACTION_SCREEN_OFF.equals(action)) {
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.
*
* @param feedbackIndex The index of the feedback in the statically
* mapped feedback resources.
*/
private void provideScreenStateChangeFeedback(int feedbackIndex) {
// We take a specific action depending on the feedback we currently provide.
switch (mProvidedFeedbackType) {
case AccessibilityServiceInfo.FEEDBACK_SPOKEN:
String utterance = generateScreenOnOrOffUtternace(feedbackIndex);
mHandler.obtainMessage(MESSAGE_SPEAK, utterance).sendToTarget();
return;
case AccessibilityServiceInfo.FEEDBACK_AUDIBLE:
mHandler.obtainMessage(MESSAGE_PLAY_EARCON, feedbackIndex, 0).sendToTarget();
return;
case AccessibilityServiceInfo.FEEDBACK_HAPTIC:
mHandler.obtainMessage(MESSAGE_VIBRATE, feedbackIndex, 0).sendToTarget();
return;
default:
throw new IllegalStateException("Unexpected feedback type "
+ mProvidedFeedbackType);
}
}
};
@Override
public void onServiceConnected() {
if (isInfrastructureInitialized) {
return;
}
mContext = this;
// Send a message to start the TTS.
mHandler.sendEmptyMessage(MESSAGE_START_TTS);
// Get the vibrator service.
mVibrator = (Vibrator) getSystemService(Service.VIBRATOR_SERVICE);
// Get the AudioManager and configure according the current ring mode.
mAudioManager = (AudioManager) getSystemService(Service.AUDIO_SERVICE);
// 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.
int ringerMode = mAudioManager.getRingerMode();
configureForRingerMode(ringerMode);
// We are in an initialized state now.
isInfrastructureInitialized = true;
}
@Override
public boolean onUnbind(Intent intent) {
if (isInfrastructureInitialized) {
// Stop the TTS service.
mHandler.sendEmptyMessage(MESSAGE_SHUTDOWN_TTS);
// Unregister the intent broadcast receiver.
if (mBroadcastReceiver != null) {
unregisterReceiver(mBroadcastReceiver);
}
// We are not in an initialized state anymore.
isInfrastructureInitialized = false;
}
return false;
}
/**
* Registers the phone state observing broadcast receiver.
*/
private void registerBroadCastReceiver() {
// Create a filter with the broadcast intents we are interested in.
IntentFilter filter = new 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.
*
* @param feedbackIndex The feedback index for looking up feedback value.
* @return The utterance.
*/
private String generateScreenOnOrOffUtternace(int feedbackIndex) {
// Get the announce template.
int resourceId = (feedbackIndex == INDEX_SCREEN_ON) ? R.string.template_screen_on
: R.string.template_screen_off;
String template = mContext.getString(resourceId);
// Format the template with the ringer percentage.
int currentRingerVolume = mAudioManager.getStreamVolume(AudioManager.STREAM_RING);
int maxRingerVolume = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_RING);
int volumePercent = (100 / maxRingerVolume) * currentRingerVolume;
// Let us round to five so it sounds better.
int 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:
* <p>
* 1. {@link AudioManager#RINGER_MODE_SILENT}<br/>
* Goal: Provide only custom haptic feedback.<br/>
* 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.<br/>
* 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.
* </p>
* <p>
* 2. {@link AudioManager#RINGER_MODE_VIBRATE}<br/>
* Goal: Provide custom audible and default haptic feedback.<br/>
* Approach: Take over the audible feedback and provide custom one.<br/>
* Take over the spoken feedback but do not provide such.<br/>
* Let some other service provide haptic feedback (KickBack).
* </p>
* <p>
* 3. {@link AudioManager#RINGER_MODE_NORMAL}
* Goal: Provide custom spoken, default audible and default haptic feedback.<br/>
* Approach: Take over the spoken feedback and provide custom one.<br/>
* Let some other services provide audible feedback (SounBack) and haptic
* feedback (KickBack).
* </p>
* Note: In the above description an assumption is made that all default feedback
* services are enabled. Such services are TalkBack, SoundBack, and KickBack.
* Also the feature of defining a service as the default for a given feedback
* type will be available in Android 2.2 and above. For previous releases the package
* specific accessibility service must be registered first i.e. checked in the
* settings.
*
* @param ringerMode The device ringer mode.
*/
private void configureForRingerMode(int ringerMode) {
if (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
| AccessibilityServiceInfo.FEEDBACK_SPOKEN
| AccessibilityServiceInfo.FEEDBACK_AUDIBLE);
// Use only an earcon to announce ringer state change.
mHandler.obtainMessage(MESSAGE_PLAY_EARCON, INDEX_RINGER_SILENT, 0).sendToTarget();
} else if (ringerMode == 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
| AccessibilityServiceInfo.FEEDBACK_SPOKEN);
// Use only an earcon to announce ringer state change.
mHandler.obtainMessage(MESSAGE_PLAY_EARCON, INDEX_RINGER_VIBRATE, 0).sendToTarget();
} else if (ringerMode == 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 {@link AccessibilityServiceInfo} which informs the system how to
* handle this {@link AccessibilityService}.
*
* @param feedbackType The type of feedback this service will provide.
* <p>
* Note: The feedbackType parameter is an bitwise or of all
* feedback types this service would like to provide.
* </p>
*/
private void setServiceInfo(int feedbackType) {
AccessibilityServiceInfo info = new 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;
// We want to receive accessibility events only from certain packages.
info.packageNames = PACKAGE_NAMES;
setServiceInfo(info);
}
@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
Log.i(LOG_TAG, mProvidedFeedbackType + " " + event.toString());
// Here we act according to the feedback type we are currently providing.
if (mProvidedFeedbackType == AccessibilityServiceInfo.FEEDBACK_SPOKEN) {
mHandler.obtainMessage(MESSAGE_SPEAK, formatUtterance(event)).sendToTarget();
} else if (mProvidedFeedbackType == AccessibilityServiceInfo.FEEDBACK_AUDIBLE) {
mHandler.obtainMessage(MESSAGE_PLAY_EARCON, event.getEventType(), 0).sendToTarget();
} else if (mProvidedFeedbackType == AccessibilityServiceInfo.FEEDBACK_HAPTIC) {
mHandler.obtainMessage(MESSAGE_VIBRATE, event.getEventType(), 0).sendToTarget();
} else {
throw new IllegalStateException("Unexpected feedback type " + mProvidedFeedbackType);
}
}
@Override
public void onInterrupt() {
// Here we act according to the feedback type we are currently providing.
if (mProvidedFeedbackType == AccessibilityServiceInfo.FEEDBACK_SPOKEN) {
mHandler.obtainMessage(MESSAGE_STOP_SPEAK).sendToTarget();
} else if (mProvidedFeedbackType == AccessibilityServiceInfo.FEEDBACK_AUDIBLE) {
mHandler.obtainMessage(MESSAGE_STOP_PLAY_EARCON).sendToTarget();
} else if (mProvidedFeedbackType == AccessibilityServiceInfo.FEEDBACK_HAPTIC) {
mHandler.obtainMessage(MESSAGE_STOP_VIBRATE).sendToTarget();
} else {
throw new IllegalStateException("Unexpected feedback type " + mProvidedFeedbackType);
}
}
/**
* Formats an utterance from an {@link AccessibilityEvent}.
*
* @param event The event from which to format an utterance.
* @return The formatted utterance.
*/
private String formatUtterance(AccessibilityEvent event) {
StringBuilder utterance = mUtterance;
// Clear the utterance before appending the formatted text.
utterance.setLength(0);
List<CharSequence> eventText = event.getText();
// We try to get the event text if such.
if (!eventText.isEmpty()) {
for (CharSequence subText : eventText) {
// Make 01 pronounced as 1
if (subText.charAt(0) =='0') {
subText = subText.subSequence(1, subText.length());
}
utterance.append(subText);
utterance.append(SPACE);
}
// Here we do a bit of enhancement of the UI presentation by using the semantic
// of the event source in the context of the Clock application.
if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_FOCUSED
&& CLASS_NAME_EDIT_TEXT.equals(event.getClassName())) {
// If the source is an edit text box and we have a mapping based on
// its position in the items of the container parent of the event source
// we append that value as well. We say "XX hours" and "XX minutes".
String resourceValue = getEventDataMappedStringResource(event);
if (resourceValue != null) {
utterance.append(resourceValue);
}
}
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).
CharSequence contentDescription = event.getContentDescription();
if (contentDescription != null) {
utterance.append(contentDescription);
return utterance.toString();
}
// No text and content description for the plus and minus buttons, so we lookup
// custom values based on the event's itemCount and currentItemIndex properties.
CharSequence className = event.getClassName();
if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_FOCUSED
&& (CLASS_NAME_NUMBER_PICKER_BUTTON_ALARM_CLOCK.equals(className)
|| CLASS_NAME_NUMBER_PICKER_BUTTON_CLOCK.equals(className))) {
String resourceValue = getEventDataMappedStringResource(event);
utterance.append(resourceValue);
}
return utterance.toString();
}
/**
* Returns a string resource mapped based on the accessibility event
* data, specifically the
* {@link AccessibilityEvent#getText()},
* {@link AccessibilityEvent#getItemCount()}, and
* {@link AccessibilityEvent#getCurrentItemIndex()} properties.
*
* @param event The {@link AccessibilityEvent} to process.
* @return The mapped string if such exists, null otherwise.
*/
private String getEventDataMappedStringResource(AccessibilityEvent event) {
int lookupIndex = computeLookupIndex(event);
int resourceId = sEventDataMappedStringResourceIds.get(lookupIndex);
return getString(resourceId);
}
/**
* Computes an index for looking up the custom text for views which either
* do not have text/content description or the position information
* is the only oracle for deciding from which widget was an accessibility
* event generated. The index is computed based on
* {@link AccessibilityEvent#getText()},
* {@link AccessibilityEvent#getItemCount()}, and
* {@link AccessibilityEvent#getCurrentItemIndex()} properties.
*
* @param event The event from which to compute the index.
* @return The lookup index.
*/
private int computeLookupIndex(AccessibilityEvent event) {
int lookupIndex = event.getItemCount();
int divided = event.getCurrentItemIndex();
while (divided > 0) {
lookupIndex *= 10;
divided /= 10;
}
lookupIndex += event.getCurrentItemIndex();
lookupIndex *= 10;
// This is primarily for handling the zero hour/zero minutes cases
if (!event.getText().isEmpty()
&& ("1".equals(event.getText().get(0).toString()) || "01".equals(event.getText()
.get(0).toString()))) {
lookupIndex++;
}
return lookupIndex;
}
/**
* Plays an earcon given its id.
*
* @param earconId The id of the earcon to be played.
*/
private void playEarcon(int earconId) {
String earconName = mEarconNames.get(earconId);
if (earconName == null) {
// We do not know the sound id, hence we need to load the sound.
int resourceId = sSoundsResourceIds.get(earconId);
earconName = "[" + earconId + "]";
mTts.addEarcon(earconName, getPackageName(), resourceId);
mEarconNames.put(earconId, earconName);
}
mTts.playEarcon(earconName, QUEUING_MODE_INTERRUPT, null);
}
}