1 /*
2  * Copyright (C) 2011 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5  * use this file except in compliance with the License. You may obtain a copy of
6  * the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13  * License for the specific language governing permissions and limitations under
14  * the License.
15  */
16 
17 package com.example.android.apis.accessibility;
18 
19 import com.example.android.apis.R;
20 
21 import android.accessibilityservice.AccessibilityService;
22 import android.text.TextUtils;
23 import android.util.Log;
24 import android.view.accessibility.AccessibilityEvent;
25 import android.view.accessibility.AccessibilityNodeInfo;
26 import android.view.accessibility.AccessibilityRecord;
27 import android.speech.tts.TextToSpeech;
28 import android.speech.tts.TextToSpeech.OnInitListener;
29 
30 import java.util.Locale;
31 
32 /**
33  * This class demonstrates how an accessibility service can query
34  * window content to improve the feedback given to the user.
35  */
36 public class TaskBackService extends AccessibilityService implements OnInitListener {
37 
38     /** Tag for logging. */
39     private static final String LOG_TAG = "TaskBackService/onAccessibilityEvent";
40 
41     /** Comma separator. */
42     private static final String SEPARATOR = ", ";
43 
44     /** The class name of TaskListView - for simplicity we speak only its items. */
45     private static final String TASK_LIST_VIEW_CLASS_NAME =
46         "com.example.android.apis.accessibility.TaskListView";
47 
48     /** Flag whether Text-To-Speech is initialized. */
49     private boolean mTextToSpeechInitialized;
50 
51     /** Handle to the Text-To-Speech engine. */
52     private TextToSpeech mTts;
53 
54     /**
55      * {@inheritDoc}
56      */
57     @Override
onServiceConnected()58     public void onServiceConnected() {
59         // Initializes the Text-To-Speech engine as soon as the service is connected.
60         mTts = new TextToSpeech(getApplicationContext(), this);
61     }
62 
63     /**
64      * Processes an AccessibilityEvent, by traversing the View's tree and
65      * putting together a message to speak to the user.
66      */
67     @Override
onAccessibilityEvent(AccessibilityEvent event)68     public void onAccessibilityEvent(AccessibilityEvent event) {
69         if (!mTextToSpeechInitialized) {
70             Log.e(LOG_TAG, "Text-To-Speech engine not ready.  Bailing out.");
71             return;
72         }
73 
74         // This AccessibilityNodeInfo represents the view that fired the
75         // AccessibilityEvent. The following code will use it to traverse the
76         // view hierarchy, using this node as a starting point.
77         //
78         // NOTE: Every method that returns an AccessibilityNodeInfo may return null,
79         // because the explored window is in another process and the
80         // corresponding View might be gone by the time your request reaches the
81         // view hierarchy.
82         AccessibilityNodeInfo source = event.getSource();
83         if (source == null) {
84             return;
85         }
86 
87         // Grab the parent of the view that fired the event.
88         AccessibilityNodeInfo rowNode = getListItemNodeInfo(source);
89         if (rowNode == null) {
90             return;
91         }
92 
93         // Using this parent, get references to both child nodes, the label and the checkbox.
94         AccessibilityNodeInfo labelNode = rowNode.getChild(0);
95         if (labelNode == null) {
96             rowNode.recycle();
97             return;
98         }
99 
100         AccessibilityNodeInfo completeNode = rowNode.getChild(1);
101         if (completeNode == null) {
102             rowNode.recycle();
103             return;
104         }
105 
106         // Determine what the task is and whether or not it's complete, based on
107         // the text inside the label, and the state of the check-box.
108         if (rowNode.getChildCount() < 2 || !rowNode.getChild(1).isCheckable()) {
109             rowNode.recycle();
110             return;
111         }
112 
113         CharSequence taskLabel = labelNode.getText();
114         final boolean isComplete = completeNode.isChecked();
115 
116         String completeStr = null;
117         if (isComplete) {
118             completeStr = getString(R.string.task_complete);
119         } else {
120             completeStr = getString(R.string.task_not_complete);
121         }
122 
123         String taskStr = getString(R.string.task_complete_template, taskLabel, completeStr);
124         StringBuilder utterance = new StringBuilder(taskStr);
125 
126         // The custom ListView added extra context to the event by adding an
127         // AccessibilityRecord to it. Extract that from the event and read it.
128         final int records = event.getRecordCount();
129         for (int i = 0; i < records; i++) {
130             AccessibilityRecord record = event.getRecord(i);
131             CharSequence contentDescription = record.getContentDescription();
132             if (!TextUtils.isEmpty(contentDescription )) {
133                 utterance.append(SEPARATOR);
134                 utterance.append(contentDescription);
135             }
136         }
137 
138         // Announce the utterance.
139         mTts.speak(utterance.toString(), TextToSpeech.QUEUE_FLUSH, null);
140         Log.d(LOG_TAG, utterance.toString());
141     }
142 
getListItemNodeInfo(AccessibilityNodeInfo source)143     private AccessibilityNodeInfo getListItemNodeInfo(AccessibilityNodeInfo source) {
144         AccessibilityNodeInfo current = source;
145         while (true) {
146             AccessibilityNodeInfo parent = current.getParent();
147             if (parent == null) {
148                 return null;
149             }
150             if (TASK_LIST_VIEW_CLASS_NAME.equals(parent.getClassName())) {
151                 return current;
152             }
153             // NOTE: Recycle the infos.
154             AccessibilityNodeInfo oldCurrent = current;
155             current = parent;
156             oldCurrent.recycle();
157         }
158     }
159 
160     /**
161      * {@inheritDoc}
162      */
163     @Override
onInterrupt()164     public void onInterrupt() {
165         /* do nothing */
166     }
167 
168     /**
169      * {@inheritDoc}
170      */
171     @Override
onInit(int status)172     public void onInit(int status) {
173         // Set a flag so that the TaskBackService knows that the Text-To-Speech
174         // engine has been initialized, and can now handle speaking requests.
175         if (status == TextToSpeech.SUCCESS) {
176             mTts.setLanguage(Locale.US);
177             mTextToSpeechInitialized = true;
178         }
179     }
180 
181     /**
182      * {@inheritDoc}
183      */
184     @Override
onDestroy()185     public void onDestroy() {
186         super.onDestroy();
187         if (mTextToSpeechInitialized) {
188             mTts.shutdown();
189         }
190     }
191 }
192