PageRenderTime 21ms CodeModel.GetById 2ms app.highlight 15ms RepoModel.GetById 1ms app.codeStats 0ms

/KeyboardTutor/src/com/googlecode/eyesfree/keyboardtutor/KeyboardTutor.java

http://eyes-free.googlecode.com/
Java | 385 lines | 275 code | 42 blank | 68 comment | 28 complexity | fc4f8ed11b4265e06f8af28614197417 MD5 | raw file
  1/*
  2 * Copyright (C) 2010 Google Inc.
  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
 17package com.googlecode.eyesfree.keyboardtutor;
 18
 19import android.app.Activity;
 20import android.app.AlertDialog;
 21import android.app.Service;
 22import android.content.ActivityNotFoundException;
 23import android.content.DialogInterface;
 24import android.content.Intent;
 25import android.content.pm.ResolveInfo;
 26import android.database.Cursor;
 27import android.net.Uri;
 28import android.os.Bundle;
 29import android.util.Log;
 30import android.view.KeyEvent;
 31import android.view.Menu;
 32import android.view.MenuInflater;
 33import android.view.MenuItem;
 34import android.view.accessibility.AccessibilityEvent;
 35import android.view.accessibility.AccessibilityManager;
 36import android.widget.TextView;
 37
 38import java.util.HashMap;
 39import java.util.List;
 40import java.util.Map;
 41
 42/**
 43 * The {@link KeyboardTutor} activity contains a single TextView that displays a
 44 * description of each key as is it typed. If the user has TalkBack enabled, the
 45 * letter or description of the key will be read to them.
 46 * 
 47 * @author clsimon@google.com (Cheryl Simon)
 48 * 
 49 */
 50public class KeyboardTutor extends Activity {
 51
 52    private static final String LOG_TAG = KeyboardTutor.class.getSimpleName();
 53
 54    private static final String ACTION_ACCESSIBILITY_SERVICE =
 55        "android.accessibilityservice.AccessibilityService";
 56
 57    private static final String CATEGORY_FEEDBACK_SPOKEN =
 58        "android.accessibilityservice.category.FEEDBACK_SPOKEN";
 59
 60    private static final String ACTION_ACCESSIBILITY_SETTINGS =
 61        "android.settings.ACCESSIBILITY_SETTINGS";
 62
 63    private static final String STATUS_PROVIDER_URI_PREFIX = "content://";
 64
 65    private static final String STATUS_PROVIDER_URI_SUFFIX = ".providers.StatusProvider";
 66
 67    private static final Intent sScreenreaderIntent = new Intent();
 68
 69    
 70    static {
 71        sScreenreaderIntent.setAction(ACTION_ACCESSIBILITY_SERVICE);
 72        sScreenreaderIntent.addCategory(CATEGORY_FEEDBACK_SPOKEN);
 73
 74    }
 75    
 76    private final Map<String, String> mPunctuationSpokenEquivalentsMap
 77        = new HashMap<String, String>();
 78    
 79    private TextView mKeyDescriptionText;
 80    private int mLastKeyCode;
 81    
 82    // used to track if onUserLeaveHint is caused by starting an activity vs. pressing home.
 83    private boolean startingActivity = false;
 84
 85    @Override
 86    public void onCreate(Bundle savedInstanceState) {
 87        super.onCreate(savedInstanceState);
 88
 89        buildPunctuationSpokenEquivalentMap();
 90     
 91        setContentView(R.layout.main);
 92        mKeyDescriptionText = (TextView) findViewById(R.id.editText);
 93        mKeyDescriptionText.requestFocus();
 94    }
 95    
 96    @Override
 97    public void onResume() {
 98        super.onResume();
 99        startingActivity = false;
100    }
101
102    private void buildPunctuationSpokenEquivalentMap() {
103        mPunctuationSpokenEquivalentsMap.put("?",
104            getString(R.string.punctuation_questionmark));
105        mPunctuationSpokenEquivalentsMap.put(" ",
106            getString(R.string.punctuation_space));
107        mPunctuationSpokenEquivalentsMap.put(",",
108            getString(R.string.punctuation_comma));
109        mPunctuationSpokenEquivalentsMap.put(".",
110            getString(R.string.punctuation_dot));
111        mPunctuationSpokenEquivalentsMap.put("!",
112            getString(R.string.punctuation_exclamation));
113        mPunctuationSpokenEquivalentsMap.put("(",
114            getString(R.string.punctuation_open_paren));
115        mPunctuationSpokenEquivalentsMap.put(")",
116            getString(R.string.punctuation_close_paren));
117        mPunctuationSpokenEquivalentsMap.put("\"",
118            getString(R.string.punctuation_double_quote));
119        mPunctuationSpokenEquivalentsMap.put(";",
120            getString(R.string.punctuation_semicolon));
121        mPunctuationSpokenEquivalentsMap.put(":",
122            getString(R.string.punctuation_colon));
123    }
124
125    /**
126     * We want to capture all key events, so that we can read the key and not
127     * leave the screen, unless the user presses home or back to exit the app.
128     */
129    @Override
130    public boolean onKeyDown(int keyCode, KeyEvent event) {
131        Log.d(LOG_TAG, "global keydown " + keyCode);
132        
133        if (keyCode == KeyEvent.KEYCODE_BACK && mLastKeyCode == KeyEvent.KEYCODE_BACK) {
134            finish();
135        } else if (keyCode == KeyEvent.KEYCODE_MENU && mLastKeyCode == KeyEvent.KEYCODE_MENU) {
136            return false;
137        }
138
139        String description = getKeyDescription(keyCode);
140        if (description == null) {
141            int unicodeChar = event.getUnicodeChar();
142            // if value is 0, this means that it is not something meant to be displayed in unicode.
143            // These tend to be special function keys that are phone specific, or keys that don't
144            // have an alt function.
145            // TODO(clsimon): find a way to describe what they do.
146            if (unicodeChar == 0) {
147                description = getString(R.string.unknown);
148            } else {
149                description = new String(new int[] { 
150                        unicodeChar 
151                    }, 0, 1);
152                // If this is a punctuation, replace with the spoken equivalent.
153                if (mPunctuationSpokenEquivalentsMap.containsKey(description)) {
154                    description = mPunctuationSpokenEquivalentsMap.get(description);
155                }
156            }
157        }
158        displayAndSpeak(description);
159
160
161        mLastKeyCode = keyCode;
162
163        // allow volume to be adjusted
164        return KeyEvent.KEYCODE_VOLUME_UP != keyCode
165            && KeyEvent.KEYCODE_VOLUME_DOWN != keyCode;
166    }
167
168    @Override
169    public void onWindowFocusChanged(boolean hasFocus) {
170        super.onWindowFocusChanged(hasFocus);
171        if (hasFocus) {        
172            ensureEnabledScreenReader();
173        }
174    }
175
176    @Override
177    protected void onUserLeaveHint() {
178        super.onUserLeaveHint();
179        if (!startingActivity) {
180            displayAndSpeak(getString(R.string.home_message));
181            // reset to empty text, so it doesnt say "Home, exiting .." the next time the user opens
182            // the application
183            mKeyDescriptionText.setText("");
184        }
185    }
186
187    /**
188     * Displays and speaks the given <code>text</code>.
189     */
190    private void displayAndSpeak(String text) {
191        mKeyDescriptionText.setText(text);
192        mKeyDescriptionText.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED);
193    }
194
195    /**
196     * If the KeyEvent is a special key, return a string value describing the
197     * key. Otherwise, return null.
198     */
199    private String getKeyDescription(int keyCode) {
200        String keyText;
201        switch (keyCode) {
202        case KeyEvent.KEYCODE_ALT_LEFT:
203        case KeyEvent.KEYCODE_ALT_RIGHT:
204            keyText = getString(R.string.alt);
205            break;
206        case KeyEvent.KEYCODE_SHIFT_LEFT:
207        case KeyEvent.KEYCODE_SHIFT_RIGHT:
208            keyText = getString(R.string.shift);
209            break;
210        case KeyEvent.KEYCODE_SYM:
211            keyText = getString(R.string.sym);
212            break;
213        case KeyEvent.KEYCODE_DEL:
214            keyText = getString(R.string.delete);
215            break;
216        case KeyEvent.KEYCODE_ENTER:
217            keyText = getString(R.string.enter);
218            break;
219        case KeyEvent.KEYCODE_SPACE:
220            keyText = getString(R.string.space);
221            break;
222        case KeyEvent.KEYCODE_SEARCH:
223            keyText = getString(R.string.search);
224            break;
225        case KeyEvent.KEYCODE_BACK:
226            keyText = getString(R.string.back_message);
227            break;
228        case KeyEvent.KEYCODE_MENU:
229            keyText = getString(R.string.menu);
230            break;
231        case KeyEvent.KEYCODE_CALL:
232            keyText = getString(R.string.call);
233            break;
234        case KeyEvent.KEYCODE_ENDCALL:
235            keyText = getString(R.string.end_call);
236            break;
237        case KeyEvent.KEYCODE_VOLUME_DOWN:
238            keyText = getString(R.string.volume_down);
239            break;
240        case KeyEvent.KEYCODE_VOLUME_UP:
241            keyText = getString(R.string.volume_up);
242            break;
243        case KeyEvent.KEYCODE_CAMERA:
244            keyText = getString(R.string.camera);
245            break;
246        case KeyEvent.KEYCODE_DPAD_LEFT:
247            keyText = getString(R.string.left);
248            break;
249        case KeyEvent.KEYCODE_DPAD_RIGHT:
250            keyText = getString(R.string.right);
251            break;
252        case KeyEvent.KEYCODE_DPAD_UP:
253            keyText = getString(R.string.up);
254            break;
255        case KeyEvent.KEYCODE_DPAD_DOWN:
256            keyText = getString(R.string.down);
257            break;
258        case KeyEvent.KEYCODE_DPAD_CENTER:
259            keyText = getString(R.string.center);
260            break;
261        default:
262            keyText = null;
263        }
264        Log.d("KeyboardTutor", keyText + ", " + keyCode);
265        return keyText;
266    }
267
268    /**
269     * Ensure there is an enabled screen reader. If no such is present we
270     * open the accessibility preferences so the user can enabled it.
271     */
272    private void ensureEnabledScreenReader() {
273        List<ResolveInfo> resolveInfos = getPackageManager().queryIntentServices(
274                sScreenreaderIntent, 0);
275        // if no screen readers installed we let the user know
276        // and quit (this should the first check)
277        if (resolveInfos.isEmpty()) {
278            showNoInstalledScreenreadersWarning();
279            return;
280        }
281
282        // check if accessibility is enabled and if not try to open accessibility
283        // preferences so the user can enable it (this should be the second check)
284        AccessibilityManager accessibilityManger =
285            (AccessibilityManager) getSystemService(Service.ACCESSIBILITY_SERVICE);
286        if (!accessibilityManger.isEnabled()) {
287            showInactiveServiceAlert();
288            return;
289        }
290
291        // find an enabled screen reader and if no such try to open accessibility
292        // preferences so the user can enable one (this should be the third check)
293        for (ResolveInfo resolveInfo : resolveInfos) {
294            Uri uri = Uri.parse(STATUS_PROVIDER_URI_PREFIX + resolveInfo.serviceInfo.packageName
295                    + STATUS_PROVIDER_URI_SUFFIX);
296            Cursor cursor = getContentResolver().query(uri, null, null, null, null);
297            if (cursor.moveToFirst() && cursor.getInt(0) == 1) {
298                return;
299            }
300        }
301        showInactiveServiceAlert();
302    }
303
304    /**
305     * Show a dialog to announce the lack of accessibility settings on the device.
306     */
307    private void showInactiveServiceAlert() {
308        new AlertDialog.Builder(this).setTitle(
309                getString(R.string.title_no_active_screen_reader_alert)).setMessage(
310                getString(R.string.message_no_active_screen_reader_alert)).setCancelable(false)
311                .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
312                    public void onClick(DialogInterface dialog, int id) {
313                        /*
314                         * There is no guarantee that an accessibility settings
315                         * menu exists, so if the ACTION_ACCESSIBILITY_SETTINGS
316                         * intent doesn't match an activity, simply start the
317                         * main settings activity.
318                         */
319                        Intent launchSettings = new Intent(ACTION_ACCESSIBILITY_SETTINGS);
320                        try {
321                            startActivity(launchSettings);
322                        } catch (ActivityNotFoundException ae) {
323                            showNoAccessibilityWarning();
324                        }
325                        dialog.dismiss();
326                    }
327                }).setNegativeButton(android.R.string.no, new DialogInterface.OnClickListener() {
328                    public void onClick(DialogInterface dialog, int id) {
329                        dialog.cancel();
330                        KeyboardTutor.this.finish();
331                    }
332                }).create().show();
333    }
334
335    /**
336     * Show a dialog to announce the lack of accessibility settings on the device.
337     */
338    private void showNoAccessibilityWarning() {
339        new AlertDialog.Builder(this).setTitle(getString(R.string.title_no_accessibility_alert))
340                .setMessage(getString(R.string.message_no_accessibility_alert)).setPositiveButton(
341                        android.R.string.ok, new DialogInterface.OnClickListener() {
342                            public void onClick(DialogInterface dialog, int id) {
343                                KeyboardTutor.this.finish();
344                            }
345                        }).create().show();
346    }
347
348    /**
349     * Show a dialog to announce the lack of screen readers on the device.
350     */
351    private void showNoInstalledScreenreadersWarning() {
352        new AlertDialog.Builder(this).setTitle(getString(R.string.title_no_screen_reader_alert))
353                .setMessage(getString(R.string.message_no_screen_reader_alert)).setPositiveButton(
354                        android.R.string.ok, new DialogInterface.OnClickListener() {
355                            public void onClick(DialogInterface dialog, int id) {
356                                KeyboardTutor.this.finish();
357                            }
358                        }).create().show();
359    }
360    
361    @Override
362    public boolean onCreateOptionsMenu(Menu menu) {
363        MenuInflater inflater = getMenuInflater();
364        inflater.inflate(R.menu.menu, menu);
365        return true;
366    }
367    
368    @Override
369    public boolean onOptionsItemSelected(MenuItem item) {
370        switch (item.getItemId()) {
371            case R.id.about_menu:
372                showAbout();
373                return true;
374            default:
375                return super.onOptionsItemSelected(item);
376        }
377        
378    }
379    
380    private void showAbout() {
381        startingActivity = true;
382        Intent intent = new Intent(this, AboutActivity.class);
383        startActivity(intent);
384    }
385}