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