Files
OpenTracks/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java
T
2010-10-25 22:35:38 -07:00

1551 lines
52 KiB
Java

/*
* Copyright 2008 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks;
import com.google.android.accounts.Account;
import com.google.android.apps.mymaps.MyMapsConstants;
import com.google.android.apps.mymaps.MyMapsList;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.io.AuthManager;
import com.google.android.apps.mytracks.io.AuthManagerFactory;
import com.google.android.apps.mytracks.io.GpxImporter;
import com.google.android.apps.mytracks.io.SendToDocs;
import com.google.android.apps.mytracks.io.SendToMyMaps;
import com.google.android.apps.mytracks.io.TrackWriter;
import com.google.android.apps.mytracks.io.TrackWriterFactory;
import com.google.android.apps.mytracks.io.SendToMyMaps.OnSendCompletedListener;
import com.google.android.apps.mytracks.io.TrackWriterFactory.TrackFileFormat;
import com.google.android.apps.mytracks.services.ITrackRecordingService;
import com.google.android.apps.mytracks.services.StatusAnnouncerFactory;
import com.google.android.apps.mytracks.services.TrackRecordingService;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.apps.mytracks.util.FileUtils;
import com.google.android.maps.mytracks.R;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.app.TabActivity;
import android.content.ComponentName;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.content.res.Resources;
import android.database.Cursor;
import android.location.Location;
import android.location.LocationManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.Window;
import android.view.View.OnTouchListener;
import android.view.ViewGroup.LayoutParams;
import android.view.WindowManager.BadTokenException;
import android.widget.RelativeLayout;
import android.widget.TabHost;
import android.widget.Toast;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import javax.xml.parsers.ParserConfigurationException;
import org.xml.sax.SAXException;
/**
* The super activity that embeds our sub activities.
*
* @author Leif Hendrik Wilden
*/
public class MyTracks extends TabActivity implements OnTouchListener,
OnSharedPreferenceChangeListener, ProgressIndicator {
private static final String WAYPOINT_ICON_URL =
"http://maps.google.com/mapfiles/ms/micons/blue-pushpin.png";
/**
* Singleton instance
*/
private static MyTracks instance = null;
private ChartActivity chartActivity = null;
public ChartActivity getChartActivity() {
return chartActivity;
}
public void setChartActivity(ChartActivity chartActivity) {
this.chartActivity = chartActivity;
}
/*
* Authentication:
*/
private AuthManager auth;
private final HashMap<String, AuthManager> authMap =
new HashMap<String, AuthManager>();
private final AccountChooser accountChooser = new AccountChooser();
/*
* Dialogs:
*/
public static final int DIALOG_PROGRESS = 1;
public static final int DIALOG_IMPORT_PROGRESS = 2;
public static final int DIALOG_WRITE_PROGRESS = 3;
public static final int DIALOG_SEND_TO_GOOGLE = 4;
public static final int DIALOG_SEND_TO_GOOGLE_RESULT = 5;
public static final int DIALOG_CHART_SETTINGS = 6;
private ProgressDialog progressDialog;
private ProgressDialog importProgressDialog;
private ProgressDialog writeProgressDialog;
private SendToGoogleDialog sendToGoogleDialog;
private AlertDialog sendToGoogleResultDialog;
private ChartSettingsDialog chartSettingsDialog;
/*
* Menu manager.
*/
private MenuManager menuManager;
/*
* Information on upload success to MyMaps/Docs.
* Used by SendToGoogleResultDialog.
*/
public boolean sendToMyMapsSuccess = false;
public boolean sendToDocsSuccess = false;
public String sendToMyMapsMapId = null;
public String sendToMyMapsMessage = "";
public String sendToDocsMessage = "";
/**
* True if a new track should be created after the track recording service
* binds.
*/
private boolean startNewTrackRequested = false;
private ITrackRecordingService trackRecordingService;
/**
* The id of the currently recording track.
*/
private long recordingTrackId = -1;
/**
* The id of the currently selected track.
*/
private long selectedTrackId = -1;
/**
* Does the user want to share the current track.
*/
private boolean shareRequested = false;
/**
* Utilities to deal with the database.
*/
private MyTracksProviderUtils providerUtils;
/**
* The connection to the track recording service.
*/
private final ServiceConnection serviceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName className, IBinder service) {
Log.d(MyTracksConstants.TAG, "MyTracks: Service now connected.");
trackRecordingService = ITrackRecordingService.Stub.asInterface(service);
if (startNewTrackRequested) {
startNewTrackRequested = false;
startRecordingNewTrack();
}
}
@Override
public void onServiceDisconnected(ComponentName className) {
Log.d(MyTracksConstants.TAG, "MyTracks: Service now disconnected.");
trackRecordingService = null;
}
};
/**
* Whether {@link #serviceConnection} is bound or not.
*/
private boolean isBound = false;
/*
* Tabs/View navigation:
*/
private NavControls navControls;
/** Icons shown on the left for each tab. */
private final int leftIcons[] =
{ R.drawable.left_arrow_chart_stateful,
R.drawable.left_arrow_track_stateful,
R.drawable.left_arrow_stats_stateful };
/** Icons shown on the right for each tab. */
private final int rightIcons[] =
{ R.drawable.right_arrow_stats_stateful,
R.drawable.right_arrow_chart_stateful,
R.drawable.right_arrow_track_stateful };
private final Runnable changeTab = new Runnable() {
public void run() {
getTabHost().setCurrentTab(navControls.getCurrentIcons());
}
};
public static MyTracks getInstance() {
return instance;
}
/**
* Checks whether we have a track recording session in progress.
* In some cases, when the service has crashed or has been restarted
* by the system, we fall back to the shared preferences.
*
* @return true if the activity is bound to the track recording service and
* the service is recording a track or in case the service is down,
* based on settings from the shared preferences.
*/
public boolean isRecording() {
if (trackRecordingService == null) {
// Fall back to alternative check method.
return isRecordingBasedOnSharedPreferences();
}
try {
return trackRecordingService.isRecording();
} catch (RemoteException e) {
Log.e(MyTracksConstants.TAG, "MyTracks: Remote exception.", e);
// Fall back to alternative check method.
return isRecordingBasedOnSharedPreferences();
}
}
private boolean isRecordingBasedOnSharedPreferences() {
// TrackRecordingServices guarantees that recordingTrackId is set to
// -1 if the track has been stopped.
// TODO: Refresh recordingTrackId.
return recordingTrackId >= 0;
}
/*
* Application lifetime events:
* ============================
*/
@Override
protected void onCreate(Bundle savedInstanceState) {
Log.d(MyTracksConstants.TAG, "MyTracks.onCreate");
super.onCreate(savedInstanceState);
instance = this;
providerUtils = MyTracksProviderUtils.Factory.get(this);
menuManager = new MenuManager(this);
// The volume we want to control is the Text-To-Speech volume
int volumeStream =
new StatusAnnouncerFactory(ApiFeatures.getInstance()).getVolumeStream();
setVolumeControlStream(volumeStream);
// We don't need a window title bar:
requestWindowFeature(Window.FEATURE_NO_TITLE);
final Resources res = getResources();
final TabHost tabHost = getTabHost();
tabHost.addTab(tabHost.newTabSpec("tab1")
.setIndicator("Map", res.getDrawable(
android.R.drawable.ic_menu_mapmode))
.setContent(new Intent(this, MyTracksMap.class)));
tabHost.addTab(tabHost.newTabSpec("tab2")
.setIndicator("Stats", res.getDrawable(R.drawable.menu_stats))
.setContent(new Intent(this, StatsActivity.class)));
tabHost.addTab(tabHost.newTabSpec("tab3")
.setIndicator("Chart", res.getDrawable(R.drawable.menu_elevation))
.setContent(new Intent(this, ChartActivity.class)));
// Hide the tab widget itself. We'll use overlayed prev/next buttons to
// switch between the tabs:
tabHost.getTabWidget().setVisibility(View.GONE);
RelativeLayout layout = new RelativeLayout(this);
LayoutParams params =
new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT);
layout.setLayoutParams(params);
navControls = new NavControls(this, layout, leftIcons, rightIcons, changeTab);
navControls.show();
tabHost.addView(layout);
layout.setOnTouchListener(this);
SharedPreferences prefs =
getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0);
if (prefs != null) {
selectedTrackId =
prefs.getLong(getString(R.string.selected_track_key), -1);
recordingTrackId =
prefs.getLong(getString(R.string.recording_track_key), -1);
prefs.registerOnSharedPreferenceChangeListener(this);
Log.d(MyTracksConstants.TAG, "recordingTrackId: " + recordingTrackId
+ ", selectedTrackId: " + selectedTrackId);
if (recordingTrackId > 0) {
Intent startIntent = new Intent(this, TrackRecordingService.class);
startService(startIntent);
}
}
// This will show the eula until the user accepts or quits the app.
Eula.showEula(this);
// Check if we got invoked via the VIEW intent:
Intent intent = getIntent();
if (intent != null && intent.getAction() != null) {
if (intent.getAction().equals(Intent.ACTION_VIEW)) {
if (intent.getScheme() != null && intent.getScheme().equals("file")) {
Log.w(MyTracksConstants.TAG,
"Received a VIEW intent with file scheme. Importing.");
importGpxFile(intent.getData().getPath());
} else {
Log.w(MyTracksConstants.TAG,
"Received a VIEW intent with unsupported scheme "
+ intent.getScheme());
}
} else {
Log.w(MyTracksConstants.TAG,
"Received an intent with unsupported action " + intent.getAction());
}
} else {
Log.d(MyTracksConstants.TAG, "Received an intent with no action.");
}
}
@Override
protected void onDestroy() {
Log.d(MyTracksConstants.TAG, "MyTracks.onDestroy");
tryUnbindTrackRecordingService();
super.onDestroy();
}
@Override
protected void onPause() {
// Called when activity is going into the background, but has not (yet) been
// killed. Shouldn't block longer than approx. 2 seconds.
Log.d(MyTracksConstants.TAG, "MyTracks.onPause");
tryUnbindTrackRecordingService();
super.onPause();
}
@Override
protected void onResume() {
// Called when the current activity is being displayed or re-displayed
// to the user.
Log.d(MyTracksConstants.TAG, "MyTracks.onResume");
tryBindTrackRecordingService();
super.onResume();
}
@Override
protected void onStop() {
Log.d(MyTracksConstants.TAG, "MyTracks.onStop");
// Clean up any temporary GPX and KML files.
cleanTmpDirectory("gpx");
cleanTmpDirectory("kml");
super.onStop();
}
private void cleanTmpDirectory(String name) {
if (!Environment.getExternalStorageState().equals(
Environment.MEDIA_MOUNTED)) {
return; // Can't do anything now.
}
String sep = System.getProperty("file.separator");
File dir = new File(
Environment.getExternalStorageDirectory() + sep + name + sep + "tmp");
if (!dir.exists()) {
return;
}
File[] list = dir.listFiles();
long now = System.currentTimeMillis();
long oldest = now - 1000 * 3600;
for (File f : list) {
if (f.lastModified() < oldest) {
f.delete();
}
}
}
/*
* Menu events:
* ============
*/
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
return menuManager.onCreateOptionsMenu(menu);
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
menuManager.onPrepareOptionsMenu(menu, providerUtils.getLastTrack() != null,
isRecording(), selectedTrackId >= 0);
return super.onPrepareOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
return (menuManager.onOptionsItemSelected(item)
? true
: super.onOptionsItemSelected(item));
}
/*
* Dialog events:
* ==============
*/
@Override
protected Dialog onCreateDialog(int id) {
switch (id) {
case DIALOG_PROGRESS:
progressDialog = new ProgressDialog(this);
progressDialog.setIcon(android.R.drawable.ic_dialog_info);
progressDialog.setTitle(getString(R.string.progress_title));
progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
progressDialog.setMessage("");
progressDialog.setMax(100);
progressDialog.setProgress(10);
return progressDialog;
case DIALOG_IMPORT_PROGRESS:
importProgressDialog = new ProgressDialog(this);
importProgressDialog.setIcon(android.R.drawable.ic_dialog_info);
importProgressDialog.setTitle(getString(R.string.progress_title));
importProgressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
importProgressDialog.setMessage(
getString(R.string.import_progress_message));
return importProgressDialog;
case DIALOG_WRITE_PROGRESS:
writeProgressDialog = new ProgressDialog(this);
writeProgressDialog.setIcon(android.R.drawable.ic_dialog_info);
writeProgressDialog.setTitle(getString(R.string.progress_title));
writeProgressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
writeProgressDialog.setMessage(
getString(R.string.write_progress_message));
return writeProgressDialog;
case DIALOG_SEND_TO_GOOGLE:
sendToGoogleDialog = new SendToGoogleDialog(this);
return sendToGoogleDialog;
case DIALOG_SEND_TO_GOOGLE_RESULT:
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setIcon(android.R.drawable.ic_dialog_info);
builder.setTitle("Title");
builder.setMessage("Message");
builder.setPositiveButton(getString(R.string.ok), null);
builder.setNeutralButton(getString(R.string.share_map),
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
shareLinkToMyMap(sendToMyMapsMapId);
dialog.dismiss();
}
});
sendToGoogleResultDialog = builder.create();
return sendToGoogleResultDialog;
case DIALOG_CHART_SETTINGS:
chartSettingsDialog = new ChartSettingsDialog(this);
return chartSettingsDialog;
}
return null;
}
@Override
protected void onPrepareDialog(int id, Dialog dialog) {
super.onPrepareDialog(id, dialog);
Log.d(MyTracksConstants.TAG, "MyTracks.onPrepareDialog: " + id);
switch (id) {
case DIALOG_SEND_TO_GOOGLE:
resetSendToGoogleStatus();
break;
case DIALOG_SEND_TO_GOOGLE_RESULT:
boolean success = sendToMyMapsSuccess && sendToDocsSuccess;
sendToGoogleResultDialog.setTitle(
success ? R.string.success : R.string.error);
sendToGoogleResultDialog.setIcon(success
? android.R.drawable.ic_dialog_info
: android.R.drawable.ic_dialog_alert);
sendToGoogleResultDialog.setMessage(getMapsResultMessage());
boolean canShare = sendToMyMapsMapId != null;
View share =
sendToGoogleResultDialog.findViewById(android.R.id.button3);
if (share != null) {
share.setVisibility(canShare ? View.VISIBLE : View.GONE);
}
break;
case DIALOG_CHART_SETTINGS:
Log.d(MyTracksConstants.TAG, "MyTracks.onPrepare chart dialog");
chartSettingsDialog.setup(chartActivity);
break;
}
}
/*
* Key events:
* ===========
*/
@Override
public boolean onTrackballEvent(MotionEvent event) {
if (isRecording()) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
insertStatisticsMarker();
return true;
}
}
return super.onTrackballEvent(event);
}
@Override
public void onActivityResult(int requestCode, int resultCode,
final Intent results) {
TrackFileFormat exportFormat = null;
switch (requestCode) {
case MyTracksConstants.GET_MAP: {
if (resultCode == RESULT_OK) {
results.putExtra("trackid", selectedTrackId);
if (results.hasExtra("mapid")) {
sendToMyMapsMapId = results.getStringExtra("mapid");
}
setProgressMessage(
R.string.progress_message_authenticating_mymaps);
authenticate(results, MyTracksConstants.SEND_TO_GOOGLE,
MyMapsConstants.MAPSHOP_SERVICE);
} else {
dismissDialogSafely(DIALOG_PROGRESS);
}
break;
}
case MyTracksConstants.GET_LOGIN: {
if (resultCode == RESULT_OK && auth != null) {
if (!auth.authResult(resultCode, results)) {
dismissDialogSafely(DIALOG_PROGRESS);
}
} else {
dismissDialogSafely(DIALOG_PROGRESS);
}
break;
}
case MyTracksConstants.SHOW_TRACK: {
if (results != null) {
final long trackId = results.getLongExtra("trackid", -1);
if (trackId >= 0) {
setSelectedTrack(trackId);
// The track list passed the requested action as result code. Hand
// it off to the onAcitivtyResult for further processing:
if (resultCode != MyTracksConstants.SHOW_TRACK) {
onActivityResult(resultCode, Activity.RESULT_OK, results);
}
}
}
break;
}
case MyTracksConstants.SHOW_WAYPOINT: {
if (results != null) {
final long waypointId = results.getLongExtra("waypointid", -1);
if (waypointId >= 0) {
MyTracksMap map =
(MyTracksMap) getLocalActivityManager().getActivity("tab1");
if (map != null) {
getTabHost().setCurrentTab(0);
map.showWaypoint(waypointId);
}
}
}
break;
}
case MyTracksConstants.DELETE_TRACK: {
if (results != null && resultCode == RESULT_OK) {
final long trackId = results.getLongExtra("trackid", selectedTrackId);
deleteTrack(trackId);
}
break;
}
case MyTracksConstants.EDIT_DETAILS: {
if (results != null && resultCode == RESULT_OK) {
final long trackId = results.getLongExtra("trackid", selectedTrackId);
Intent intent = new Intent(this, MyTracksDetails.class);
intent.putExtra("trackid", trackId);
startActivity(intent);
}
break;
}
case MyTracksConstants.AUTHENTICATE_TO_DOCS: {
if (resultCode == RESULT_OK) {
setProgressValue(0);
setProgressMessage(
R.string.progress_message_authenticating_docs);
authenticate(results,
MyTracksConstants.AUTHENTICATE_TO_TRIX, "writely");
} else {
dismissDialogSafely(DIALOG_PROGRESS);
}
break;
}
case MyTracksConstants.AUTHENTICATE_TO_TRIX: {
if (resultCode == RESULT_OK) {
setProgressValue(30);
setProgressMessage(
R.string.progress_message_authenticating_docs);
authenticate(results, MyTracksConstants.SEND_TO_DOCS, "wise");
} else {
dismissDialogSafely(DIALOG_PROGRESS);
}
break;
}
case MyTracksConstants.SEND_TO_DOCS: {
if (results != null && resultCode == RESULT_OK) {
Log.d(MyTracksConstants.TAG, "Sending to Docs....");
setProgressValue(50);
setProgressMessage(R.string.progress_message_sending_docs);
final long trackId = results.getLongExtra("trackid", selectedTrackId);
final SendToDocs sender = new SendToDocs(this, authMap.get("wise"),
authMap.get("writely"), trackId);
Runnable onCompletion = new Runnable() {
public void run() {
setProgressValue(100);
dismissDialogSafely(DIALOG_PROGRESS);
runOnUiThread(new Runnable() {
public void run() {
sendToDocsMessage = sender.getStatusMessage();
sendToDocsSuccess = sender.wasSuccess();
handleMapsFinish();
}
});
}
};
sender.setOnCompletion(onCompletion);
sender.run();
} else {
dismissDialogSafely(DIALOG_PROGRESS);
}
break;
}
case MyTracksConstants.SEND_TO_GOOGLE_DIALOG: {
shareRequested = false;
showDialogSafely(DIALOG_SEND_TO_GOOGLE);
break;
}
case MyTracksConstants.SEND_TO_GOOGLE: {
if (results != null && resultCode == RESULT_OK) {
final String mapId;
final long trackId;
if (results.hasExtra("mapid")) {
mapId = results.getStringExtra("mapid");
} else {
mapId = "new";
}
if (results.hasExtra("trackid")) {
trackId = results.getLongExtra("trackid", -1);
} else {
trackId = selectedTrackId;
}
OnSendCompletedListener onCompletion = new OnSendCompletedListener() {
@Override
public void onSendCompleted(String mapId, boolean success, int statusMessage) {
sendToMyMapsMessage = getString(statusMessage);
sendToMyMapsSuccess = success;
if (sendToMyMapsSuccess) {
sendToMyMapsMapId = mapId;
// Update the map id for this track:
try {
Track track = providerUtils.getTrack(trackId);
track.setMapId(mapId);
providerUtils.updateTrack(track);
} catch (RuntimeException e) {
// If that fails whatever reasons we'll just log an error, but
// continue.
Log.w(MyTracksConstants.TAG, "Updating map id failed.", e);
}
}
if (sendToGoogleDialog.getSendToDocs()) {
onActivityResult(MyTracksConstants.AUTHENTICATE_TO_DOCS,
RESULT_OK, new Intent());
} else {
dismissDialogSafely(DIALOG_PROGRESS);
runOnUiThread(new Runnable() {
public void run() {
handleMapsFinish();
}
});
}
}
};
final SendToMyMaps sender = new SendToMyMaps(this, mapId, auth,
trackId, this/*progressIndicator*/, onCompletion);
HandlerThread handlerThread = new HandlerThread("SendToMyMaps");
handlerThread.start();
Handler handler = new Handler(handlerThread.getLooper());
handler.post(sender);
} else {
dismissDialogSafely(DIALOG_PROGRESS);
}
break;
}
case MyTracksConstants.SAVE_GPX_FILE:
if (exportFormat == null) { exportFormat = TrackFileFormat.GPX; }
//$FALL-THROUGH$
case MyTracksConstants.SAVE_KML_FILE:
if (exportFormat == null) { exportFormat = TrackFileFormat.KML; }
//$FALL-THROUGH$
case MyTracksConstants.SAVE_CSV_FILE:
if (exportFormat == null) { exportFormat = TrackFileFormat.CSV; }
//$FALL-THROUGH$
case MyTracksConstants.SAVE_TCX_FILE:
if (exportFormat == null) { exportFormat = TrackFileFormat.TCX; }
if (results != null && resultCode == Activity.RESULT_OK) {
final long trackId = results.getLongExtra("trackid", selectedTrackId);
if (trackId >= 0) {
saveTrack(trackId, exportFormat);
}
}
break;
case MyTracksConstants.SHARE_LINK: {
Track selectedTrack = providerUtils.getTrack(selectedTrackId);
if (selectedTrack != null) {
if (selectedTrack.getMapId().length() > 0) {
shareLinkToMyMap(selectedTrack.getMapId());
} else {
shareRequested = true;
showDialogSafely(DIALOG_SEND_TO_GOOGLE);
}
}
break;
}
case MyTracksConstants.SHARE_GPX_FILE:
if (exportFormat == null) { exportFormat = TrackFileFormat.GPX; }
//$FALL-THROUGH$
case MyTracksConstants.SHARE_KML_FILE:
if (exportFormat == null) { exportFormat = TrackFileFormat.KML; }
//$FALL-THROUGH$
case MyTracksConstants.SHARE_CSV_FILE:
if (exportFormat == null) { exportFormat = TrackFileFormat.CSV; }
//$FALL-THROUGH$
case MyTracksConstants.SHARE_TCX_FILE: {
if (exportFormat == null) { exportFormat = TrackFileFormat.TCX; }
if (results != null && resultCode == Activity.RESULT_OK) {
final long trackId = results.getLongExtra("trackid", selectedTrackId);
if (trackId >= 0) {
sendTrack(trackId, exportFormat);
}
}
break;
}
case MyTracksConstants.CLEAR_MAP: {
setSelectedTrack(-1);
break;
}
case MyTracksConstants.WELCOME: {
CheckUnits.check(this);
break;
}
default: {
Log.w(MyTracksConstants.TAG,
"Warning unhandled request code: " + requestCode);
}
}
}
@Override
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
navControls.show();
}
return false;
}
@Override
public void onSharedPreferenceChanged(
SharedPreferences sharedPreferences, String key) {
// The service itself cannot listen to changes (not supported by Android for
// services that run in a separate process). So we'll notify it manually:
if (key != null && trackRecordingService != null) {
try {
trackRecordingService.sharedPreferenceChanged(key);
} catch (RemoteException e) {
Log.w(MyTracksConstants.TAG,
"MyTracks: Cannot notify track recording service of changes "
+ "to shared preferences: ", e);
}
}
if (key != null && key.equals(getString(R.string.selected_track_key))) {
selectedTrackId =
sharedPreferences.getLong(getString(R.string.selected_track_key), -1);
}
}
/**
* Simulates the recording of a random location.
* This is for debugging and testing only. Useful if there is no GPS signal
* available.
*/
// public void recordRandomLocation() {
// if (trackRecordingService != null) {
// Location loc = new Location("gps");
// double latitude = 37.5 + random.nextDouble() / 1000;
// double longitude = -120.0 + random.nextDouble() / 1000;
// loc.setLatitude(latitude);
// loc.setLongitude(longitude);
// loc.setAltitude(random.nextDouble() * 100);
// loc.setTime(System.currentTimeMillis());
// loc.setSpeed(random.nextFloat());
// MyTracksMap map =
// (MyTracksMap) getLocalActivityManager().getActivity("tab1");
// if (map != null) {
// map.onLocationChanged(loc);
// }
// StatsActivity stats =
// (StatsActivity) getLocalActivityManager().getActivity("tab2");
// if (stats != null) {
// stats.onLocationChanged(loc);
// }
// try {
// trackRecordingService.recordLocation(loc);
// } catch (RemoteException e) {
// Log.e(MyTracksConstants.TAG, "MyTracks", e);
// }
// }
// }
/**
* Resets status information for sending to MyMaps/Docs.
*/
private void resetSendToGoogleStatus() {
sendToMyMapsMessage = "";
sendToMyMapsSuccess = true;
sendToDocsMessage = "";
sendToDocsSuccess = true;
sendToMyMapsMapId = null;
}
/**
* Reads data from a provider, for example gmail attachment preview. Currently
* fails (due to permissions?!). This is currently dead code not invoked from
* anywhere.
*/
protected String getContent(Uri uri) {
Cursor cursor = null;
try {
cursor = managedQuery(uri,
null /*projection*/, null /*selection*/, null /*selectionArgs*/,
null /*sortOrder*/);
if (cursor == null) {
Toast.makeText(this,
R.string.error_unable_to_read_file, Toast.LENGTH_LONG).show();
return null;
}
if (cursor.moveToFirst()) {
String cols[] = cursor.getColumnNames();
String s = "Cols:";
for (String c : cols) { s += c + ","; }
Toast.makeText(this, s, Toast.LENGTH_LONG).show();
return "";
} else {
Toast.makeText(this, R.string.error_generic, Toast.LENGTH_LONG).show();
return null;
}
} finally {
if (cursor != null) {
stopManagingCursor(cursor);
cursor.close();
}
}
}
private void importGpxFile(final String fileName) {
showDialogSafely(DIALOG_IMPORT_PROGRESS);
Thread t = new Thread() {
@Override
public void run() {
int message = R.string.success;
long[] trackIdsImported = null;
try {
try {
InputStream is = new FileInputStream(fileName);
trackIdsImported = GpxImporter.importGPXFile(is, providerUtils);
} catch (SAXException e) {
Log.e(MyTracksConstants.TAG, "Caught an unexpected exception.", e);
message = R.string.error_generic;
} catch (ParserConfigurationException e) {
Log.e(MyTracksConstants.TAG, "Caught an unexpected exception.", e);
message = R.string.error_generic;
} catch (IOException e) {
Log.e(MyTracksConstants.TAG, "Caught an unexpected exception.", e);
message = R.string.error_unable_to_read_file;
} catch (NullPointerException e) {
Log.e(MyTracksConstants.TAG, "Caught an unexpected exception.", e);
message = R.string.error_invalid_gpx_format;
} catch (OutOfMemoryError e) {
Log.e(MyTracksConstants.TAG, "Caught an unexpected exception.", e);
message = R.string.error_out_of_memory;
}
if (trackIdsImported != null && trackIdsImported.length > 0) {
// select last track from import file
setSelectedTrack(trackIdsImported[trackIdsImported.length - 1]);
} else {
MyTracks.this.showMessageDialog(message, false/* success */);
}
} finally {
runOnUiThread(new Runnable() {
public void run() {
dismissDialog(DIALOG_IMPORT_PROGRESS);
}
});
}
}
};
t.start();
}
/**
* Just like showDialog, but will catch a BadTokenException that sometimes
* (very rarely) gets thrown. This might happen if the user hits the "back"
* button immediately after sending tracks to google.
*
* @param id the dialog id
*/
public void showDialogSafely(final int id) {
runOnUiThread(new Runnable() {
public void run() {
try {
showDialog(id);
} catch (BadTokenException e) {
Log.w(MyTracksConstants.TAG,
"Could not display dialog with id " + id, e);
} catch (IllegalStateException e) {
Log.w(MyTracksConstants.TAG,
"Could not display dialog with id " + id, e);
}
}
});
}
/**
* Dismisses the progress dialog if it is showing. Executed on the UI thread.
*/
public void dismissDialogSafely(final int id) {
runOnUiThread(new Runnable() {
public void run() {
try {
dismissDialog(id);
} catch (IllegalArgumentException e) {
// This will be thrown if this dialog was not shown before.
}
}
});
}
// ProgressIndicator implementation
@Override
public void setProgressMessage(int resId) {
setProgressMessage(getString(resId));
}
private void setProgressMessage(final String message) {
runOnUiThread(new Runnable() {
public void run() {
synchronized (this) {
if (progressDialog != null) {
progressDialog.setMessage(message);
}
}
}
});
}
@Override
public void clearProgressMessage() {
setProgressMessage("");
}
@Override
public void setProgressValue(final int percent) {
runOnUiThread(new Runnable() {
public void run() {
synchronized (this) {
if (progressDialog != null) {
progressDialog.setProgress(percent);
}
}
}
});
}
/**
* Shares a link to a My Map via external app (email, gmail, ...)
* A chooser with apps that support text/plain will be shown to the user.
*/
private void shareLinkToMyMap(String mapId) {
Intent shareIntent = new Intent(Intent.ACTION_SEND);
shareIntent.setType("text/plain");
shareIntent.putExtra(Intent.EXTRA_SUBJECT,
getResources().getText(R.string.share_map_subject).toString());
SharedPreferences prefs =
getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0);
boolean shareUrlOnly = true;
if (prefs != null) {
shareUrlOnly =
prefs.getBoolean(getString(R.string.share_url_only_key), false);
}
String url = MyMapsConstants.MAPSHOP_BASE_URL + "?msa=0&msid=" + mapId;
String msg = (shareUrlOnly
? url
: String.format(
getResources().getText(R.string.share_map_body_format).toString(),
url));
shareIntent.putExtra(Intent.EXTRA_TEXT, msg);
startActivity(Intent.createChooser(shareIntent,
getResources().getText(R.string.share_map).toString()));
}
/**
* Deletes the track with the given id.
* Prompts the user if he want to really delete the track first.
* If the selected track is deleted, the selection will be removed.
*/
public void deleteTrack(final long trackId) {
AlertDialog dialog = null;
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setMessage(getString(R.string.track_will_be_permanently_deleted));
builder.setTitle(getString(R.string.are_you_sure_question));
builder.setIcon(android.R.drawable.ic_dialog_alert);
builder.setPositiveButton(getString(R.string.yes),
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int i) {
dialog.dismiss();
providerUtils.deleteTrack(trackId);
if (trackId == selectedTrackId) {
setSelectedTrack(-1);
}
}});
builder.setNegativeButton(getString(R.string.no),
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int i) {
dialog.dismiss();
}
});
dialog = builder.create();
dialog.show();
}
public Location getCurrentLocation() {
// TODO: Let's look at more advanced algorithms to determine the best
// current location.
LocationManager locationManager =
(LocationManager) getSystemService(Context.LOCATION_SERVICE);
if (locationManager == null) {
return null;
}
final long maxAgeMilliSeconds = 1000 * 60 * 1; // 1 minute
final long maxAgeNetworkMilliSeconds = 1000 * 60 * 10; // 10 minutes
final long now = System.currentTimeMillis();
Location loc = locationManager.getLastKnownLocation(
MyTracksConstants.GPS_PROVIDER);
if (loc == null || loc.getTime() < now - maxAgeMilliSeconds) {
// We don't have a recent GPS fix, just use cell towers if available
loc = locationManager.getLastKnownLocation(
LocationManager.NETWORK_PROVIDER);
if (loc == null || loc.getTime() < now - maxAgeNetworkMilliSeconds) {
// We don't have a recent cell tower location, let the user know:
Toast.makeText(this, getString(R.string.status_no_location),
Toast.LENGTH_LONG).show();
return null;
} else {
// Let the user know we have only an approximate location:
Toast.makeText(this, getString(R.string.status_approximate_location),
Toast.LENGTH_LONG).show();
}
}
return loc;
}
public Location getLastLocation() {
if (providerUtils.getLastLocationId(recordingTrackId) < 0) {
return null;
}
LocationManager locationManager =
(LocationManager) getSystemService(Context.LOCATION_SERVICE);
return locationManager.getLastKnownLocation(MyTracksConstants.GPS_PROVIDER);
}
/**
* Inserts a waypoint marker.
*
* @return the id of the inserted statistics marker, or
* -1 unable to find location
* -2 track recording service is not running?
* -3 remote exception when contacting track recording service
* -4 inserting marker into provider failed
*/
public long insertWaypointMarker() {
Location location = getCurrentLocation();
if (location == null) {
Toast.makeText(this, R.string.error_unable_to_insert_marker,
Toast.LENGTH_LONG).show();
return -1;
}
if (trackRecordingService != null) {
try {
Waypoint wpt = new Waypoint();
wpt.setName(getString(R.string.waypoint));
wpt.setType(Waypoint.TYPE_WAYPOINT);
wpt.setTrackId(recordingTrackId);
wpt.setIcon(WAYPOINT_ICON_URL);
wpt.setLocation(location);
long waypointId = trackRecordingService.insertWaypointMarker(wpt);
if (waypointId >= 0) {
Toast.makeText(this, R.string.status_waypoint_inserted,
Toast.LENGTH_LONG).show();
return waypointId;
} else {
Toast.makeText(this, R.string.error_unable_to_insert_marker,
Toast.LENGTH_LONG).show();
Log.e(MyTracksConstants.TAG, "Cannot insert waypoint marker?");
return -4;
}
} catch (RemoteException e) {
Toast.makeText(this, R.string.error_unable_to_insert_marker,
Toast.LENGTH_LONG).show();
Log.e(MyTracksConstants.TAG, "Cannot insert waypoint marker.", e);
}
return -3;
}
return -2;
}
/**
* Inserts a statistics marker.
*
* @return the id of the inserted statistics marker, or
* -1 unable to find location
* -2 track recording service is not running?
* -3 remote exception when contacting track recording service
* -4 inserting marker into provider failed
*/
public long insertStatisticsMarker() {
Location location = getLastLocation();
if (location == null) {
Toast.makeText(this, R.string.error_unable_to_insert_marker,
Toast.LENGTH_LONG).show();
return -1;
}
if (trackRecordingService != null) {
try {
long waypointId =
trackRecordingService.insertStatisticsMarker(location);
if (waypointId >= 0) {
Toast.makeText(this, R.string.status_statistics_inserted,
Toast.LENGTH_LONG).show();
return waypointId;
} else {
Toast.makeText(this, R.string.error_unable_to_insert_marker,
Toast.LENGTH_LONG).show();
Log.e(MyTracksConstants.TAG, "Cannot insert statistics marker?");
return -4;
}
} catch (RemoteException e) {
Toast.makeText(this, R.string.error_unable_to_insert_marker,
Toast.LENGTH_LONG).show();
Log.e(MyTracksConstants.TAG, "Cannot insert statistics marker?", e);
}
return -3;
}
return -2;
}
/**
* Initializes the authentication manager which obtains an authentication
* token, prompting the user for a login and password if needed.
*/
private void authenticate(final Intent results, final int requestCode,
final String service) {
auth = authMap.get(service);
if (auth == null) {
Log.i(MyTracksConstants.TAG,
"Creating a new authentication for service: " + service);
auth = AuthManagerFactory.getAuthManager(this,
MyTracksConstants.GET_LOGIN,
null,
true,
service);
authMap.put(service, auth);
}
Log.d(MyTracksConstants.TAG, "Loggin in to " + service + "...");
if (AuthManagerFactory.useModernAuthManager()) {
runOnUiThread(new Runnable() {
@Override
public void run() {
accountChooser.chooseAccount(MyTracks.this,
new AccountChooser.AccountHandler() {
@Override
public void handleAccountSelected(Account account) {
if (account == null) {
dismissDialogSafely(DIALOG_PROGRESS);
return;
}
doLogin(results, requestCode, service, account);
}
});
}
});
} else {
doLogin(results, requestCode, service, null);
}
}
private void doLogin(final Intent results, final int requestCode,
final String service, final Account account) {
auth.doLogin(new Runnable() {
public void run() {
Log.d(MyTracksConstants.TAG, "Loggin success for " + service + "!");
onActivityResult(requestCode, RESULT_OK, results);
}
}, account);
}
private void startRecordingNewTrack() {
try {
recordingTrackId = trackRecordingService.startNewTrack();
Toast.makeText(this, getString(R.string.status_now_recording),
Toast.LENGTH_SHORT).show();
setSelectedAndRecordingTrack(recordingTrackId, recordingTrackId);
} catch (RemoteException e) {
Toast.makeText(this,
getString(R.string.error_unable_to_start_recording),
Toast.LENGTH_SHORT).show();
Log.w(MyTracksConstants.TAG, "Unable to start recording.", e);
}
}
/**
* Starts the track recording service (if not already running) and binds to
* it. Starts recording a new track.
*/
public void startRecording() {
if (trackRecordingService == null) {
startNewTrackRequested = true;
Intent startIntent = new Intent(this, TrackRecordingService.class);
startService(startIntent);
tryBindTrackRecordingService();
} else {
startRecordingNewTrack();
}
}
/**
* Stops the track recording service and unbinds from it. Will display a toast
* "Stopped recording" and pop up the Track Details activity.
*/
public void stopRecording() {
if (trackRecordingService != null) {
try {
trackRecordingService.endCurrentTrack();
} catch (RemoteException e) {
Log.e(MyTracksConstants.TAG, "Unable to stop recording.", e);
}
setRecordingTrack(-1);
Intent intent = new Intent(MyTracks.this, MyTracksDetails.class);
intent.putExtra("trackid", recordingTrackId);
intent.putExtra("hasCancelButton", false);
recordingTrackId = -1;
startActivity(intent);
}
tryUnbindTrackRecordingService();
try {
stopService(new Intent(MyTracks.this, TrackRecordingService.class));
} catch (SecurityException e) {
Log.e(MyTracksConstants.TAG,
"Encountered a security exception when trying to stop service.", e);
}
trackRecordingService = null;
}
/**
* Initiates the process to send tracks to google.
* This is called once the user has selected sending options via the
* SendToGoogleDialog.
*/
public void sendToGoogle() {
if (sendToGoogleDialog == null) {
return;
}
setProgressValue(0);
clearProgressMessage();
showDialogSafely(DIALOG_PROGRESS);
if (sendToGoogleDialog.getSendToMyMaps()) {
if (!sendToGoogleDialog.getCreateNewMap()) {
Intent listIntent = new Intent(this, MyMapsList.class);
startActivityForResult(listIntent, MyTracksConstants.GET_MAP);
} else {
setProgressValue(0);
setProgressMessage(
R.string.progress_message_authenticating_mymaps);
authenticate(new Intent(), MyTracksConstants.SEND_TO_GOOGLE,
MyMapsConstants.MAPSHOP_SERVICE);
}
} else {
onActivityResult(MyTracksConstants.AUTHENTICATE_TO_DOCS, RESULT_OK,
new Intent());
}
}
/**
* Writes the selected track id to the shared preferences.
* Executed on the UI thread.
*
* @param trackId the id of the track
*/
public void setSelectedTrack(final long trackId) {
runOnUiThread(new Runnable() {
public void run() {
SharedPreferences prefs =
getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0);
SharedPreferences.Editor editor = prefs.edit();
editor.putLong(getString(R.string.selected_track_key), trackId);
editor.commit();
}
});
}
protected long getSelectedTrack() {
return selectedTrackId;
}
/**
* Writes the recording track id to the shared preferences.
* Executed on the UI thread.
*
* @param trackId the id of the track
*/
private void setRecordingTrack(final long trackId) {
runOnUiThread(new Runnable() {
public void run() {
SharedPreferences prefs =
getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0);
SharedPreferences.Editor editor = prefs.edit();
editor.putLong(getString(R.string.recording_track_key), trackId);
editor.commit();
}
});
}
/**
* Writes the selected and the recording track id to the shared preferences.
* Executed on UI thread.
*/
private void setSelectedAndRecordingTrack(final long theSelectedTrackId,
final long theRecordingTrackId) {
runOnUiThread(new Runnable() {
public void run() {
SharedPreferences prefs =
getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0);
if (prefs != null) {
SharedPreferences.Editor editor = prefs.edit();
editor.putLong(getString(R.string.selected_track_key), theSelectedTrackId);
editor.putLong(getString(R.string.recording_track_key), theRecordingTrackId);
editor.commit();
}
}
});
}
/**
* Binds to track recording service if it is running.
*/
private void tryBindTrackRecordingService() {
Log.d(MyTracksConstants.TAG,
"MyTracks: Trying to bind to track recording service...");
bindService(new Intent(this, TrackRecordingService.class),
serviceConnection, 0);
Log.d(MyTracksConstants.TAG, "MyTracks: ...bind finished!");
isBound = true;
}
/**
* Tries to unbind the track recording service. Catches exception silently in
* case service is not registered anymore.
*/
private void tryUnbindTrackRecordingService() {
if (isBound) {
Log.d(MyTracksConstants.TAG,
"MyTracks: Trying to unbind from track recording service...");
try {
unbindService(serviceConnection);
Log.d(MyTracksConstants.TAG, "MyTracks: ...unbind finished!");
} catch (IllegalArgumentException e) {
Log.d(MyTracksConstants.TAG,
"MyTracks: Tried unbinding, but service was not registered.", e);
}
isBound = false;
}
}
/**
* Shows a dialog with the given message.
* Does it on the UI thread.
*
* @param success if true, displays an info icon/title, otherwise an error
* icon/title
* @param message resource string id
*/
public void showMessageDialog(final int message, final boolean success) {
runOnUiThread(new Runnable() {
public void run() {
AlertDialog dialog = null;
AlertDialog.Builder builder = new AlertDialog.Builder(MyTracks.this);
builder.setMessage(MyTracks.this.getString(message));
builder.setNegativeButton(MyTracks.this.getString(R.string.ok), null);
builder.setIcon(success ? android.R.drawable.ic_dialog_info :
android.R.drawable.ic_dialog_alert);
builder.setTitle(success ? R.string.success : R.string.error);
dialog = builder.create();
dialog.show();
}
});
}
/**
* Saves the track with the given id to the SD card.
*
* @param trackId The id of the track to be sent
*/
public void saveTrack(long trackId, TrackFileFormat format) {
showDialogSafely(DIALOG_WRITE_PROGRESS);
final TrackWriter writer =
TrackWriterFactory.newWriter(this, providerUtils, trackId, format);
writer.setOnCompletion(new Runnable() {
public void run() {
dismissDialogSafely(DIALOG_WRITE_PROGRESS);
showMessageDialog(writer.getErrorMessage(), writer.wasSuccess());
}
});
writer.writeTrackAsync();
}
/**
* Sends the requested track as an email attachment.
* This will leave the gpx file on the SD card for at least one hour.
* Temporary gpx files will be deleted in onStop.
*
* @param trackId The id of the track to be sent
*/
public void sendTrack(long trackId, final TrackFileFormat format) {
showDialogSafely(DIALOG_WRITE_PROGRESS);
final TrackWriter writer =
TrackWriterFactory.newWriter(this, providerUtils, trackId, format);
FileUtils fileUtils = new FileUtils();
String extension = format.getExtension();
String dirName = fileUtils.buildExternalDirectoryPath(extension, "tmp");
File dir = new File(dirName);
writer.setDirectory(dir);
writer.setOnCompletion(new Runnable() {
public void run() {
dismissDialogSafely(DIALOG_WRITE_PROGRESS);
if (!writer.wasSuccess()) {
showMessageDialog(writer.getErrorMessage(), writer.wasSuccess());
} else {
Intent shareIntent = new Intent(Intent.ACTION_SEND);
shareIntent.putExtra(Intent.EXTRA_SUBJECT,
getResources().getText(R.string.send_track_subject).toString());
shareIntent.putExtra(Intent.EXTRA_TEXT,
getResources().getText(R.string.send_track_body_format)
.toString());
shareIntent.setType(format.getMimeType());
Uri u = Uri.fromFile(new File(writer.getAbsolutePath()));
shareIntent.putExtra(Intent.EXTRA_STREAM, u);
startActivity(Intent.createChooser(shareIntent,
getResources().getText(R.string.share_track).toString()));
}
}
});
writer.writeTrackAsync();
}
/**
* Notifies that uploading to maps is finished.
*/
private void handleMapsFinish() {
if (shareRequested && sendToMyMapsSuccess) {
// Just share
Toast.makeText(this, getMapsResultMessage(), Toast.LENGTH_LONG).show();
shareLinkToMyMap(sendToMyMapsMapId);
} else {
showDialogSafely(DIALOG_SEND_TO_GOOGLE_RESULT);
}
}
private String getMapsResultMessage() {
StringBuilder message = new StringBuilder();
if (sendToGoogleDialog.getSendToMyMaps()) {
message.append(sendToMyMapsMessage);
}
if (sendToGoogleDialog.getSendToDocs()) {
if (message.length() > 0) {
message.append(' ');
}
message.append(sendToDocsMessage);
}
if (sendToMyMapsSuccess && sendToDocsSuccess) {
message.append(' ');
message.append(getString(R.string.status_mymap_info));
}
return message.toString();
}
public AccountChooser getAccountChooser() {
return accountChooser;
}
}