/* * 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.apis.accessibility; import com.example.android.apis.R; 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 — the service handles only * accessibility events from the clock application. * </li> * <li> * Providing dynamic, context-dependent feedback — feedback type changes * depending on the ringer state. * </li> * </ol> */ 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 = " "; /** 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(AccessibilityEvent.TYPE_VIEW_HOVER_ENTER, new long[] { 0L, 15L, 10L, 15L, 15L, 10L }); 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(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); } // 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); if (pattern != null) { 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> * * @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); } 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(); } return utterance.toString(); } /** * 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. Integer resourceId = sSoundsResourceIds.get(earconId); if (resourceId != null) { earconName = "[" + earconId + "]"; mTts.addEarcon(earconName, getPackageName(), resourceId); mEarconNames.put(earconId, earconName); } } mTts.playEarcon(earconName, QUEUING_MODE_INTERRUPT, null); } }