/KeyboardTutor/src/com/googlecode/eyesfree/keyboardtutor/KeyboardTutor.java
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}