/compass/src/com/google/marvin/compass/TalkingCompass.java

http://eyes-free.googlecode.com/ · Java · 569 lines · 431 code · 97 blank · 41 comment · 77 complexity · b038bb40d4c5dac29576d20297b3ffe8 MD5 · raw file

  1. /*
  2. * Copyright (C) 2008 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.google.marvin.compass;
  17. import com.google.marvin.compass.StreetLocator.StreetLocatorListener;
  18. import com.google.tts.TTSEarcon;
  19. import com.google.tts.TextToSpeechBeta;
  20. import com.whereabout.location.LocationManager;
  21. import com.whereabout.location.LocationManager.WifiLocalizationListener;
  22. import android.app.Activity;
  23. import android.content.Context;
  24. import android.graphics.Canvas;
  25. import android.graphics.Color;
  26. import android.graphics.Paint;
  27. import android.graphics.Path;
  28. import android.hardware.SensorListener;
  29. import android.hardware.SensorManager;
  30. import android.location.GpsStatus;
  31. import android.location.Location;
  32. import android.location.LocationListener;
  33. import android.location.LocationProvider;
  34. import android.os.Bundle;
  35. import android.os.Vibrator;
  36. import android.util.Config;
  37. import android.util.Log;
  38. import android.view.KeyEvent;
  39. import android.view.MotionEvent;
  40. import android.view.View;
  41. /**
  42. * Provides spoken feedback augmented by non-speech audio and tactile feedback
  43. * to turn an Android handset into a wearable compass.
  44. *
  45. * @author clchen@google.com (Charles L. Chen)
  46. */
  47. public class TalkingCompass extends Activity implements StreetLocatorListener {
  48. private static final int MUTE_SPEECHMODE = 0;
  49. private static final int DEFAULT_SPEECHMODE = 1;
  50. private static final int VERBOSE_SPEECHMODE = 2;
  51. private static final int NUM_SPEECH_MODES = 3;
  52. private static final int NORTH = 0;
  53. private static final int EAST = 90;
  54. private static final int SOUTH = 180;
  55. private static final int WEST = 270;
  56. private static final String[] DIRECTION_NAMES = {
  57. "north", "north north east", "north east", "east north east", "east",
  58. "east south east", "south east", "south south east", "south", "south south west",
  59. "south west", "west south west", "west", "west north west", "north west",
  60. "north north west", "north"
  61. };
  62. private static final int TTS_MIN_VER = 1;
  63. private static final String TAG = "Compass";
  64. private static final int MIN_STABLECOUNT = 50;
  65. private static final int STABLECOUNT_AFTER_MODESETTING = 25;
  66. private static final int STABLECOUNT_FOR_VERBOSE = -100;
  67. private static final int STABLECOUNT_FOR_CALIBRATION = -200;
  68. // Degrees of tolerance for a reading to be considered stable
  69. private static final int STABLE_TOLERANCE = 5;
  70. private static final int CARDINAL_TOLERANCE = 1;
  71. private static final int NORTH_LEFT_MAX = 359;
  72. private static final int NORTH_RIGHT_MAX = 1;
  73. private static final long[] VIBE_PATTERN = {
  74. 0, 1, 40, 41
  75. };
  76. private SensorManager sensorManager;
  77. private CompassView view;
  78. private float currentHeading;
  79. private float lastStableHeading;
  80. private int stableCount;
  81. private int speechMode;
  82. private int lastCardinalDir;
  83. private Vibrator vibe;
  84. private boolean sensorOk;
  85. private TalkingCompass self;
  86. private TextToSpeechBeta tts;
  87. private long networkLocLastUpdateTime = -1;
  88. private long gpsLocLastUpdateTime = -1;
  89. private long locationUpdateWaitTime = 15000;
  90. private LocationManager locationManager;
  91. private Location gpsLoc = null;
  92. private Location networkLoc = null;
  93. private StreetLocator locator = null;
  94. private boolean usingGps = false;
  95. private String currentAddress = "";
  96. private String currentIntersection = "";
  97. private String currentFrontBackStreets = "";
  98. /**
  99. * Handles the sensor events for changes to readings and accuracy
  100. */
  101. private final SensorListener mListener = new SensorListener() {
  102. public void onSensorChanged(int sensor, float[] values) {
  103. currentHeading = values[0]; // Values are yaw (heading), pitch, and
  104. // roll.
  105. if (view != null) {
  106. view.invalidate();
  107. }
  108. processDirection();
  109. }
  110. public void onAccuracyChanged(int arg0, int arg1) {
  111. sensorOk = (arg1 == SensorManager.SENSOR_STATUS_ACCURACY_HIGH);
  112. }
  113. };
  114. private TextToSpeechBeta.OnInitListener ttsInitListener = new TextToSpeechBeta.OnInitListener() {
  115. public void onInit(int status, int version) {
  116. if (view != null) {
  117. view.setVisibility(View.GONE);
  118. view = null;
  119. }
  120. Log.e("Compass debug", 3 + "");
  121. view = new CompassView(self);
  122. setContentView(view);
  123. stableCount = 0;
  124. currentHeading = 0;
  125. lastStableHeading = 0;
  126. speechMode = DEFAULT_SPEECHMODE;
  127. lastCardinalDir = 0;
  128. tts.speak(getString(R.string.compass), 0, null);
  129. Log.e("Compass debug", 4 + "");
  130. }
  131. };
  132. private LocationListener networkLocationListener = new LocationListener() {
  133. public void onLocationChanged(Location arg0) {
  134. networkLoc = arg0;
  135. if (networkLocLastUpdateTime + locationUpdateWaitTime < System.currentTimeMillis()){
  136. updateLocationString();
  137. networkLocLastUpdateTime = System.currentTimeMillis();
  138. }
  139. Log.e("Network location", "Lat: " + arg0.getLatitude() + ", Long: "
  140. + arg0.getLongitude());
  141. Log.e("Network location", "Accuracy: " + arg0.getAccuracy());
  142. }
  143. public void onProviderDisabled(String arg0) {
  144. unregisterLocationServices();
  145. networkLoc = null;
  146. networkLocLastUpdateTime = -1;
  147. }
  148. public void onProviderEnabled(String arg0) {
  149. }
  150. public void onStatusChanged(String arg0, int arg1, Bundle arg2) {
  151. }
  152. };
  153. private LocationListener gpsLocationListener = new LocationListener() {
  154. public void onLocationChanged(Location arg0) {
  155. gpsLoc = arg0;
  156. if (gpsLocLastUpdateTime + locationUpdateWaitTime < System.currentTimeMillis()){
  157. updateLocationString();
  158. gpsLocLastUpdateTime = System.currentTimeMillis();
  159. }
  160. Log.e("GPS location", "Lat: " + arg0.getLatitude() + ", Long: " + arg0.getLongitude());
  161. Log.e("GPS location", "Accuracy: " + arg0.getAccuracy());
  162. }
  163. public void onProviderDisabled(String arg0) {
  164. unregisterLocationServices();
  165. gpsLoc = null;
  166. gpsLocLastUpdateTime = -1;
  167. }
  168. public void onProviderEnabled(String arg0) {
  169. }
  170. public void onStatusChanged(String arg0, int arg1, Bundle arg2) {
  171. }
  172. };
  173. GpsStatus.Listener mGpsStatusListener = new GpsStatus.Listener(){
  174. public void onGpsStatusChanged(int event) {
  175. // TODO Auto-generated method stub
  176. }
  177. };
  178. @Override
  179. protected void onCreate(Bundle icicle) {
  180. super.onCreate(icicle);
  181. self = this;
  182. vibe = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE);
  183. sensorOk = true;
  184. sensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
  185. tts = new TextToSpeechBeta(this, ttsInitListener);
  186. locator = new StreetLocator(this);
  187. locationManager = new LocationManager(this);
  188. locationManager.requestLocationUpdates(android.location.LocationManager.GPS_PROVIDER, 1000, 1,
  189. gpsLocationListener);
  190. locationManager.requestLocationUpdates(android.location.LocationManager.NETWORK_PROVIDER, 0, 0,
  191. networkLocationListener);
  192. locationManager.addGpsStatusListener(mGpsStatusListener);
  193. }
  194. @Override
  195. protected void onResume() {
  196. if (Config.LOGD) {
  197. Log.d(TAG, "onResume");
  198. }
  199. super.onResume();
  200. sensorManager.registerListener(mListener, SensorManager.SENSOR_ORIENTATION,
  201. SensorManager.SENSOR_DELAY_GAME);
  202. }
  203. @Override
  204. protected void onStop() {
  205. if (Config.LOGD) {
  206. Log.d(TAG, "onStop");
  207. }
  208. sensorManager.unregisterListener(mListener);
  209. super.onStop();
  210. }
  211. @Override
  212. protected void onDestroy() {
  213. tts.shutdown();
  214. unregisterLocationServices();
  215. locationManager.shutdown();
  216. super.onDestroy();
  217. }
  218. private void updateLocationString() {
  219. Location currentLocation = null;
  220. long gpsTimeAdvantage = 300000;
  221. if (gpsLocLastUpdateTime + gpsTimeAdvantage > networkLocLastUpdateTime) {
  222. currentLocation = gpsLoc;
  223. usingGps = true;
  224. } else {
  225. currentLocation = networkLoc;
  226. usingGps = false;
  227. }
  228. //currentLocation = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER);
  229. //usingGps = true;
  230. if (currentLocation != null) {
  231. locator.getAddressAsync(currentLocation.getLatitude(), currentLocation.getLongitude());
  232. locator.getStreetIntersectionAsync(currentLocation.getLatitude(), currentLocation
  233. .getLongitude());
  234. locator.getStreetsInFrontAndBackAsync(currentLocation.getLatitude(), currentLocation
  235. .getLongitude(), currentHeading);
  236. }
  237. }
  238. public void onAddressLocated(String address) {
  239. currentAddress = "";
  240. if (address.length() > 0) {
  241. // Drop the country
  242. address = address.substring(0, address.lastIndexOf(","));
  243. // Extract the state and zip and insert spaces in the state name
  244. // that the synthesizer will do the right thing.
  245. String rawStateZip = address.substring(address.lastIndexOf(",") + 1);
  246. String zip = rawStateZip.substring(rawStateZip.lastIndexOf(" ") + 1);
  247. String state = rawStateZip.substring(0, rawStateZip.lastIndexOf(" ") + 1);
  248. String stateZip = "";
  249. for (int i = 0; i < state.length(); i++) {
  250. stateZip = stateZip + state.charAt(i) + " ";
  251. }
  252. stateZip = stateZip + zip;
  253. currentAddress = "Near " + address.substring(0, address.lastIndexOf(",")) + ". "
  254. + stateZip;
  255. }
  256. Log.e("currentAddress", currentAddress);
  257. }
  258. public void onIntersectionLocated(String[] streetnames) {
  259. currentIntersection = "";
  260. if (streetnames.length == 0) {
  261. return;
  262. }
  263. currentIntersection = "";
  264. for (String ad : streetnames) {
  265. if (currentAddress.indexOf(ad) == -1) {
  266. currentIntersection += ad + " and ";
  267. }
  268. }
  269. if (currentIntersection.length() > 5) {
  270. currentIntersection = currentIntersection
  271. .substring(0, currentIntersection.length() - 4);
  272. currentIntersection = " Nearby streets are: " + currentIntersection;
  273. }
  274. if (currentIntersection.indexOf("Unknown road") != -1){
  275. currentIntersection = "";
  276. }
  277. Log.e("currentIntersection", currentIntersection);
  278. }
  279. public void onFrontBackLocated(String[] streetsFront, String[] streetsBack) {
  280. currentFrontBackStreets = "";
  281. String currentFrontStreet = "";
  282. String currentBackStreet = "";
  283. if (streetsFront.length > 0) {
  284. for (String ad : streetsFront) {
  285. if (currentAddress.indexOf(ad) == -1) {
  286. currentFrontStreet += ad + " and ";
  287. }
  288. }
  289. if (currentFrontStreet.length() > 5) {
  290. currentFrontStreet = currentFrontStreet.substring(0,
  291. currentFrontStreet.length() - 4);
  292. currentFrontStreet = "Ahead. " + currentFrontStreet;
  293. }
  294. }
  295. if (currentFrontStreet.indexOf("Unknown road") != -1){
  296. currentFrontStreet = "";
  297. }
  298. if (streetsBack.length > 0) {
  299. for (String ad : streetsBack) {
  300. if (currentAddress.indexOf(ad) == -1) {
  301. currentBackStreet += ad + " and ";
  302. }
  303. }
  304. if (currentBackStreet.length() > 5) {
  305. currentBackStreet = currentBackStreet.substring(0,
  306. currentBackStreet.length() - 4);
  307. currentBackStreet = " Behind. " + currentBackStreet;
  308. }
  309. }
  310. if (currentBackStreet.indexOf("Unknown road") != -1){
  311. currentBackStreet = "";
  312. }
  313. currentFrontBackStreets = currentFrontStreet + currentBackStreet;
  314. Log.e("currentFrontBackStreets", currentFrontBackStreets);
  315. }
  316. protected void processDirection() {
  317. // Do not speak immediately - wait until the sensor readings have been
  318. // stable for some time.
  319. if (Math.abs(lastStableHeading - currentHeading) < STABLE_TOLERANCE) {
  320. stableCount++;
  321. } else {
  322. lastStableHeading = currentHeading;
  323. stableCount = 0;
  324. }
  325. // if (stableCount > MIN_STABLECOUNT) {
  326. // speakDirection();
  327. // }
  328. // Do not try bother determining if a new cardinal direction
  329. // was reached if the sensors are not functioning correctly.
  330. if (!sensorOk) {
  331. return;
  332. }
  333. boolean newCardinalDir = false;
  334. int candidateCardinal = findCardinalDir(currentHeading);
  335. if (candidateCardinal != lastCardinalDir) {
  336. newCardinalDir = true;
  337. lastCardinalDir = candidateCardinal;
  338. }
  339. if (newCardinalDir) {
  340. vibe.vibrate(VIBE_PATTERN, -1);
  341. }
  342. }
  343. private int findCardinalDir(float heading) {
  344. if ((heading > NORTH_LEFT_MAX) || (heading < NORTH_RIGHT_MAX)) {
  345. return NORTH;
  346. } else if ((heading > EAST - CARDINAL_TOLERANCE) && (heading < EAST + CARDINAL_TOLERANCE)) {
  347. return EAST;
  348. } else if ((heading > SOUTH - CARDINAL_TOLERANCE) && (heading < SOUTH + CARDINAL_TOLERANCE)) {
  349. return SOUTH;
  350. } else if ((heading > WEST - CARDINAL_TOLERANCE) && (heading < WEST + CARDINAL_TOLERANCE)) {
  351. return WEST;
  352. } else {
  353. return -1;
  354. }
  355. }
  356. public void setAndSpeakCurrentMode(int newSpeechMode) {
  357. speechMode = (newSpeechMode + NUM_SPEECH_MODES) % NUM_SPEECH_MODES;
  358. String text = "";
  359. switch (speechMode) {
  360. case VERBOSE_SPEECHMODE:
  361. stableCount = STABLECOUNT_AFTER_MODESETTING;
  362. text = getString(R.string.verbose);
  363. break;
  364. case DEFAULT_SPEECHMODE:
  365. stableCount = STABLECOUNT_AFTER_MODESETTING;
  366. text = getString(R.string.default_);
  367. break;
  368. case MUTE_SPEECHMODE:
  369. text = getString(R.string.muted);
  370. break;
  371. }
  372. tts.speak(text, 0, null);
  373. }
  374. public void speakDirection() {
  375. stableCount = 0;
  376. if (!sensorOk) {
  377. tts.speak(getString(R.string.calibrate), 0, null);
  378. stableCount = STABLECOUNT_FOR_CALIBRATION;
  379. return;
  380. }
  381. if (speechMode == MUTE_SPEECHMODE) {
  382. return;
  383. }
  384. tts.speak(directionToString(currentHeading), 0, null);
  385. String currentLocationUtterance = currentAddress + " " + currentIntersection + " " + currentFrontBackStreets;
  386. if (currentLocationUtterance.length() > 2){
  387. if (usingGps){
  388. currentLocationUtterance = getString(R.string.gps) + currentLocationUtterance;
  389. } else {
  390. currentLocationUtterance = getString(R.string.network) + currentLocationUtterance;
  391. }
  392. tts.speak(currentLocationUtterance, 1, null);
  393. }
  394. if (speechMode == VERBOSE_SPEECHMODE) {
  395. stableCount = STABLECOUNT_FOR_VERBOSE;
  396. int degrees = Math.round(currentHeading);
  397. tts.speak(Integer.toString(degrees), 1, null);
  398. }
  399. }
  400. public static String directionToString(float heading) {
  401. int index = (int) ((heading * 100 + 1125) / 2250);
  402. return DIRECTION_NAMES[index];
  403. }
  404. private void unregisterLocationServices() {
  405. LocationManager locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
  406. locationManager.removeUpdates(networkLocationListener);
  407. locationManager.removeUpdates(gpsLocationListener);
  408. }
  409. @Override
  410. public boolean onKeyDown(int keyCode, KeyEvent event){
  411. if (keyCode == KeyEvent.KEYCODE_SEARCH){
  412. boolean localizationStarted = locationManager.localizeWithWifi(5000, new WifiLocalizationListener(){
  413. public void onWifiLocalizationResult(int arg0, Location result) {
  414. final String locationName = result == null ? "Location cannot be determined." :
  415. result.getExtras().getStringArray(LocationManager.MATCH_LOCATIONS)[0];
  416. tts.speak(locationName, 0, null);
  417. }
  418. });
  419. if (localizationStarted){
  420. // TODO: Restore the previous speech mode after location name is spoken
  421. speechMode = MUTE_SPEECHMODE;
  422. tts.speak("Scanning", 0, null);
  423. }
  424. }
  425. return super.onKeyDown(keyCode, event);
  426. }
  427. private class CompassView extends View {
  428. private Paint paint = new Paint();
  429. private Path path = new Path();
  430. private float downY;
  431. public CompassView(Context context) {
  432. super(context);
  433. // Construct a wedge-shaped path
  434. path.moveTo(0, -50);
  435. path.lineTo(-20, 60);
  436. path.lineTo(0, 50);
  437. path.lineTo(20, 60);
  438. path.close();
  439. }
  440. @Override
  441. protected void onDraw(Canvas canvas) {
  442. canvas.drawColor(Color.WHITE);
  443. paint.setAntiAlias(true);
  444. paint.setColor(Color.BLACK);
  445. paint.setStyle(Paint.Style.FILL);
  446. int w = canvas.getWidth();
  447. int h = canvas.getHeight();
  448. int cx = w / 2;
  449. int cy = h / 2;
  450. canvas.translate(cx, cy);
  451. canvas.rotate(-currentHeading);
  452. canvas.drawPath(path, paint);
  453. }
  454. @Override
  455. public boolean onTouchEvent(MotionEvent event) {
  456. if (event.getAction() == MotionEvent.ACTION_DOWN) {
  457. downY = event.getY();
  458. return true;
  459. } else if (event.getAction() == MotionEvent.ACTION_UP) {
  460. if (event.getY() + 100 < downY) {
  461. setAndSpeakCurrentMode(speechMode + 1);
  462. } else if (event.getY() - 100 > downY) {
  463. setAndSpeakCurrentMode(speechMode - 1);
  464. } else {
  465. speakDirection();
  466. }
  467. return true;
  468. }
  469. return false;
  470. }
  471. }
  472. }