diff --git a/MyTracks/AndroidManifest.xml b/MyTracks/AndroidManifest.xml index 166ec86ff..b2c4d90cd 100755 --- a/MyTracks/AndroidManifest.xml +++ b/MyTracks/AndroidManifest.xml @@ -44,6 +44,110 @@ limitations under the License. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -52,44 +156,6 @@ limitations under the License. - - - - - - - - - - - - - - - - - - - - - - diff --git a/MyTracks/res/values-ja/strings.xml b/MyTracks/res/values-ja/strings.xml index 5732e571f..1a6a34106 100644 --- a/MyTracks/res/values-ja/strings.xml +++ b/MyTracks/res/values-ja/strings.xml @@ -159,8 +159,8 @@ limitations under the License. $2 : Attempted imports $3 : MyTracks gpx path --> - %3$s からGPXファイル %2$d件のうち %1$d件をインポートしました - %s ではGPXファイルがありません.SDカードを読み込めないかディレクトリにGPXファイルが無いかも知れません. + %3$s からGPXファイル %2$d件のうち %1$d件をインポートしました + %s ではGPXファイルがありません.SDカードを読み込めないかディレクトリにGPXファイルが無いかも知れません. ファイル %s のインポートエラー diff --git a/MyTracks/res/values-ru/strings.xml b/MyTracks/res/values-ru/strings.xml index 5e3068b2f..0598737ce 100644 --- a/MyTracks/res/values-ru/strings.xml +++ b/MyTracks/res/values-ru/strings.xml @@ -162,8 +162,8 @@ limitations under the License. $2 : Attempted imports $3 : MyTracks gpx path --> - Импортировано %1$d из %2$d GPX файлов с %3$s - GPX файлы не найденны в %s. Возможно не читается SD-карта или каталог не содержит GPX файлы. + Импортировано %1$d из %2$d GPX файлов с %3$s + GPX файлы не найденны в %s. Возможно не читается SD-карта или каталог не содержит GPX файлы. Ошибка импорта файла %s diff --git a/MyTracks/res/values/do_not_translate.xml b/MyTracks/res/values/do_not_translate.xml index 9b586e743..f6252d1db 100644 --- a/MyTracks/res/values/do_not_translate.xml +++ b/MyTracks/res/values/do_not_translate.xml @@ -42,4 +42,7 @@ limitations under the License. UA-7222692-2 + + + android.intent.action.SAVE diff --git a/MyTracks/res/values/strings.xml b/MyTracks/res/values/strings.xml index 694c2520d..5a5e33fba 100644 --- a/MyTracks/res/values/strings.xml +++ b/MyTracks/res/values/strings.xml @@ -163,9 +163,12 @@ limitations under the License. $2 : Attempted imports $3 : MyTracks gpx path --> - Imported %1$d of %2$d GPX files from %3$s - No GPX files found in %s. SD card may be unreadable or directory may not contain GPX files. + Imported %1$d of %2$d GPX files from %3$s + No GPX files found in %s. SD card may be unreadable or directory may not contain GPX files. Import error for file %s + Import into My Tracks + Successfully imported %1$d track(s) from file. + Show track Delete all markers diff --git a/MyTracks/src/com/dsi/ant/AntInterface.java b/MyTracks/src/com/dsi/ant/AntInterface.java index 0e425e57d..5fd5ee345 100644 --- a/MyTracks/src/com/dsi/ant/AntInterface.java +++ b/MyTracks/src/com/dsi/ant/AntInterface.java @@ -61,10 +61,10 @@ public class AntInterface { private static Object INSTANCE_LOCK = new Object(); /** The context to use. */ - private static Context sContext = null; + private Context sContext = null; /** Listens to changes to service connection status. */ - private static ServiceListener sServiceListener; + private ServiceListener sServiceListener; /** Is the ANT Radio Proxy Service connected. */ private static boolean sServiceConnected = false; @@ -210,7 +210,7 @@ public class AntInterface { /** * Class for interacting with the ANT interface. */ - private static ServiceConnection sIAntConnection = new ServiceConnection() { + private final ServiceConnection sIAntConnection = new ServiceConnection() { public void onServiceConnected(ComponentName pClassName, IBinder pService) { // This is called when the connection with the service has been // established, giving us the service object we can use to @@ -1223,7 +1223,7 @@ public class AntInterface { */ public boolean requestForceClaimInterface(String appName) throws AntInterfaceException { - if((null == appName) || ("" == appName)) + if((null == appName) || ("".equals(appName))) { throw new IllegalArgumentException(); } diff --git a/MyTracks/src/com/google/android/apps/mytracks/AccountChooser.java b/MyTracks/src/com/google/android/apps/mytracks/AccountChooser.java index 05ba6ad01..ff2a2d0ea 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/AccountChooser.java +++ b/MyTracks/src/com/google/android/apps/mytracks/AccountChooser.java @@ -83,15 +83,19 @@ public class AccountChooser { builder.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + if (selectedAccountIndex >= 0) { selectedAccount = accounts[selectedAccountIndex]; - handler.handleAccountSelected(selectedAccount); } + + handler.handleAccountSelected(selectedAccount); } }); builder.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); handler.handleAccountSelected(null); } }); @@ -122,6 +126,7 @@ public class AccountChooser { builder.setNegativeButton(R.string.ok, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); handler.handleAccountSelected(null); } }); diff --git a/MyTracks/src/com/google/android/apps/mytracks/AggregatedStatsActivity.java b/MyTracks/src/com/google/android/apps/mytracks/AggregatedStatsActivity.java index cb385f35c..3e2767bf2 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/AggregatedStatsActivity.java +++ b/MyTracks/src/com/google/android/apps/mytracks/AggregatedStatsActivity.java @@ -19,9 +19,9 @@ import java.util.List; /** * Activity for viewing the combined statistics for all the recorded tracks. - * + * * Other features to add - menu items to change setings. - * + * * @author Fergus Nelson */ public class AggregatedStatsActivity extends Activity implements @@ -43,7 +43,7 @@ public class AggregatedStatsActivity extends Activity implements Log.d(Constants.TAG, "StatsActivity: onSharedPreferences changed " + key); if (key != null) { - if (key.equals(R.string.metric_units_key)) { + if (key.equals(getString(R.string.metric_units_key))) { metricUnits = sharedPreferences.getBoolean( getString(R.string.metric_units_key), true); utils.setMetricUnits(metricUnits); @@ -86,8 +86,8 @@ public class AggregatedStatsActivity extends Activity implements } /** - * 1. Reads tracks from the db - * 2. Merges the trip stats from the tracks + * 1. Reads tracks from the db + * 2. Merges the trip stats from the tracks * 3. Updates the view */ private void loadAggregatedStats() { diff --git a/MyTracks/src/com/google/android/apps/mytracks/ChartActivity.java b/MyTracks/src/com/google/android/apps/mytracks/ChartActivity.java index 87efb8d58..1d9ba4e4c 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/ChartActivity.java +++ b/MyTracks/src/com/google/android/apps/mytracks/ChartActivity.java @@ -35,6 +35,8 @@ import com.google.android.apps.mytracks.util.UnitConversions; import com.google.android.maps.mytracks.R; import android.app.Activity; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; import android.location.Location; import android.os.Bundle; import android.util.Log; @@ -105,8 +107,6 @@ public class ChartActivity extends Activity implements TrackDataListener { protected void onCreate(Bundle savedInstanceState) { Log.w(TAG, "ChartActivity.onCreate"); super.onCreate(savedInstanceState); - MyTracks.getInstance().setChartActivity(this); - dataHub = MyTracks.getInstance().getDataHub(); // The volume we want to control is the Text-To-Speech volume int volumeStream = @@ -142,6 +142,7 @@ public class ChartActivity extends Activity implements TrackDataListener { protected void onResume() { super.onResume(); + dataHub = TrackDataHub.getStartedInstance(); dataHub.registerTrackDataListener(this, EnumSet.of( ListenerDataType.SELECTED_TRACK_CHANGED, ListenerDataType.TRACK_UPDATES, @@ -154,6 +155,7 @@ public class ChartActivity extends Activity implements TrackDataListener { @Override protected void onPause() { dataHub.unregisterTrackDataListener(this); + dataHub = null; super.onPause(); } @@ -187,10 +189,6 @@ public class ChartActivity extends Activity implements TrackDataListener { runOnUiThread(updateChart); } - public boolean isSeriesEnabled(int index) { - return chartView.getChartValueSeries(index).isEnabled(); - } - @Override public boolean onCreateOptionsMenu(Menu menu) { super.onCreateOptionsMenu(menu); @@ -205,13 +203,36 @@ public class ChartActivity extends Activity implements TrackDataListener { public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case Constants.MENU_CHART_SETTINGS: - MyTracks.getInstance().getDialogManager().showDialogSafely( - DialogManager.DIALOG_CHART_SETTINGS); + showSettingsDialog(); return true; } return super.onOptionsItemSelected(item); } + private void showSettingsDialog() { + final ChartSettingsDialog settingsDialog = new ChartSettingsDialog(this); + settingsDialog.setOwnerActivity(this); + + settingsDialog.setMode(mode); + for (int i = 0; i < ChartView.NUM_SERIES; i++) { + settingsDialog.setSeriesEnabled(i, chartView.getChartValueSeries(i).isEnabled()); + } + + settingsDialog.setOnClickListener(new OnClickListener() { + @Override + public void onClick(DialogInterface arg0, int which) { + if (which != DialogInterface.BUTTON_POSITIVE) return; + + for (int i = 0; i < ChartView.NUM_SERIES; i++) { + chartView.getChartValueSeries(i).setEnabled(settingsDialog.isSeriesEnabled(i)); + } + setMode(settingsDialog.getMode()); + } + }); + + settingsDialog.show(); + } + /** * Given a location, creates a new data point for the chart. A data point is * an array double[3 or 6], where: diff --git a/MyTracks/src/com/google/android/apps/mytracks/ChartSettingsDialog.java b/MyTracks/src/com/google/android/apps/mytracks/ChartSettingsDialog.java index 34d64e82f..228cee046 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/ChartSettingsDialog.java +++ b/MyTracks/src/com/google/android/apps/mytracks/ChartSettingsDialog.java @@ -36,6 +36,7 @@ import android.widget.RadioGroup; public class ChartSettingsDialog extends Dialog { private RadioButton distance; private CheckBox[] series; + private OnClickListener clickListener; public ChartSettingsDialog(Context context) { super(context); @@ -44,23 +45,31 @@ public class ChartSettingsDialog extends Dialog { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + requestWindowFeature(Window.FEATURE_NO_TITLE); setContentView(R.layout.chart_settings); Button cancel = (Button) findViewById(R.id.chart_settings_cancel); cancel.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { + if (clickListener != null) { + clickListener.onClick(ChartSettingsDialog.this, BUTTON_NEGATIVE); + } dismiss(); } }); - Button ok = (Button) findViewById(R.id.chart_settings_ok); - ok.setOnClickListener(new View.OnClickListener() { + + Button okButton = (Button) findViewById(R.id.chart_settings_ok); + okButton.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { - handleOk(); + if (clickListener != null) { + clickListener.onClick(ChartSettingsDialog.this, BUTTON_POSITIVE); + } + dismiss(); } }); distance = (RadioButton) findViewById(R.id.chart_settings_by_distance); - + series = new CheckBox[ChartView.NUM_SERIES]; series[ChartView.ELEVATION_SERIES] = (CheckBox) findViewById(R.id.chart_settings_elevation); @@ -74,29 +83,30 @@ public class ChartSettingsDialog extends Dialog { (CheckBox) findViewById(R.id.chart_settings_heart_rate); } - public void setup(ChartActivity chart) { - if (chart == null) { - return; - } - + public void setMode(Mode mode) { RadioGroup rd = (RadioGroup) findViewById(R.id.chart_settings_x); - rd.check(chart.getMode() == Mode.BY_DISTANCE + rd.check(mode == Mode.BY_DISTANCE ? R.id.chart_settings_by_distance : R.id.chart_settings_by_time); - for (int i = 0; i < ChartView.NUM_SERIES; i++) { - series[i].setChecked(chart.isSeriesEnabled(i)); - } } - private void handleOk() { - ChartActivity chart = MyTracks.getInstance().getChartActivity(); - chart.setMode(distance.isChecked() ? Mode.BY_DISTANCE : Mode.BY_TIME); + public void setSeriesEnabled(int seriesIdx, boolean enabled) { + series[seriesIdx].setChecked(enabled); + } - // TODO: check that something is visible. - for (int i = 0; i < ChartView.NUM_SERIES; i++) { - chart.setSeriesEnabled(i, series[i].isChecked()); - } + public Mode getMode() { + if (distance == null) return Mode.BY_DISTANCE; - dismiss(); + return distance.isSelected() ? Mode.BY_DISTANCE : Mode.BY_TIME; + } + + public boolean isSeriesEnabled(int seriesIdx) { + if (series == null) return true; + + return series[seriesIdx].isChecked(); + } + + public void setOnClickListener(OnClickListener clickListener) { + this.clickListener = clickListener; } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/ChartView.java b/MyTracks/src/com/google/android/apps/mytracks/ChartView.java index 0a6e665ad..e9e308508 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/ChartView.java +++ b/MyTracks/src/com/google/android/apps/mytracks/ChartView.java @@ -49,6 +49,7 @@ import java.util.ArrayList; * @author Leif Hendrik Wilden */ public class ChartView extends View { + private static final int MIN_ZOOM_LEVEL = 1; /* * Scrolling logic: @@ -62,7 +63,6 @@ public class ChartView extends View { * Zoom logic: */ private int zoomLevel = 1; - private final int minZoomLevel = 1; private int maxZoomLevel = 10; private static final int MAX_INTERVALS = 5; @@ -333,7 +333,7 @@ public class ChartView extends View { * @return true if the chart can be zoomed out */ public boolean canZoomOut() { - return zoomLevel > minZoomLevel; + return zoomLevel > MIN_ZOOM_LEVEL; } /** @@ -593,7 +593,7 @@ public class ChartView extends View { final float x = getWaypointX(waypoint); c.drawLine(x, h - bottomBorder, x, topBorder, gridPaint); - c.translate(x - markerWidth / 2, markerHeight); + c.translate(x - (float) markerWidth / 2.0f, (float) markerHeight); if (waypoints.get(i).getType() == Waypoint.TYPE_STATISTICS) { statsMarker.draw(c); } else { diff --git a/MyTracks/src/com/google/android/apps/mytracks/Constants.java b/MyTracks/src/com/google/android/apps/mytracks/Constants.java index c3fc58acf..063bb6679 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/Constants.java +++ b/MyTracks/src/com/google/android/apps/mytracks/Constants.java @@ -50,7 +50,7 @@ public abstract class Constants { public static final int AUTHENTICATE_TO_DOCLIST = 10; public static final int AUTHENTICATE_TO_TRIX = 11; public static final int DELETE_TRACK = 13; - public static final int SEND_TO_GOOGLE_DIALOG = 14; + public static final int SEND_TO_GOOGLE = 14; public static final int SHARE_LINK = 15; public static final int SHARE_GPX_FILE = 16; public static final int SHARE_KML_FILE = 17; @@ -190,7 +190,7 @@ public abstract class Constants { public static int getActionFromMenuId(int menuId) { switch (menuId) { case Constants.MENU_SEND_TO_GOOGLE: - return Constants.SEND_TO_GOOGLE_DIALOG; + return Constants.SEND_TO_GOOGLE; case Constants.MENU_EDIT: return Constants.EDIT_DETAILS; case Constants.MENU_DELETE: diff --git a/MyTracks/src/com/google/android/apps/mytracks/DialogManager.java b/MyTracks/src/com/google/android/apps/mytracks/DialogManager.java index 2d2d17312..01eba2c3e 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/DialogManager.java +++ b/MyTracks/src/com/google/android/apps/mytracks/DialogManager.java @@ -17,14 +17,12 @@ package com.google.android.apps.mytracks; import static com.google.android.apps.mytracks.Constants.TAG; -import com.google.android.apps.mytracks.io.sendtogoogle.SendDialog; 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.os.Bundle; +import android.content.DialogInterface; import android.util.Log; import android.view.WindowManager.BadTokenException; @@ -35,148 +33,22 @@ import android.view.WindowManager.BadTokenException; */ public class DialogManager { - public static final int DIALOG_CHART_SETTINGS = 1; - public static final int DIALOG_IMPORT_PROGRESS = 2; - public static final int DIALOG_PROGRESS = 3; - public static final int DIALOG_SEND_TO_GOOGLE = 4; - - private ProgressDialog progressDialog; - private ProgressDialog importProgressDialog; - private SendDialog sendToGoogleDialog; - private ChartSettingsDialog chartSettingsDialog; - - private MyTracks activity; - - public DialogManager(MyTracks activity) { - this.activity = activity; - } - - protected Dialog onCreateDialog(int id, Bundle args) { - switch (id) { - case DIALOG_CHART_SETTINGS: - chartSettingsDialog = new ChartSettingsDialog(activity); - return chartSettingsDialog; - case DIALOG_IMPORT_PROGRESS: - importProgressDialog = new ProgressDialog(activity); - importProgressDialog.setIcon(android.R.drawable.ic_dialog_info); - importProgressDialog.setTitle( - activity.getString(R.string.progress_title)); - importProgressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER); - importProgressDialog.setMessage( - activity.getString(R.string.import_progress_message)); - return importProgressDialog; - case DIALOG_PROGRESS: - progressDialog = new ProgressDialog(activity); - progressDialog.setIcon(android.R.drawable.ic_dialog_info); - progressDialog.setTitle(activity.getString(R.string.progress_title)); - progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); - progressDialog.setMessage(""); - progressDialog.setMax(100); - progressDialog.setProgress(10); - return progressDialog; - case DIALOG_SEND_TO_GOOGLE: - sendToGoogleDialog = new SendDialog(activity); - return sendToGoogleDialog; - } - return null; - } - - protected void onPrepareDialog(int id, Dialog dialog) { - switch (id) { - case DIALOG_SEND_TO_GOOGLE: - activity.resetSendToGoogleStatus(); - break; - case DIALOG_CHART_SETTINGS: - Log.d(TAG, "MyTracks.onPrepare chart dialog"); - chartSettingsDialog.setup(activity.getChartActivity()); - break; - } - } - - public void setProgressMessage(final String message) { - activity.runOnUiThread(new Runnable() { - public void run() { - synchronized (this) { - if (progressDialog != null) { - progressDialog.setMessage(message); - } - } - } - }); - } - - public void setProgressValue(final int percent) { - activity.runOnUiThread(new Runnable() { - public void run() { - synchronized (this) { - if (progressDialog != null) { - progressDialog.setProgress(percent); - } - } - } - }); - } - - /** - * @return the sendToGoogleDialog - */ - public SendDialog getSendToGoogleDialog() { - return sendToGoogleDialog; - } - - /** - * 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) { - if (activity.isFinishing()) { + public static void showMessageDialog( + Activity ctx, int message, boolean success, DialogInterface.OnClickListener okListener) { + if (ctx.isFinishing()) { Log.w(TAG, "Activity finishing - not showing dialog"); return; } - activity.runOnUiThread(new Runnable() { - public void run() { - AlertDialog dialog = null; - AlertDialog.Builder builder = new AlertDialog.Builder(activity); - builder.setMessage(activity.getString(message)); - builder.setNegativeButton(activity.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(); - } - }); - } - - /** - * Just like showDialog, but will catch a {@link 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) { - if (activity.isFinishing()) { - Log.w(TAG, "Activity finishing - not showing dialog"); - return; - } - - activity.runOnUiThread(new Runnable() { - public void run() { - try { - activity.showDialog(id); - } catch (BadTokenException e) { - Log.w(TAG, "Could not display dialog with id " + id, e); - } catch (IllegalStateException e) { - Log.w(TAG, "Could not display dialog with id " + id, e); - } - } - }); + AlertDialog dialog = null; + AlertDialog.Builder builder = new AlertDialog.Builder(ctx); + builder.setMessage(message); + builder.setNeutralButton(R.string.ok, okListener); + 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(); } /** @@ -202,26 +74,6 @@ public class DialogManager { }); } - /** - * Dismisses the progress dialog if it is showing. Executed on the UI thread. - */ - public void dismissDialogSafely(final int id) { - if (activity.isFinishing()) { - Log.w(TAG, "Activity finishing - not dismissing dialog"); - return; - } - - activity.runOnUiThread(new Runnable() { - public void run() { - try { - activity.dismissDialog(id); - } catch (IllegalArgumentException e) { - // This will be thrown if this dialog was not shown before. - } - } - }); - } - /** * The equivalent of {@link #dismissDialogSafely(int)}, but for a specific * dialog instance. @@ -242,4 +94,6 @@ public class DialogManager { } }); } + + private DialogManager() {} } diff --git a/MyTracks/src/com/google/android/apps/mytracks/ImportAllTracks.java b/MyTracks/src/com/google/android/apps/mytracks/ImportAllTracks.java index 8826e40c7..0e23f3d8a 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/ImportAllTracks.java +++ b/MyTracks/src/com/google/android/apps/mytracks/ImportAllTracks.java @@ -104,9 +104,9 @@ public class ImportAllTracks { Log.i(Constants.TAG, "ImportAllTracks: Done"); AlertDialog.Builder builder = new AlertDialog.Builder(activity); if (gpxFileCount == 0) { - builder.setMessage(activity.getString(R.string.import_empty, gpxPath + "/")); + builder.setMessage(activity.getString(R.string.import_multi_empty, gpxPath + "/")); } else { - builder.setMessage(activity.getString(R.string.import_done, importSuccessCount, gpxFileCount, + builder.setMessage(activity.getString(R.string.import_multi_done, importSuccessCount, gpxFileCount, gpxPath + "/")); } builder.setPositiveButton(R.string.ok, null); diff --git a/MyTracks/src/com/google/android/apps/mytracks/MapActivity.java b/MyTracks/src/com/google/android/apps/mytracks/MapActivity.java index 1ac9c1f05..d7391a211 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/MapActivity.java +++ b/MyTracks/src/com/google/android/apps/mytracks/MapActivity.java @@ -23,6 +23,8 @@ import com.google.android.apps.mytracks.content.TrackDataHub; import com.google.android.apps.mytracks.content.TrackDataHub.ListenerDataType; import com.google.android.apps.mytracks.content.TrackDataListener; import com.google.android.apps.mytracks.content.Waypoint; +import com.google.android.apps.mytracks.io.file.SaveActivity; +import com.google.android.apps.mytracks.io.sendtogoogle.SendActivity; import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerFactory; import com.google.android.apps.mytracks.stats.TripStatistics; import com.google.android.apps.mytracks.util.ApiFeatures; @@ -122,7 +124,7 @@ public class MapActivity extends com.google.android.maps.MapActivity // ------------------------ @Override - public void onCreate(Bundle bundle) { + protected void onCreate(Bundle bundle) { Log.d(TAG, "MapActivity.onCreate"); super.onCreate(bundle); @@ -131,8 +133,6 @@ public class MapActivity extends com.google.android.maps.MapActivity new StatusAnnouncerFactory(ApiFeatures.getInstance()).getVolumeStream(); setVolumeControlStream(volumeStream); - dataHub = MyTracks.getInstance().getDataHub(); - // We don't need a window title bar: requestWindowFeature(Window.FEATURE_NO_TITLE); @@ -179,9 +179,10 @@ public class MapActivity extends com.google.android.maps.MapActivity @Override protected void onResume() { - Log.d(TAG, "MapActivity.onStart"); + Log.d(TAG, "MapActivity.onResume"); super.onResume(); + dataHub = TrackDataHub.getStartedInstance(); dataHub.registerTrackDataListener(this, EnumSet.of( ListenerDataType.SELECTED_TRACK_CHANGED, ListenerDataType.POINT_UPDATES, @@ -202,9 +203,10 @@ public class MapActivity extends com.google.android.maps.MapActivity @Override protected void onPause() { - Log.d(TAG, "MapActivity.onStop"); + Log.d(TAG, "MapActivity.onPause"); dataHub.unregisterTrackDataListener(this); + dataHub = null; super.onPause(); } @@ -392,13 +394,27 @@ public class MapActivity extends com.google.android.maps.MapActivity @Override public boolean onMenuItemSelected(int featureId, MenuItem item) { - if (!super.onMenuItemSelected(featureId, item)) { - MyTracks.getInstance().onActivityResult( - Constants.getActionFromMenuId(item.getItemId()), RESULT_OK, - new Intent()); - return true; + switch (item.getItemId()) { + case Constants.MENU_SEND_TO_GOOGLE: + SendActivity.sendToGoogle(this, dataHub.getSelectedTrackId(), false); + return true; + case Constants.MENU_SHARE_LINK: + SendActivity.sendToGoogle(this, dataHub.getSelectedTrackId(), true); + return true; + case Constants.MENU_SAVE_GPX_FILE: + case Constants.MENU_SAVE_KML_FILE: + case Constants.MENU_SAVE_CSV_FILE: + case Constants.MENU_SAVE_TCX_FILE: + case Constants.MENU_SHARE_GPX_FILE: + case Constants.MENU_SHARE_KML_FILE: + case Constants.MENU_SHARE_CSV_FILE: + case Constants.MENU_SHARE_TCX_FILE: + SaveActivity.handleExportTrackAction(this, dataHub.getSelectedTrackId(), + Constants.getActionFromMenuId(item.getItemId())); + return true; + default: + return super.onMenuItemSelected(featureId, item); } - return false; } @Override diff --git a/MyTracks/src/com/google/android/apps/mytracks/MapOverlay.java b/MyTracks/src/com/google/android/apps/mytracks/MapOverlay.java index 403b1abe5..0203d8d6b 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/MapOverlay.java +++ b/MyTracks/src/com/google/android/apps/mytracks/MapOverlay.java @@ -175,14 +175,18 @@ public class MapOverlay extends Overlay { */ public void addLocation(Location l) { // Queue up in the pending queue until it's merged with {@code #points}. - pendingPoints.offer(new CachedLocation(l)); + if (!pendingPoints.offer(new CachedLocation(l))) { + Log.e(TAG, "Unable to add pending points"); + } } /** * Adds a segment split to the map overlay. */ public void addSegmentSplit() { - pendingPoints.offer(new CachedLocation()); + if (!pendingPoints.offer(new CachedLocation())) { + Log.e(TAG, "Unable to add pending points"); + } } public void addWaypoint(Waypoint wpt) { diff --git a/MyTracks/src/com/google/android/apps/mytracks/MyMapsList.java b/MyTracks/src/com/google/android/apps/mytracks/MyMapsList.java index 7b74ac606..3e3f0096c 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/MyMapsList.java +++ b/MyTracks/src/com/google/android/apps/mytracks/MyMapsList.java @@ -1,12 +1,12 @@ /* * Copyright 2011 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 @@ -25,11 +25,7 @@ import com.google.android.maps.mytracks.R; import android.app.Activity; import android.content.Intent; import android.os.Bundle; -import android.view.ContextMenu; -import android.view.ContextMenu.ContextMenuInfo; -import android.view.MenuItem; import android.view.View; -import android.view.View.OnCreateContextMenuListener; import android.widget.AdapterView; import android.widget.AdapterView.OnItemClickListener; import android.widget.ListView; @@ -42,29 +38,12 @@ import android.widget.TextView; * @author Rodrigo Damazio */ public class MyMapsList extends Activity implements MapsFacade.MapsListCallback { - private static final int MENU_OPEN = 0; - private static final int MENU_SHARE = 2; private static final int GET_LOGIN = 1; private MapsFacade mapsClient; private AuthManager auth; private MyMapsListAdapter listAdapter; - private int contextPosition; - - private final OnCreateContextMenuListener contextMenuListener = - new OnCreateContextMenuListener() { - @Override - public void onCreateContextMenu(ContextMenu menu, View v, - ContextMenuInfo menuInfo) { - AdapterView.AdapterContextMenuInfo info = - (AdapterView.AdapterContextMenuInfo) menuInfo; - contextPosition = info.position; - menu.add(0, MENU_OPEN, 0, R.string.open_map); - menu.add(0, MENU_SHARE, 0, R.string.share_map); - } - }; - private final OnItemClickListener clickListener = new OnItemClickListener() { @Override @@ -91,7 +70,6 @@ public class MyMapsList extends Activity implements MapsFacade.MapsListCallback ListView list = (ListView) findViewById(R.id.maplist); list.setOnItemClickListener(clickListener); - list.setOnCreateContextMenuListener(contextMenuListener); list.setAdapter(listAdapter); startLogin(); @@ -101,7 +79,8 @@ public class MyMapsList extends Activity implements MapsFacade.MapsListCallback // Starts in the UI thread. // TODO fix this for non-froyo devices. if (AuthManagerFactory.useModernAuthManager()) { - MyTracks.getInstance().getAccountChooser().chooseAccount( + AccountChooser accountChooser = new AccountChooser(); + accountChooser.chooseAccount( MyMapsList.this, new AccountChooser.AccountHandler() { @Override @@ -111,6 +90,8 @@ public class MyMapsList extends Activity implements MapsFacade.MapsListCallback // The user did not quit and there was a valid google // account. doLogin(account); + } else { + finish(); } } }); @@ -183,27 +164,4 @@ public class MyMapsList extends Activity implements MapsFacade.MapsListCallback } super.onActivityResult(requestCode, resultCode, data); } - - @Override - public boolean onMenuItemSelected(int featureId, MenuItem item) { - switch (item.getItemId()) { - case MENU_OPEN: - clickListener.onItemClick(null, null, contextPosition, 0); - return true; - case MENU_SHARE: - Intent shareIntent = new Intent(Intent.ACTION_SEND); - shareIntent.setType("text/plain"); - shareIntent.putExtra(Intent.EXTRA_SUBJECT, - getText(R.string.share_map_subject)); - String[] listItem = (String[]) listAdapter.getMapListingArray(contextPosition); - shareIntent.putExtra(Intent.EXTRA_TEXT, String.format( - getText(R.string.share_map_body_format).toString(), - listItem[1], - MapsFacade.buildMapUrl(listItem[0]))); - startActivity(Intent.createChooser(shareIntent, - getText(R.string.share_map).toString())); - return true; - } - return false; - } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java b/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java index 11fab5f83..ed3914120 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java +++ b/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java @@ -16,58 +16,33 @@ package com.google.android.apps.mytracks; import static com.google.android.apps.mytracks.Constants.TAG; -import static com.google.android.apps.mytracks.DialogManager.DIALOG_IMPORT_PROGRESS; -import static com.google.android.apps.mytracks.DialogManager.DIALOG_PROGRESS; -import static com.google.android.apps.mytracks.DialogManager.DIALOG_SEND_TO_GOOGLE; -import com.google.android.accounts.Account; import com.google.android.apps.analytics.GoogleAnalyticsTracker; import com.google.android.apps.mytracks.content.MyTracksProviderUtils; -import com.google.android.apps.mytracks.content.Track; import com.google.android.apps.mytracks.content.TrackDataHub; +import com.google.android.apps.mytracks.content.TracksColumns; import com.google.android.apps.mytracks.content.WaypointCreationRequest; -import com.google.android.apps.mytracks.io.AuthManager; -import com.google.android.apps.mytracks.io.AuthManagerFactory; -import com.google.android.apps.mytracks.io.SendToDocs; -import com.google.android.apps.mytracks.io.SendToFusionTables; -import com.google.android.apps.mytracks.io.SendToFusionTables.OnSendCompletedListener; -import com.google.android.apps.mytracks.io.SendToMyMaps; -import com.google.android.apps.mytracks.io.file.GpxImporter; import com.google.android.apps.mytracks.io.file.TempFileCleaner; -import com.google.android.apps.mytracks.io.file.TrackWriter; -import com.google.android.apps.mytracks.io.file.TrackWriterFactory; -import com.google.android.apps.mytracks.io.file.TrackWriterFactory.TrackFileFormat; -import com.google.android.apps.mytracks.io.mymaps.MapsFacade; -import com.google.android.apps.mytracks.io.mymaps.MyMapsConstants; -import com.google.android.apps.mytracks.io.sendtogoogle.ResultDialogFactory; -import com.google.android.apps.mytracks.io.sendtogoogle.SendDialog; -import com.google.android.apps.mytracks.io.sendtogoogle.SendResult; -import com.google.android.apps.mytracks.io.sendtogoogle.SendType; import com.google.android.apps.mytracks.services.ITrackRecordingService; -import com.google.android.apps.mytracks.services.TrackRecordingService; +import com.google.android.apps.mytracks.services.ServiceUtils; +import com.google.android.apps.mytracks.services.TrackRecordingServiceConnection; import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerFactory; import com.google.android.apps.mytracks.util.ApiFeatures; -import com.google.android.apps.mytracks.util.FileUtils; import com.google.android.apps.mytracks.util.SystemUtils; +import com.google.android.apps.mytracks.util.UriUtils; import com.google.android.maps.mytracks.R; import android.app.Activity; import android.app.AlertDialog; -import android.app.Dialog; import android.app.TabActivity; -import android.content.ComponentName; +import android.content.ContentUris; import android.content.DialogInterface; import android.content.Intent; -import android.content.ServiceConnection; import android.content.SharedPreferences; import android.content.res.Resources; import android.net.Uri; import android.os.Bundle; -import android.os.Handler; -import android.os.HandlerThread; -import android.os.IBinder; import android.os.RemoteException; -import android.text.TextUtils; import android.util.Log; import android.view.Menu; import android.view.MenuItem; @@ -80,65 +55,24 @@ 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.ArrayList; -import java.util.HashMap; -import java.util.List; - -import javax.xml.parsers.ParserConfigurationException; - -import org.xml.sax.SAXException; - /** * The super activity that embeds our sub activities. * * @author Leif Hendrik Wilden + * @author Rodrigo Damazio */ -public class MyTracks extends TabActivity implements OnTouchListener, - ProgressIndicator { - /** - * Singleton instance - */ - private static MyTracks instance; - +public class MyTracks extends TabActivity implements OnTouchListener { private TrackDataHub dataHub; - private ChartActivity chartActivity; - /* - * Authentication - */ - private AuthManager auth; - private final HashMap authMap = - new HashMap(); - private final AccountChooser accountChooser = new AccountChooser(); - - /* - * Dialogs manager. - */ - private DialogManager dialogManager; - - /* + /** * Menu manager. */ private MenuManager menuManager; - /* - * Information on upload success to MyMaps/Docs. - * Used by SendToGoogleResultDialog. + /** + * Preferences. */ - - public long sendToTrackId = -1; - public boolean sendToMyMapsSuccess = false; - public boolean sendToFusionTablesSuccess = false; - public boolean sendToDocsSuccess = false; - public String sendToMyMapsMapId; - public String sendToMyMapsMessage = ""; - public String sendToFusionTablesTableId; - public String sendToFusionTablesMessage = ""; - public String sendToDocsMessage = ""; + private SharedPreferences preferences; /** * True if a new track should be created after the track recording service @@ -146,58 +80,15 @@ public class MyTracks extends TabActivity implements OnTouchListener, */ private boolean startNewTrackRequested = false; - private ITrackRecordingService trackRecordingService; - - /** - * Does the user want to share the current track. - */ - private boolean shareRequested = false; - /** * Utilities to deal with the database. */ private MyTracksProviderUtils providerUtils; - private SharedPreferences sharedPreferences; - /** * Google Analytics tracker */ private GoogleAnalyticsTracker tracker; - - /** - * The connection to the track recording service. - */ - private final ServiceConnection serviceConnection = new ServiceConnection() { - @Override - public void onServiceConnected(ComponentName className, IBinder service) { - Log.d(Constants.TAG, "MyTracks: Service now connected."); - // Delay setting the service until we are done with initialization. - ITrackRecordingService trackRecordingService = - ITrackRecordingService.Stub.asInterface(service); - try { - // TODO: Send a start service intent and broadcast service started - // message to avoid the hack below and a race condition. - if (startNewTrackRequested) { - startNewTrackRequested = false; - startRecordingNewTrack(trackRecordingService); - } - } finally { - MyTracks.this.trackRecordingService = trackRecordingService; - } - } - - @Override - public void onServiceDisconnected(ComponentName className) { - Log.d(TAG, "MyTracks: Service now disconnected."); - trackRecordingService = null; - } - }; - - /** - * Whether {@link #serviceConnection} is bound or not. - */ - private boolean isBound = false; /* * Tabs/View navigation: @@ -211,35 +102,27 @@ public class MyTracks extends TabActivity implements OnTouchListener, } }; - 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. + /* + * Recording service interaction: */ - public boolean isRecording() { - if (trackRecordingService == null) { - // Fall back to alternative check method. - return dataHub.isRecording(); - } - try { - return trackRecordingService.isRecording(); - // TODO: We catch Exception, because after eliminating the service process - // all exceptions it may throw are no longer wrapped in a RemoteException. - } catch (Exception e) { - Log.e(TAG, "MyTracks: Remote exception.", e); - // Fall back to alternative check method. - return dataHub.isRecording(); + private final Runnable serviceBindCallback = new Runnable() { + @Override + public void run() { + synchronized (serviceConnection) { + ITrackRecordingService service = serviceConnection.getServiceIfBound(); + if (startNewTrackRequested && service != null) { + Log.i(TAG, "Starting recording"); + startNewTrackRequested = false; + startRecordingNewTrack(service); + } else if (startNewTrackRequested) { + Log.w(TAG, "Not yet starting recording"); + } + } } - } + }; + + private TrackRecordingServiceConnection serviceConnection; /* * Application lifetime events: @@ -250,7 +133,6 @@ public class MyTracks extends TabActivity implements OnTouchListener, protected void onCreate(Bundle savedInstanceState) { Log.d(TAG, "MyTracks.onCreate"); super.onCreate(savedInstanceState); - instance = this; ApiFeatures apiFeatures = ApiFeatures.getInstance(); if (!SystemUtils.isRelease(this)) { apiFeatures.getApiPlatformAdapter().enableStrictMode(); @@ -258,16 +140,16 @@ public class MyTracks extends TabActivity implements OnTouchListener, tracker = GoogleAnalyticsTracker.getInstance(); // Start the tracker in manual dispatch mode... - tracker.start(this.getString(R.string.google_analytics_id), this.getApplicationContext()); + tracker.start(getString(R.string.google_analytics_id), getApplicationContext()); tracker.setProductVersion("android-mytracks", SystemUtils.getMyTracksVersion(this)); tracker.trackPageView("/appstart"); tracker.dispatch(); providerUtils = MyTracksProviderUtils.Factory.get(this); - sharedPreferences = getSharedPreferences(Constants.SETTINGS_NAME, 0); - dataHub = new TrackDataHub(this, sharedPreferences, providerUtils); + preferences = getSharedPreferences(Constants.SETTINGS_NAME, 0); + dataHub = TrackDataHub.newInstance(this); menuManager = new MenuManager(this); - dialogManager = new DialogManager(this); + serviceConnection = new TrackRecordingServiceConnection(this, serviceBindCallback); // The volume we want to control is the Text-To-Speech volume int volumeStream = @@ -309,67 +191,6 @@ public class MyTracks extends TabActivity implements OnTouchListener, // This will show the eula until the user accepts or quits the app. Eula.showEulaRequireAcceptance(this); - - // Check if we got invoked via the VIEW intent: - Intent intent = getIntent(); - String action; - if (intent != null && (action = intent.getAction()) != null) { - if (action.equals(Intent.ACTION_MAIN)) { - // Do nothing. - } else if (action.equals(Intent.ACTION_VIEW)) { - if (intent.getScheme() != null && intent.getScheme().equals("file")) { - Log.w(TAG, "Received a VIEW intent with file scheme. Importing."); - importGpxFile(intent.getData().getPath()); - } else { - Log.w(TAG, "Received a VIEW intent with unsupported scheme " + intent.getScheme()); - } - } else { - Log.w(TAG, "Received an intent with unsupported action " + action); - } - } else { - Log.d(TAG, "Received an intent with no action."); - } - } - - @Override - protected void onDestroy() { - Log.d(TAG, "MyTracks.onDestroy"); - - dataHub.destroy(); - - tryUnbindTrackRecordingService(); - tracker.dispatch(); - tracker.stop(); - super.onDestroy(); - } - - @Override - protected void onStop() { - Log.d(TAG, "MyTracks.onStop"); - - dataHub.stop(); - - // Clean up any temporary track files. - TempFileCleaner.clean(); - super.onStop(); - } - - @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(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(TAG, "MyTracks.onResume"); - tryBindTrackRecordingService(); - super.onResume(); } @Override @@ -378,17 +199,60 @@ public class MyTracks extends TabActivity implements OnTouchListener, super.onStart(); dataHub.start(); - // Ensure that service is running if we're supposed to be recording - if (dataHub.isRecording()) { - Intent startIntent = new Intent(this, TrackRecordingService.class); - startService(startIntent); + // Ensure that service is running and bound if we're supposed to be recording + if (ServiceUtils.isRecording(this, null, preferences)) { + serviceConnection.startAndBind(); + } + + Intent intent = getIntent(); + String action = intent.getAction(); + Uri data = intent.getData(); + if ((Intent.ACTION_VIEW.equals(action) || Intent.ACTION_EDIT.equals(action)) + && TracksColumns.CONTENT_ITEMTYPE.equals(intent.getType()) + && UriUtils.matchesContentUri(data, TracksColumns.CONTENT_URI)) { + long trackId = ContentUris.parseId(data); + dataHub.loadTrack(trackId); } } - /* - * Menu events: - * ============ - */ + @Override + protected void onResume() { + // Called when the current activity is being displayed or re-displayed + // to the user. + Log.d(TAG, "MyTracks.onResume"); + serviceConnection.bindIfRunning(); + super.onResume(); + } + + @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(TAG, "MyTracks.onPause"); + super.onPause(); + } + + @Override + protected void onStop() { + Log.d(TAG, "MyTracks.onStop"); + + dataHub.stop(); + + tracker.dispatch(); + tracker.stop(); + + // Clean up any temporary track files. + TempFileCleaner.clean(); + super.onStop(); + } + + @Override + protected void onDestroy() { + Log.d(TAG, "MyTracks.onDestroy"); + serviceConnection.unbind(); + + super.onDestroy(); + } @Override public boolean onCreateOptionsMenu(Menu menu) { @@ -399,7 +263,8 @@ public class MyTracks extends TabActivity implements OnTouchListener, @Override public boolean onPrepareOptionsMenu(Menu menu) { menuManager.onPrepareOptionsMenu(menu, providerUtils.getLastTrack() != null, - isRecording(), dataHub.isATrackSelected()); + ServiceUtils.isRecording(this, serviceConnection.getServiceIfBound(), preferences), + dataHub.isATrackSelected()); return super.onPrepareOptionsMenu(menu); } @@ -410,27 +275,6 @@ public class MyTracks extends TabActivity implements OnTouchListener, : super.onOptionsItemSelected(item); } - /* - * Dialog events: - * ============== - */ - - @Override - protected Dialog onCreateDialog(int id, Bundle args) { - return dialogManager.onCreateDialog(id, args); - } - - @Override - protected Dialog onCreateDialog(int id) { - return dialogManager.onCreateDialog(id, null); - } - - @Override - protected void onPrepareDialog(int id, Dialog dialog) { - super.onPrepareDialog(id, dialog); - dialogManager.onPrepareDialog(id, dialog); - } - /* * Key events: * =========== @@ -438,8 +282,8 @@ public class MyTracks extends TabActivity implements OnTouchListener, @Override public boolean onTrackballEvent(MotionEvent event) { - if (isRecording()) { - if (event.getAction() == MotionEvent.ACTION_DOWN) { + if (event.getAction() == MotionEvent.ACTION_DOWN) { + if (ServiceUtils.isRecording(this, serviceConnection.getServiceIfBound(), preferences)) { try { insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS); } catch (RemoteException e) { @@ -450,6 +294,7 @@ public class MyTracks extends TabActivity implements OnTouchListener, return true; } } + return super.onTrackballEvent(event); } @@ -457,19 +302,12 @@ public class MyTracks extends TabActivity implements OnTouchListener, public void onActivityResult(int requestCode, int resultCode, final Intent results) { Log.d(TAG, "MyTracks.onActivityResult"); - TrackFileFormat exportFormat = null; long trackId = dataHub.getSelectedTrackId(); if (results != null) { trackId = results.getLongExtra("trackid", trackId); } switch (requestCode) { - case Constants.GET_LOGIN: { - if (resultCode != RESULT_OK || auth == null || !auth.authResult(resultCode, results)) { - dialogManager.dismissDialogSafely(DIALOG_PROGRESS); - } - break; - } case Constants.SHOW_TRACK: { if (results != null) { if (trackId >= 0) { @@ -512,121 +350,6 @@ public class MyTracks extends TabActivity implements OnTouchListener, } break; } - case Constants.SEND_TO_GOOGLE_DIALOG: { - shareRequested = false; - dialogManager.showDialogSafely(DIALOG_SEND_TO_GOOGLE); - break; - } - case Constants.GET_MAP: { - // User picked a map to upload to - if (resultCode == RESULT_OK) { - results.putExtra("trackid", dataHub.getSelectedTrackId()); - if (results.hasExtra("mapid")) { - sendToMyMapsMapId = results.getStringExtra("mapid"); - } - authenticateToGoogleMaps(results); - } else { - onSendToGoogleDone(); - } - break; - } - case Constants.AUTHENTICATE_TO_MY_MAPS: { - // Authenticated with Google My Maps - if (results != null && resultCode == RESULT_OK) { - final String mapId; - if (results.hasExtra("mapid")) { - mapId = results.getStringExtra("mapid"); - } else { - mapId = "new"; - } - - sendToGoogleMaps(trackId, mapId); - } else { - onSendToGoogleDone(); - } - break; - } - case Constants.AUTHENTICATE_TO_FUSION_TABLES: { - // Authenticated with Google Fusion Tables - if (results != null && resultCode == RESULT_OK) { - sendToFusionTables(trackId); - } else { - onSendToGoogleDone(); - } - break; - } - case Constants.AUTHENTICATE_TO_DOCLIST: { - // Authenticated with Google Docs - if (resultCode == RESULT_OK) { - authenticateToGoogleTrix(); - } else { - onSendToGoogleDone(); - } - break; - } - case Constants.AUTHENTICATE_TO_TRIX: { - // Authenticated with Trix - if (resultCode == RESULT_OK) { - sendToGoogleDocs(trackId); - } else { - onSendToGoogleDone(); - } - break; - } - case Constants.SAVE_GPX_FILE: - if (exportFormat == null) { exportFormat = TrackFileFormat.GPX; } - //$FALL-THROUGH$ - case Constants.SAVE_KML_FILE: - if (exportFormat == null) { exportFormat = TrackFileFormat.KML; } - //$FALL-THROUGH$ - case Constants.SAVE_CSV_FILE: - if (exportFormat == null) { exportFormat = TrackFileFormat.CSV; } - //$FALL-THROUGH$ - case Constants.SAVE_TCX_FILE: - if (exportFormat == null) { exportFormat = TrackFileFormat.TCX; } - - if (results != null && resultCode == Activity.RESULT_OK) { - if (trackId >= 0) { - saveTrack(trackId, exportFormat); - } - } - break; - case Constants.SHARE_LINK: { - Track selectedTrack = providerUtils.getTrack(dataHub.getSelectedTrackId()); - if (selectedTrack != null) { - if (!TextUtils.isEmpty(selectedTrack.getMapId())) { - shareLinkToMap(MapsFacade.buildMapUrl(selectedTrack.getMapId())); - } else if (!TextUtils.isEmpty(selectedTrack.getTableId())) { - String url = getFusionTablesUrl(dataHub.getSelectedTrackId()); - if (url != null) { - shareLinkToMap(url); - } - } else { - shareRequested = true; - dialogManager.showDialogSafely(DIALOG_SEND_TO_GOOGLE); - } - } - break; - } - case Constants.SHARE_GPX_FILE: - if (exportFormat == null) { exportFormat = TrackFileFormat.GPX; } - //$FALL-THROUGH$ - case Constants.SHARE_KML_FILE: - if (exportFormat == null) { exportFormat = TrackFileFormat.KML; } - //$FALL-THROUGH$ - case Constants.SHARE_CSV_FILE: - if (exportFormat == null) { exportFormat = TrackFileFormat.CSV; } - //$FALL-THROUGH$ - case Constants.SHARE_TCX_FILE: { - if (exportFormat == null) { exportFormat = TrackFileFormat.TCX; } - - if (results != null && resultCode == Activity.RESULT_OK) { - if (trackId >= 0) { - sendTrack(trackId, exportFormat); - } - } - break; - } case Constants.CLEAR_MAP: { dataHub.unloadCurrentTrack(); break; @@ -650,107 +373,6 @@ public class MyTracks extends TabActivity implements OnTouchListener, return false; } - /** - * Resets status information for sending to MyMaps/Docs. - */ - public void resetSendToGoogleStatus() { - sendToMyMapsMapId = null; - sendToMyMapsMessage = ""; - sendToMyMapsSuccess = true; - sendToFusionTablesMessage = ""; - sendToFusionTablesSuccess = true; - sendToDocsMessage = ""; - sendToDocsSuccess = true; - sendToFusionTablesTableId = null; - } - - private void importGpxFile(final String fileName) { - dialogManager.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(TAG, "Caught an unexpected exception.", e); - message = R.string.error_generic; - } catch (ParserConfigurationException e) { - Log.e(TAG, "Caught an unexpected exception.", e); - message = R.string.error_generic; - } catch (IOException e) { - Log.e(TAG, "Caught an unexpected exception.", e); - message = R.string.error_unable_to_read_file; - } catch (NullPointerException e) { - Log.e(TAG, "Caught an unexpected exception.", e); - message = R.string.error_invalid_gpx_format; - } catch (OutOfMemoryError e) { - Log.e(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 - dataHub.loadTrack(trackIdsImported[trackIdsImported.length - 1]); - } else { - dialogManager.showMessageDialog(message, false/* success */); - } - } finally { - runOnUiThread(new Runnable() { - public void run() { - dismissDialog(DIALOG_IMPORT_PROGRESS); - } - }); - } - } - }; - t.start(); - } - - // ProgressIndicator implementation - - @Override - public void setProgressMessage(int resId) { - dialogManager.setProgressMessage(getString(resId)); - } - - @Override - public void clearProgressMessage() { - dialogManager.setProgressMessage(""); - } - - @Override - public void setProgressValue(final int percent) { - dialogManager.setProgressValue(percent); - } - - /** - * Shares a link to a My Map or Fusion Table via external app (email, gmail, ...) - * A chooser with apps that support text/plain will be shown to the user. - */ - public void shareLinkToMap(String url) { - Intent shareIntent = new Intent(Intent.ACTION_SEND); - shareIntent.setType("text/plain"); - shareIntent.putExtra(Intent.EXTRA_SUBJECT, - getResources().getText(R.string.share_map_subject).toString()); - - boolean shareUrlOnly = true; - if (sharedPreferences != null) { - shareUrlOnly = sharedPreferences.getBoolean( - getString(R.string.share_url_only_key), false); - } - - 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. @@ -784,10 +406,13 @@ public class MyTracks extends TabActivity implements OnTouchListener, /** * Inserts a waypoint marker. * + * TODO: Merge with WaypointsList#insertWaypoint. + * * @return Id of the inserted statistics marker. * @throws RemoteException If the call on the service failed. */ - public long insertWaypoint(WaypointCreationRequest request) throws RemoteException { + private long insertWaypoint(WaypointCreationRequest request) throws RemoteException { + ITrackRecordingService trackRecordingService = serviceConnection.getServiceIfBound(); if (trackRecordingService == null) { throw new IllegalStateException("The recording service is not bound."); } @@ -805,55 +430,6 @@ public class MyTracks extends TabActivity implements OnTouchListener, } } - /** - * 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(TAG, "Creating a new authentication for service: " + service); - auth = AuthManagerFactory.getAuthManager(this, - Constants.GET_LOGIN, - null, - true, - service); - authMap.put(service, auth); - } - Log.d(TAG, "Logging 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) { - dialogManager.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(TAG, "Loggin success for " + service + "!"); - onActivityResult(requestCode, RESULT_OK, results); - } - }, account); - } - private void startRecordingNewTrack( ITrackRecordingService trackRecordingService) { try { @@ -876,14 +452,16 @@ public class MyTracks extends TabActivity implements OnTouchListener, * Starts the track recording service (if not already running) and binds to * it. Starts recording a new track. */ - public void startRecording() { - if (trackRecordingService == null) { + void startRecording() { + synchronized (serviceConnection) { startNewTrackRequested = true; - Intent startIntent = new Intent(this, TrackRecordingService.class); - startService(startIntent); - tryBindTrackRecordingService(); - } else { - startRecordingNewTrack(trackRecordingService); + serviceConnection.startAndBind(); + + // Binding was already requested before, it either already happened + // (in which case running the callback manually triggers the actual recording start) + // or it will happen in the future + // (in which case running the callback now will have no effect). + serviceBindCallback.run(); } } @@ -891,327 +469,28 @@ public class MyTracks extends TabActivity implements OnTouchListener, * 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() { + void stopRecording() { + // Save the track id as the shared preference will overwrite the recording track id. + SharedPreferences sharedPreferences = getSharedPreferences(Constants.SETTINGS_NAME, 0); + long currentTrackId = sharedPreferences.getLong(getString(R.string.recording_track_key), -1); + + ITrackRecordingService trackRecordingService = serviceConnection.getServiceIfBound(); if (trackRecordingService != null) { - // Save the track id as the shared preference will overwrite the recording track id. - long currentTrackId = sharedPreferences.getLong(getString(R.string.recording_track_key), -1); try { trackRecordingService.endCurrentTrack(); - // TODO: We catch Exception, because after eliminating the service process - // all exceptions it may throw are no longer wrapped in a RemoteException. } catch (Exception e) { Log.e(TAG, "Unable to stop recording.", e); } - - if (currentTrackId > 0) { - Intent intent = new Intent(MyTracks.this, TrackDetails.class); - intent.putExtra("trackid", currentTrackId); - intent.putExtra("hasCancelButton", false); - startActivity(intent); - } - } - tryUnbindTrackRecordingService(); - try { - stopService(new Intent(MyTracks.this, TrackRecordingService.class)); - } catch (SecurityException e) { - Log.e(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. - * - * TODO: Change this whole flow to an actual state machine. - */ - public void sendToGoogle() { - SendDialog sendToGoogleDialog = - dialogManager.getSendToGoogleDialog(); - if (sendToGoogleDialog == null) { - return; - } - setProgressValue(0); - clearProgressMessage(); - dialogManager.showDialogSafely(DIALOG_PROGRESS); - - if (sendToGoogleDialog.getSendToMyMaps()) { - sendToGoogleMapsOrPickMap(sendToGoogleDialog); - } else if (sendToGoogleDialog.getSendToFusionTables()) { - authenticateToFusionTables(null); - } else if (sendToGoogleDialog.getSendToDocs()) { - authenticateToGoogleDocs(); - } else { - Log.w(TAG, "Nowhere to upload to"); - onSendToGoogleDone(); - } - } - - private void sendToGoogleMapsOrPickMap(SendDialog sendToGoogleDialog) { - tracker.trackPageView("/send/maps"); - if (!sendToGoogleDialog.getCreateNewMap()) { - // Ask the user to choose a map to upload into - Intent listIntent = new Intent(this, MyMapsList.class); - startActivityForResult(listIntent, Constants.GET_MAP); - // The callback for GET_MAP calls authenticateToGoogleMaps - } else { - authenticateToGoogleMaps(null); - } - } - - private void authenticateToGoogleMaps(Intent results) { - if (results == null) { results = new Intent(); } - - setProgressValue(0); - setProgressMessage( - R.string.progress_message_authenticating_mymaps); - authenticate(results, Constants.AUTHENTICATE_TO_MY_MAPS, - MyMapsConstants.SERVICE_NAME); - // AUTHENTICATE_TO_MY_MAPS callback calls sendToGoogleMaps - } - - private void sendToGoogleMaps(final long trackId, String mapId) { - SendToMyMaps.OnSendCompletedListener onCompletion = new SendToMyMaps.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); - if (track != null) { - track.setMapId(mapId); - providerUtils.updateTrack(track); - } else { - Log.w(TAG, "Updating map id failed."); - } - } catch (RuntimeException e) { - // If that fails whatever reasons we'll just log an error, but - // continue. - Log.w(TAG, "Updating map id failed.", e); - } - } - - onSendToGoogleMapsDone(); - } - }; - 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); - } - - private void onSendToGoogleMapsDone() { - SendDialog sendToGoogleDialog = dialogManager.getSendToGoogleDialog(); - if (sendToGoogleDialog.getSendToFusionTables()) { - authenticateToFusionTables(null); - } else if (sendToGoogleDialog.getSendToDocs()) { - authenticateToGoogleDocs(); - } else { - onSendToGoogleDone(); - } - } - - private void authenticateToFusionTables(Intent results) { - if (results == null) { results = new Intent(); } - - setProgressValue(0); - setProgressMessage(R.string.progress_message_authenticating_fusiontables); - authenticate(results, Constants.AUTHENTICATE_TO_FUSION_TABLES, - SendToFusionTables.SERVICE_ID); - // AUTHENTICATE_TO_FUSION_TABLES callback calls sendToFusionTables - } - - private void sendToFusionTables(final long trackId) { - tracker.trackPageView("/send/fusion_tables"); - OnSendCompletedListener onCompletion = new OnSendCompletedListener() { - @Override - public void onSendCompleted(String tableId, boolean success, - int statusMessage) { - sendToFusionTablesMessage = getString(statusMessage); - sendToFusionTablesSuccess = success; - if (sendToFusionTablesSuccess) { - sendToFusionTablesTableId = tableId; - // Update the table id for this track: - try { - Track track = providerUtils.getTrack(trackId); - if (track != null) { - track.setTableId(tableId); - providerUtils.updateTrack(track); - } else { - Log.w(TAG, "Updating table id failed."); - } - } catch (RuntimeException e) { - // If that fails whatever reasons we'll just log an error, but - // continue. - Log.w(TAG, "Updating table id failed.", e); - } - } - - onSendToFusionTablesDone(); - } - }; - sendToTrackId = trackId; - final SendToFusionTables sender = new SendToFusionTables(this, auth, - trackId, this/*progressIndicator*/, onCompletion); - - HandlerThread handlerThread = new HandlerThread("SendToFusionTables"); - handlerThread.start(); - Handler handler = new Handler(handlerThread.getLooper()); - handler.post(sender); - } - - private void onSendToFusionTablesDone() { - SendDialog sendToGoogleDialog = dialogManager.getSendToGoogleDialog(); - if (sendToGoogleDialog.getSendToDocs()) { - authenticateToGoogleDocs(); - } else { - onSendToGoogleDone(); - } - } - - private void authenticateToGoogleDocs() { - setProgressValue(0); - setProgressMessage( - R.string.progress_message_authenticating_docs); - authenticate(new Intent(), - Constants.AUTHENTICATE_TO_DOCLIST, - SendToDocs.GDATA_SERVICE_NAME_DOCLIST); - // AUTHENTICATE_TO_DOCLIST callback calls authenticateToGoogleTrix - } - - private void authenticateToGoogleTrix() { - setProgressValue(30); - setProgressMessage( - R.string.progress_message_authenticating_docs); - authenticate(new Intent(), - Constants.AUTHENTICATE_TO_TRIX, - SendToDocs.GDATA_SERVICE_NAME_TRIX); - // AUTHENTICATE_TO_TRIX callback calls sendToGoogleDocs - } - - private void sendToGoogleDocs(final long trackId) { - Log.d(TAG, "Sending to Docs...."); - tracker.trackPageView("/send/docs"); - setProgressValue(50); - setProgressMessage(R.string.progress_message_sending_docs); - final SendToDocs sender = new SendToDocs(this, - authMap.get(SendToDocs.GDATA_SERVICE_NAME_TRIX), - authMap.get(SendToDocs.GDATA_SERVICE_NAME_DOCLIST), trackId); - sendToTrackId = trackId; - Runnable onCompletion = new Runnable() { - public void run() { - setProgressValue(100); - dialogManager.dismissDialogSafely(DIALOG_PROGRESS); - sendToDocsMessage = sender.getStatusMessage(); - sendToDocsSuccess = sender.wasSuccess(); - - onSendToGoogleDocsDone(); - } - }; - sender.setOnCompletion(onCompletion); - sender.run(); - } - - private void onSendToGoogleDocsDone() { - onSendToGoogleDone(); - } - - private void onSendToGoogleDone() { - tracker.dispatch(); - SendDialog sendToGoogleDialog = dialogManager.getSendToGoogleDialog(); - final boolean sentToMyMaps = sendToGoogleDialog.getSendToMyMaps(); - final boolean sentToFusionTables = sendToGoogleDialog.getSendToFusionTables(); - dialogManager.dismissDialogSafely(DIALOG_PROGRESS); - - // We've finished sending the track to the user-selected services. Now - // we tell them the results of the upload, and optionally share the track. - // There are a few different paths through this code: - // - // 1. The user pre-requested a share (shareRequested == true). We're going - // to display the result dialog *without* the share button (the share - // listener will be null). The OK button listener will initiate the - // share. - // - // 2. The user did not pre-request a share, and the set of services to - // which we succeeded in uploading the track are compatible with - // sharing. We'll display a share button (the share listener will be - // non-null), and will share the link if the user clicks it. - // - // 3. The user did not pre-request a share, and the set of services to - // which we succeeded in uploading the track are incompatible with - // sharing. We won't display a share button. - - final boolean canShare = getSendToFusionTablesTableId() != null - || getSendToMyMapsMapId() != null; - - DialogInterface.OnClickListener doShareListener = null; - if (canShare) { - doShareListener = new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - shareLinkToMap(sentToMyMaps, sentToFusionTables); - } - }; } - DialogInterface.OnClickListener onOkListener = (canShare && shareRequested) - ? doShareListener : null; - DialogInterface.OnClickListener onShareListener = (canShare && !shareRequested) - ? doShareListener : null; + serviceConnection.stop(); - AlertDialog sendToGoogleResultDialog = ResultDialogFactory.makeDialog(this, - makeSendToGoogleResults(sendToGoogleDialog), onOkListener, onShareListener); - DialogManager.showDialogSafely(this, sendToGoogleResultDialog); - } - - boolean shareLinkToMap(boolean sentToMyMaps, boolean sentToFusionTables) { - String url = null; - if (sentToMyMaps && sendToMyMapsSuccess) { - // Prefer a link to My Maps - url = MapsFacade.buildMapUrl(sendToMyMapsMapId); - } else if (sentToFusionTables && sendToFusionTablesSuccess) { - // Otherwise try using the link to fusion tables - url = getFusionTablesUrl(sendToTrackId); + if (currentTrackId > 0) { + Intent intent = new Intent(MyTracks.this, TrackDetails.class); + intent.putExtra("trackid", currentTrackId); + intent.putExtra("hasCancelButton", false); + startActivity(intent); } - - if (url != null) { - shareLinkToMap(url); - return true; - } - - return false; - } - - protected String getFusionTablesUrl(long trackId) { - Track track = providerUtils.getTrack(trackId); - return SendToFusionTables.getMapVisualizationUrl(track); - } - - /** - * Creates a list of {@link SendResult} instances based on the set of - * services selected in {@link SendDialog} and the results as known to - * this class. - */ - private List makeSendToGoogleResults(SendDialog dialog) { - List results = new ArrayList(); - if (dialog.getSendToMyMaps()) { - results.add(new SendResult(SendType.MYMAPS, sendToMyMapsSuccess)); - } - if (dialog.getSendToFusionTables()) { - results.add(new SendResult(SendType.FUSION_TABLES, sendToFusionTablesSuccess)); - } - if (dialog.getSendToDocs()) { - results.add(new SendResult(SendType.DOCS, sendToDocsSuccess)); - } - - return results; } void clearSelectedTrack() { @@ -1221,154 +500,4 @@ public class MyTracks extends TabActivity implements OnTouchListener, long getSelectedTrackId() { return dataHub.getSelectedTrackId(); } - - /** - * Binds to track recording service if it is running. - */ - private void tryBindTrackRecordingService() { - Log.d(TAG, - "MyTracks: Trying to bind to track recording service..."); - bindService(new Intent(this, TrackRecordingService.class), - serviceConnection, 0); - Log.d(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(TAG, "MyTracks: Trying to unbind from track recording service..."); - try { - unbindService(serviceConnection); - Log.d(TAG, "MyTracks: ...unbind finished!"); - } catch (IllegalArgumentException e) { - Log.d(TAG, "MyTracks: Tried unbinding, but service was not registered.", e); - } - isBound = false; - } - } - - /** - * 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) { - TrackWriter writer = TrackWriterFactory.newWriter(this, providerUtils, trackId, format); - if (writer == null) { - dialogManager.showMessageDialog(R.string.error_track_does_not_exist, false); - return; - } - - WriteProgressController controller = new WriteProgressController(this, writer); - controller.setOnCompletionListener(new WriteProgressController.OnCompletionListener() { - @Override - public void onComplete(TrackWriter writer) { - dialogManager.showMessageDialog(writer.getErrorMessage(), writer.wasSuccess()); - } - }); - controller.startWrite(); - } - - /** - * 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) { - TrackWriter writer = TrackWriterFactory.newWriter(this, providerUtils, trackId, format); - if (writer == null) { - dialogManager.showMessageDialog(R.string.error_track_does_not_exist, false); - return; - } - - FileUtils fileUtils = new FileUtils(); - String extension = format.getExtension(); - String dirName = fileUtils.buildExternalDirectoryPath(extension, "tmp"); - - File dir = new File(dirName); - writer.setDirectory(dir); - - WriteProgressController controller = new WriteProgressController(this, writer); - controller.setOnCompletionListener(new WriteProgressController.OnCompletionListener() { - @Override - public void onComplete(TrackWriter writer) { - if (!writer.wasSuccess()) { - dialogManager.showMessageDialog(writer.getErrorMessage(), writer.wasSuccess()); - return; - } - - 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())); - } - }); - - controller.startWrite(); - } - - public AccountChooser getAccountChooser() { - return accountChooser; - } - - public ChartActivity getChartActivity() { - return chartActivity; - } - - public void setChartActivity(ChartActivity chartActivity) { - this.chartActivity = chartActivity; - } - - public DialogManager getDialogManager() { - return dialogManager; - } - - public String getSendToMyMapsMapId() { - return sendToMyMapsMapId; - } - - public String getSendToFusionTablesTableId() { - return sendToFusionTablesTableId; - } - - public boolean getSendToGoogleSuccess() { - return sendToFusionTablesSuccess && sendToDocsSuccess; - } - - // @VisibleForTesting - long getRecordingTrackId() { - return sharedPreferences.getLong(getString(R.string.recording_track_key), -1); - } - - // @VisibleForTesting - SharedPreferences getSharedPreferences() { - return sharedPreferences; - } - - // @VisibleForTesting - static void clearInstance() { - instance = null; - } - - // @VisibleForTesting - ITrackRecordingService getTrackRecordingService() { - return trackRecordingService; - } - - public TrackDataHub getDataHub() { - return dataHub; - } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/SensorStateActivity.java b/MyTracks/src/com/google/android/apps/mytracks/SensorStateActivity.java index 5302ad80b..08251014c 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/SensorStateActivity.java +++ b/MyTracks/src/com/google/android/apps/mytracks/SensorStateActivity.java @@ -16,8 +16,10 @@ package com.google.android.apps.mytracks; import static com.google.android.apps.mytracks.Constants.TAG; + import com.google.android.apps.mytracks.content.Sensor; import com.google.android.apps.mytracks.services.ITrackRecordingService; +import com.google.android.apps.mytracks.services.TrackRecordingServiceConnection; import com.google.android.apps.mytracks.services.sensors.SensorUtils; import com.google.android.maps.mytracks.R; import com.google.protobuf.InvalidProtocolBufferException; @@ -57,6 +59,11 @@ public class SensorStateActivity extends Activity { } }; + /** + * Connection to the recording service. + */ + private TrackRecordingServiceConnection serviceConnection; + /** * A task which will update the U/I. */ @@ -79,35 +86,45 @@ public class SensorStateActivity extends Activity { setContentView(R.layout.sensor_state); + // TODO: Allow reading from sensors even if service is not running. + serviceConnection = new TrackRecordingServiceConnection(this, stateUpdater); + serviceConnection.bindIfRunning(); updateState(); } @Override protected void onResume() { super.onResume(); + + serviceConnection.bindIfRunning(); + timer = new Timer(); timer.schedule(new RefreshTask(), REFRESH_PERIOD_MS, REFRESH_PERIOD_MS); } @Override protected void onPause() { - super.onPause(); timer.cancel(); timer.purge(); timer = null; + super.onPause(); + } + + @Override + protected void onDestroy() { + serviceConnection.unbind(); + super.onDestroy(); } protected void updateState() { - MyTracks mt = MyTracks.getInstance(); - - ITrackRecordingService service = - mt == null ? null : mt.getTrackRecordingService(); + ITrackRecordingService service = serviceConnection.getServiceIfBound(); if (service == null) { Log.d(Constants.TAG, "Could not get track recording service."); updateSensorState(Sensor.SensorState.NONE); updateSensorData(null); return; } + Sensor.SensorDataSet sds = null; try { byte[] buff = service.getSensorData(); @@ -153,8 +170,8 @@ public class SensorStateActivity extends Activity { if (sds.hasPower() && sds.getPower().hasValue() && sds.getPower().getState() == Sensor.SensorState.SENDING) { - utils.setText(R.id.power_state_register, - Integer.toString(sds.getPower().getValue())); + utils.setText(R.id.power_state_register, + Integer.toString(sds.getPower().getValue())); } else { utils.setText(R.id.power_state_register, SensorUtils.getStateAsString( @@ -180,14 +197,14 @@ public class SensorStateActivity extends Activity { if (sds.hasHeartRate() && sds.getHeartRate().hasValue() && sds.getHeartRate().getState() == Sensor.SensorState.SENDING) { utils.setText(R.id.heart_rate_register, - Integer.toString(sds.getHeartRate().getValue())); + Integer.toString(sds.getHeartRate().getValue())); } else { - utils.setText(R.id.heart_rate_register, - SensorUtils.getStateAsString( - sds.hasHeartRate() - ? sds.getHeartRate().getState() - : Sensor.SensorState.NONE, - this)); + utils.setText(R.id.heart_rate_register, + SensorUtils.getStateAsString( + sds.hasHeartRate() + ? sds.getHeartRate().getState() + : Sensor.SensorState.NONE, + this)); } } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/StatsActivity.java b/MyTracks/src/com/google/android/apps/mytracks/StatsActivity.java index e73111291..d8d9b4bef 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/StatsActivity.java +++ b/MyTracks/src/com/google/android/apps/mytracks/StatsActivity.java @@ -22,11 +22,13 @@ import com.google.android.apps.mytracks.content.TrackDataHub; import com.google.android.apps.mytracks.content.TrackDataHub.ListenerDataType; import com.google.android.apps.mytracks.content.TrackDataListener; import com.google.android.apps.mytracks.content.Waypoint; +import com.google.android.apps.mytracks.services.ServiceUtils; import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerFactory; import com.google.android.apps.mytracks.util.ApiFeatures; import com.google.android.maps.mytracks.R; import android.app.Activity; +import android.content.SharedPreferences; import android.location.Location; import android.os.Bundle; import android.util.DisplayMetrics; @@ -46,6 +48,18 @@ import java.util.EnumSet; * @author Rodrigo Damazio */ public class StatsActivity extends Activity implements TrackDataListener { + /** + * A runnable for posting to the UI thread. Will update the total time field. + */ + private final Runnable updateResults = new Runnable() { + public void run() { + if (dataHub.isRecordingSelected()) { + utils.setTime(R.id.total_time_register, + System.currentTimeMillis() - startTime); + } + } + }; + private StatsUtilities utils; private UIUpdateThread thread; @@ -61,18 +75,7 @@ public class StatsActivity extends Activity implements TrackDataListener { private boolean showCurrentSegment = false; private TrackDataHub dataHub; - - /** - * A runnable for posting to the UI thread. Will update the total time field. - */ - private final Runnable updateResults = new Runnable() { - public void run() { - if (dataHub.isRecordingSelected()) { - utils.setTime(R.id.total_time_register, - System.currentTimeMillis() - startTime); - } - } - }; + private SharedPreferences preferences; /** * A thread that updates the total time field every second. @@ -87,7 +90,7 @@ public class StatsActivity extends Activity implements TrackDataListener { @Override public void run() { Log.i(TAG, "Started UI update thread"); - while (MyTracks.getInstance().isRecording()) { + while (ServiceUtils.isRecording(StatsActivity.this, null, preferences)) { runOnUiThread(updateResults); try { Thread.sleep(1000L); @@ -102,10 +105,10 @@ public class StatsActivity extends Activity implements TrackDataListener { /** Called when the activity is first created. */ @Override - public void onCreate(Bundle savedInstanceState) { + protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - dataHub = MyTracks.getInstance().getDataHub(); + preferences = getSharedPreferences(Constants.SETTINGS_NAME, 0); utils = new StatsUtilities(this); // The volume we want to control is the Text-To-Speech volume @@ -132,18 +135,20 @@ public class StatsActivity extends Activity implements TrackDataListener { @Override protected void onResume() { + super.onResume(); + + dataHub = TrackDataHub.getStartedInstance(); dataHub.registerTrackDataListener(this, EnumSet.of( ListenerDataType.SELECTED_TRACK_CHANGED, ListenerDataType.TRACK_UPDATES, ListenerDataType.LOCATION_UPDATES, ListenerDataType.DISPLAY_PREFERENCES)); - - super.onResume(); } @Override protected void onPause() { dataHub.unregisterTrackDataListener(this); + dataHub = null; if (thread != null) { thread.interrupt(); @@ -171,7 +176,7 @@ public class StatsActivity extends Activity implements TrackDataListener { utils.setReportSpeed(displaySpeed); updateLabels(); - + return true; // Reload data } diff --git a/MyTracks/src/com/google/android/apps/mytracks/TrackList.java b/MyTracks/src/com/google/android/apps/mytracks/TrackList.java index d39fbc032..17e927f28 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/TrackList.java +++ b/MyTracks/src/com/google/android/apps/mytracks/TrackList.java @@ -1,12 +1,12 @@ /* * 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 @@ -16,6 +16,10 @@ package com.google.android.apps.mytracks; import com.google.android.apps.mytracks.content.TracksColumns; +import com.google.android.apps.mytracks.io.file.SaveActivity; +import com.google.android.apps.mytracks.io.sendtogoogle.SendActivity; +import com.google.android.apps.mytracks.services.ServiceUtils; +import com.google.android.apps.mytracks.services.TrackRecordingServiceConnection; import com.google.android.apps.mytracks.util.StringUtils; import com.google.android.apps.mytracks.util.UnitConversions; import com.google.android.maps.mytracks.R; @@ -76,8 +80,7 @@ public class TrackList extends ListActivity R.string.tracklist_show_track); menu.add(0, Constants.MENU_EDIT, 0, R.string.tracklist_edit_track); - if (!MyTracks.getInstance().isRecording() - || trackId != recordingTrackId) { + if (!isRecording() || trackId != recordingTrackId) { menu.add(0, Constants.MENU_SEND_TO_GOOGLE, 0, R.string.tracklist_send_to_google); SubMenu share = menu.addSubMenu(0, Constants.MENU_SHARE, 0, @@ -109,6 +112,16 @@ public class TrackList extends ListActivity } }; + private final Runnable serviceBindingChanged = new Runnable() { + @Override + public void run() { + updateButtonsEnabled(); + } + }; + + private TrackRecordingServiceConnection serviceConnection; + private SharedPreferences preferences; + @Override public void onSharedPreferenceChanged( SharedPreferences sharedPreferences, String key) { @@ -138,32 +151,46 @@ public class TrackList extends ListActivity @Override public boolean onMenuItemSelected(int featureId, MenuItem item) { - if (!super.onMenuItemSelected(featureId, item)) { - switch (item.getItemId()) { - case Constants.MENU_SHOW: { - onListItemClick(null, null, 0, trackId); - return true; - } - case Constants.MENU_EDIT: { - Intent intent = new Intent(this, TrackDetails.class); - intent.putExtra("trackid", trackId); - startActivity(intent); - return true; - } - case Constants.MENU_SHARE: - case Constants.MENU_WRITE_TO_SD_CARD: - return false; - default: { - Intent result = new Intent(); - result.putExtra("trackid", trackId); - setResult( - Constants.getActionFromMenuId(item.getItemId()), result); - finish(); - return true; - } + switch (item.getItemId()) { + case Constants.MENU_SHOW: { + onListItemClick(null, null, 0, trackId); + return true; + } + case Constants.MENU_EDIT: { + Intent intent = new Intent(this, TrackDetails.class); + intent.putExtra("trackid", trackId); + startActivity(intent); + return true; + } + case Constants.MENU_SHARE: + case Constants.MENU_WRITE_TO_SD_CARD: + return false; + case Constants.MENU_SEND_TO_GOOGLE: + SendActivity.sendToGoogle(this, trackId, false); + return true; + case Constants.MENU_SHARE_LINK: + SendActivity.sendToGoogle(this, trackId, true); + return true; + case Constants.MENU_SAVE_GPX_FILE: + case Constants.MENU_SAVE_KML_FILE: + case Constants.MENU_SAVE_CSV_FILE: + case Constants.MENU_SAVE_TCX_FILE: + case Constants.MENU_SHARE_GPX_FILE: + case Constants.MENU_SHARE_KML_FILE: + case Constants.MENU_SHARE_CSV_FILE: + case Constants.MENU_SHARE_TCX_FILE: + SaveActivity.handleExportTrackAction(this, trackId, + Constants.getActionFromMenuId(item.getItemId())); + return true; + default: { + Intent result = new Intent(); + result.putExtra("trackid", trackId); + setResult( + Constants.getActionFromMenuId(item.getItemId()), result); + finish(); + return true; } } - return false; } @Override @@ -197,17 +224,19 @@ public class TrackList extends ListActivity listView = getListView(); listView.setOnCreateContextMenuListener(contextMenuListener); + preferences = getSharedPreferences(Constants.SETTINGS_NAME, 0); + serviceConnection = new TrackRecordingServiceConnection(this, serviceBindingChanged); + View deleteAll = findViewById(R.id.tracklist_btn_delete_all); - View exportAll = findViewById(R.id.tracklist_btn_export_all); - boolean notRecording = !MyTracks.getInstance().isRecording(); deleteAll.setOnClickListener(this); - deleteAll.setEnabled(notRecording); + + View exportAll = findViewById(R.id.tracklist_btn_export_all); exportAll.setOnClickListener(this); - exportAll.setEnabled(notRecording); + + updateButtonsEnabled(); + findViewById(R.id.tracklist_btn_import_all).setOnClickListener(this); - SharedPreferences preferences = - getSharedPreferences(Constants.SETTINGS_NAME, 0); preferences.registerOnSharedPreferenceChangeListener(this); metricUnits = preferences.getBoolean(getString(R.string.metric_units_key), true); @@ -220,6 +249,29 @@ public class TrackList extends ListActivity setListAdapter(); } + @Override + protected void onStart() { + super.onStart(); + + serviceConnection.bindIfRunning(); + } + + @Override + protected void onDestroy() { + serviceConnection.unbind(); + + super.onDestroy(); + } + + private void updateButtonsEnabled() { + View deleteAll = findViewById(R.id.tracklist_btn_delete_all); + View exportAll = findViewById(R.id.tracklist_btn_export_all); + + boolean notRecording = !isRecording(); + deleteAll.setEnabled(notRecording); + exportAll.setEnabled(notRecording); + } + private void setListAdapter() { // Get a cursor with all tracks SimpleCursorAdapter adapter = new SimpleCursorAdapter( @@ -283,4 +335,8 @@ public class TrackList extends ListActivity }); setListAdapter(adapter); } + + private boolean isRecording() { + return ServiceUtils.isRecording(TrackList.this, serviceConnection.getServiceIfBound(), preferences); + } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/WaypointsList.java b/MyTracks/src/com/google/android/apps/mytracks/WaypointsList.java index 9dc0c4295..8c96698f1 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/WaypointsList.java +++ b/MyTracks/src/com/google/android/apps/mytracks/WaypointsList.java @@ -15,10 +15,14 @@ */ package com.google.android.apps.mytracks; +import static com.google.android.apps.mytracks.Constants.TAG; + import com.google.android.apps.mytracks.content.MyTracksProviderUtils; import com.google.android.apps.mytracks.content.Waypoint; import com.google.android.apps.mytracks.content.WaypointCreationRequest; import com.google.android.apps.mytracks.content.WaypointsColumns; +import com.google.android.apps.mytracks.services.ITrackRecordingService; +import com.google.android.apps.mytracks.services.TrackRecordingServiceConnection; import com.google.android.apps.mytracks.util.StringUtils; import com.google.android.maps.mytracks.R; @@ -43,6 +47,7 @@ import android.widget.ImageView; import android.widget.ListView; import android.widget.SimpleCursorAdapter; import android.widget.TextView; +import android.widget.Toast; /** * Activity which shows the list of waypoints in a track. @@ -60,6 +65,7 @@ public class WaypointsList extends ListActivity private Button insertStatisticsButton = null; private long recordingTrackId = -1; private MyTracksProviderUtils providerUtils; + private TrackRecordingServiceConnection serviceConnection; private Cursor waypointsCursor = null; @@ -125,6 +131,7 @@ public class WaypointsList extends ListActivity super.onCreate(savedInstanceState); providerUtils = MyTracksProviderUtils.Factory.get(this); + serviceConnection = new TrackRecordingServiceConnection(this, null); // We don't need a window title bar: requestWindowFeature(Window.FEATURE_NO_TITLE); @@ -171,6 +178,20 @@ public class WaypointsList extends ListActivity setListAdapter(); } + @Override + protected void onResume() { + super.onResume(); + + serviceConnection.bindIfRunning(); + } + + @Override + protected void onDestroy() { + serviceConnection.unbind(); + + super.onDestroy(); + } + @Override public void onClick(View v) { WaypointCreationRequest request; @@ -184,17 +205,10 @@ public class WaypointsList extends ListActivity default: return; } - long id; - try { - id = MyTracks.getInstance().insertWaypoint(request); - } catch (RemoteException e) { - Log.e(Constants.TAG, "Cannot insert marker.", e); - return; - } catch (IllegalStateException e) { - Log.e(Constants.TAG, "Cannot insert marker.", e); - return; - } + long id = insertWaypoint(request); if (id < 0) { + Toast.makeText(this, R.string.error_unable_to_insert_marker, + Toast.LENGTH_LONG).show(); Log.e(Constants.TAG, "Failed to insert marker."); return; } @@ -203,6 +217,28 @@ public class WaypointsList extends ListActivity startActivity(intent); } + private long insertWaypoint(WaypointCreationRequest request) { + try { + ITrackRecordingService trackRecordingService = serviceConnection.getServiceIfBound(); + if (trackRecordingService != null) { + long waypointId = trackRecordingService.insertWaypoint(request); + if (waypointId >= 0) { + Toast.makeText(this, R.string.status_statistics_inserted, + Toast.LENGTH_LONG).show(); + return waypointId; + } + } else { + Log.e(TAG, "Not connected to service, not inserting waypoint"); + } + } catch (RemoteException e) { + Log.e(Constants.TAG, "Cannot insert marker.", e); + } catch (IllegalStateException e) { + Log.e(Constants.TAG, "Cannot insert marker.", e); + } + + return -1; + } + private void setListAdapter() { // Get a cursor with all tracks SimpleCursorAdapter adapter = new SimpleCursorAdapter( diff --git a/MyTracks/src/com/google/android/apps/mytracks/content/TrackDataHub.java b/MyTracks/src/com/google/android/apps/mytracks/content/TrackDataHub.java index 5ee3e7b5d..5b4e1017d 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/content/TrackDataHub.java +++ b/MyTracks/src/com/google/android/apps/mytracks/content/TrackDataHub.java @@ -22,6 +22,7 @@ import static com.google.android.apps.mytracks.Constants.MAX_NETWORK_AGE_MS; import static com.google.android.apps.mytracks.Constants.TAG; import static com.google.android.apps.mytracks.Constants.TARGET_DISPLAYED_TRACK_POINTS; +import com.google.android.apps.mytracks.Constants; import com.google.android.apps.mytracks.content.DataSourceManager.DataSourceListener; import com.google.android.apps.mytracks.content.MyTracksProviderUtils.DoubleBufferedLocationFactory; import com.google.android.apps.mytracks.content.MyTracksProviderUtils.LocationIterator; @@ -149,24 +150,21 @@ public class TrackDataHub { // Get content notifications on the main thread, send listener callbacks in another. // This ensures listener calls are serialized. - private final HandlerThread listenerHandlerThread; - private final Handler listenerHandler; + private HandlerThread listenerHandlerThread; + private Handler listenerHandler; /** Manager for external listeners (those from activities). */ private final TrackDataListeners listeners; /** Wrapper for interacting with system data managers. */ - private final DataSourcesWrapper dataSources; + private DataSourcesWrapper dataSources; /** Manager for system data listener registrations. */ - private final DataSourceManager dataSourceManager; + private DataSourceManager dataSourceManager; /** Condensed listener for system data listener events. */ private final DataSourceListener dataSourceListener = new HubDataSourceListener(); - /** Whether we've been started. */ - private boolean started; - // Cached preference values private int minRequiredAccuracy; private boolean useMetricUnits; @@ -191,28 +189,34 @@ public class TrackDataHub { private int lastSamplingFrequency; private DoubleBufferedLocationFactory locationFactory; + private static TrackDataHub startedInstance; + /** - * Default constructor. + * Builds a new {@link TrackDataHub} instance. */ - public TrackDataHub(Context ctx, SharedPreferences preferences, - MyTracksProviderUtils providerUtils) { - this(ctx, new DataSourcesWrapperImpl(ctx, preferences), new TrackDataListeners(), - preferences, providerUtils, TARGET_DISPLAYED_TRACK_POINTS); + public synchronized static TrackDataHub newInstance(Context context) { + // Ensure our singleton is never bound to an activity, to avoid memory leaks. + context = context.getApplicationContext(); + + SharedPreferences preferences = context.getSharedPreferences(Constants.SETTINGS_NAME, 0); + MyTracksProviderUtils providerUtils = MyTracksProviderUtils.Factory.get(context); + return new TrackDataHub(context, + new TrackDataListeners(), + preferences, providerUtils, + TARGET_DISPLAYED_TRACK_POINTS); } /** * Injection constructor. */ // @VisibleForTesting - TrackDataHub(Context ctx, DataSourcesWrapper dataSources, TrackDataListeners listeners, - SharedPreferences preferences, MyTracksProviderUtils providerUtils, int targetNumPoints) { + TrackDataHub(Context ctx, TrackDataListeners listeners, SharedPreferences preferences, + MyTracksProviderUtils providerUtils, int targetNumPoints) { this.context = ctx; this.listeners = listeners; this.preferences = preferences; this.providerUtils = providerUtils; - this.dataSources = dataSources; this.targetNumPoints = targetNumPoints; - this.dataSourceManager = new DataSourceManager(dataSourceListener, dataSources); this.locationFactory = new DoubleBufferedLocationFactory(); SELECTED_TRACK_KEY = context.getString(R.string.selected_track_key); @@ -221,10 +225,6 @@ public class TrackDataHub { METRIC_UNITS_KEY = context.getString(R.string.metric_units_key); SPEED_REPORTING_KEY = context.getString(R.string.report_speed_key); - listenerHandlerThread = new HandlerThread("trackDataContentThread"); - listenerHandlerThread.start(); - listenerHandler = new Handler(listenerHandlerThread.getLooper()); - resetState(); } @@ -234,11 +234,17 @@ public class TrackDataHub { */ public void start() { Log.i(TAG, "TrackDataHub.start"); - if (started) { + if (startedInstance != null) { Log.w(TAG, "Already started, ignoring"); return; } - started = true; + startedInstance = this; + + listenerHandlerThread = new HandlerThread("trackDataContentThread"); + listenerHandlerThread.start(); + listenerHandler = new Handler(listenerHandlerThread.getLooper()); + dataSources = newDataSources(); + dataSourceManager = new DataSourceManager(dataSourceListener, dataSources); // This may or may not register internal listeners, depending on whether // we already had external listeners. @@ -249,12 +255,23 @@ public class TrackDataHub { loadDataForAllListeners(); } - private void loadSharedPreferences() { - selectedTrackId = preferences.getLong(SELECTED_TRACK_KEY, -1); - useMetricUnits = preferences.getBoolean(METRIC_UNITS_KEY, true); - reportSpeed = preferences.getBoolean(SPEED_REPORTING_KEY, true); - minRequiredAccuracy = preferences.getInt(MIN_REQUIRED_ACCURACY_KEY, - DEFAULT_MIN_REQUIRED_ACCURACY); + // @VisibleForTesting + protected DataSourcesWrapper newDataSources() { + return new DataSourcesWrapperImpl(context, preferences); + } + + /** + * If there's an instance for which {@link start} has been called, returns it. + * + * @return the started instance + * @throws IllegalStateException if there isn't a started instance + */ + public static TrackDataHub getStartedInstance() { + if (startedInstance == null) { + throw new IllegalStateException("Data hub not started"); + } + + return startedInstance; } /** @@ -263,25 +280,42 @@ public class TrackDataHub { */ public void stop() { Log.i(TAG, "TrackDataHub.stop"); - if (!started) { + if (!isStarted()) { Log.w(TAG, "Not started, ignoring"); return; } // Unregister internal listeners even if there are external listeners registered. dataSourceManager.unregisterAllListeners(); + listenerHandlerThread.getLooper().quit(); - started = false; + startedInstance = null; + + dataSources = null; + dataSourceManager = null; + listenerHandlerThread = null; + listenerHandler = null; } - /** Permanently invalidates and throws away all resources used by this class. */ - public void destroy() { - if (started) { - throw new IllegalStateException("Can only destroy the data hub after it's been stopped"); + private boolean isStarted() { + return startedInstance != null; + } + + @Override + protected void finalize() throws Throwable { + if (isStarted() || listenerHandlerThread.isAlive()) { + Log.e(TAG, "Forgot to stop() TrackDataHub"); } - ApiFeatures.getInstance().getApiPlatformAdapter() - .stopHandlerThread(listenerHandlerThread); + super.finalize(); + } + + private void loadSharedPreferences() { + selectedTrackId = preferences.getLong(SELECTED_TRACK_KEY, -1); + useMetricUnits = preferences.getBoolean(METRIC_UNITS_KEY, true); + reportSpeed = preferences.getBoolean(SPEED_REPORTING_KEY, true); + minRequiredAccuracy = preferences.getInt(MIN_REQUIRED_ACCURACY_KEY, + DEFAULT_MIN_REQUIRED_ACCURACY); } /** Updates known magnetic declination if needed. */ @@ -324,6 +358,10 @@ public class TrackDataHub { * is not available or doesn't have a fix. */ public void forceUpdateLocation() { + if (!isStarted()) { + Log.w(TAG, "Not started, not forcing location update"); + return; + } Log.i(TAG, "Forcing location update"); Location loc = dataSources.getLastKnownLocation(); @@ -335,7 +373,7 @@ public class TrackDataHub { /** Returns the ID of the currently-selected track. */ public long getSelectedTrackId() { - if (!started) { + if (!isStarted()) { loadSharedPreferences(); } return selectedTrackId; @@ -346,17 +384,9 @@ public class TrackDataHub { return getSelectedTrackId() > 0; } - /** Returns whether we're currently recording a track. */ - public boolean isRecording() { - if (!started) { - loadSharedPreferences(); - } - return preferences.getLong(RECORDING_TRACK_KEY, -1) > 0; - } - /** Returns whether the selected track is still being recorded. */ public boolean isRecordingSelected() { - if (!started) { + if (!isStarted()) { loadSharedPreferences(); } long recordingTrackId = preferences.getLong(RECORDING_TRACK_KEY, -1); @@ -412,7 +442,7 @@ public class TrackDataHub { // Don't load any data or start internal listeners if start() hasn't been // called. When it is called, we'll do both things. - if (!started) return; + if (!isStarted()) return; reloadDataForListener(registration); @@ -426,7 +456,7 @@ public class TrackDataHub { // Don't load any data or start internal listeners if start() hasn't been // called. When it is called, we'll do both things. - if (!started) return; + if (!isStarted()) return; dataSourceManager.updateAllListeners(getNeededListenerTypes()); } @@ -449,7 +479,7 @@ public class TrackDataHub { * Assumes it's called from a block that synchronizes on {@link #listeners}. */ private void reloadDataForListener(final ListenerRegistration registration) { - if (!started) { + if (!isStarted()) { Log.w(TAG, "Not started, not reloading"); return; } @@ -541,7 +571,7 @@ public class TrackDataHub { * Reloads all track data received so far into the specified listeners. */ private void loadDataForAllListeners() { - if (!started) { + if (!isStarted()) { Log.w(TAG, "Not started, not reloading"); return; } @@ -608,7 +638,7 @@ public class TrackDataHub { /** Called when the speed/pace reporting preference changes. */ private void notifySpeedReportingChanged() { - if (!started) return; + if (!isStarted()) return; runInListenerThread(new Runnable() { @Override @@ -630,7 +660,7 @@ public class TrackDataHub { /** Called when the metric units setting changes. */ private void notifyUnitsChanged() { - if (!started) return; + if (!isStarted()) return; runInListenerThread(new Runnable() { @Override @@ -1064,6 +1094,12 @@ public class TrackDataHub { // @VisibleForTesting protected void runInListenerThread(Runnable runnable) { + if (listenerHandler == null) { + // Use a Throwable to ensure the stack trace is logged. + Log.e(TAG, "Tried to use listener thread before start()", new Throwable()); + return; + } + listenerHandler.post(runnable); } diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/ModernAuthManager.java b/MyTracks/src/com/google/android/apps/mytracks/io/ModernAuthManager.java index 24c721bc4..8048f4fab 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/ModernAuthManager.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/ModernAuthManager.java @@ -22,7 +22,6 @@ import com.google.android.accounts.AccountManagerFuture; import com.google.android.accounts.AuthenticatorException; import com.google.android.accounts.OperationCanceledException; import com.google.android.apps.mytracks.AccountChooser; -import com.google.android.apps.mytracks.MyTracks; import com.google.android.apps.mytracks.Constants; import android.app.Activity; @@ -183,7 +182,9 @@ public class ModernAuthManager implements AuthManager { public void run() { accountManager.invalidateAuthToken(Constants.ACCOUNT_TYPE, authToken); - MyTracks.getInstance().getAccountChooser().chooseAccount(activity, + + AccountChooser accountChooser = new AccountChooser(); + accountChooser.chooseAccount(activity, new AccountChooser.AccountHandler() { @Override public void handleAccountSelected(Account account) { diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/SendToDocs.java b/MyTracks/src/com/google/android/apps/mytracks/io/SendToDocs.java index 5105285e4..1b0047740 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/SendToDocs.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/SendToDocs.java @@ -15,8 +15,8 @@ */ package com.google.android.apps.mytracks.io; -import com.google.android.apps.mytracks.MyTracks; import com.google.android.apps.mytracks.Constants; +import com.google.android.apps.mytracks.ProgressIndicator; import com.google.android.apps.mytracks.content.MyTracksProviderUtils; import com.google.android.apps.mytracks.content.Track; import com.google.android.apps.mytracks.io.docs.DocsHelper; @@ -25,10 +25,10 @@ import com.google.android.apps.mytracks.io.gdata.GDataWrapper; import com.google.android.common.gdata.AndroidXmlParserFactory; import com.google.android.maps.mytracks.R; import com.google.wireless.gdata.client.GDataClient; +import com.google.wireless.gdata.client.GDataServiceClient; import com.google.wireless.gdata.docs.DocumentsClient; import com.google.wireless.gdata.docs.SpreadsheetsClient; import com.google.wireless.gdata.docs.XmlDocsGDataParserFactory; -import com.google.wireless.gdata.client.GDataServiceClient; import android.app.Activity; import android.content.SharedPreferences; @@ -49,11 +49,11 @@ public class SendToDocs { /** The GData service name for the Google Docs Document List */ public static final String GDATA_SERVICE_NAME_DOCLIST = "writely"; - + private final Activity activity; private final AuthManager trixAuth; private final AuthManager docListAuth; - private final long trackId; + private final ProgressIndicator progressIndicator; private final boolean metricUnits; private final HandlerThread handlerThread; private final Handler handler; @@ -64,12 +64,13 @@ public class SendToDocs { private String statusMessage = ""; private Runnable onCompletion = null; + public SendToDocs(Activity activity, AuthManager trixAuth, - AuthManager docListAuth, long trackId) { + AuthManager docListAuth, ProgressIndicator progressIndicator) { this.activity = activity; this.trixAuth = trixAuth; this.docListAuth = docListAuth; - this.trackId = trackId; + this.progressIndicator = progressIndicator; SharedPreferences preferences = activity.getSharedPreferences( Constants.SETTINGS_NAME, 0); @@ -81,23 +82,23 @@ public class SendToDocs { metricUnits = true; } - Log.d(Constants.TAG, - "Sending to Google Docs: trackId = " + trackId); handlerThread = new HandlerThread("SendToGoogleDocs"); handlerThread.start(); handler = new Handler(handlerThread.getLooper()); } - public void run() { + public void sendToDocs(final long trackId) { + Log.d(Constants.TAG, + "Sending to Google Docs: trackId = " + trackId); handler.post(new Runnable() { @Override public void run() { - doUpload(); + doUpload(trackId); } }); } - private void doUpload() { + private void doUpload(long trackId) { // TODO statusMessage = activity.getString(R.string.error_sending_to_fusiontables); success = false; @@ -198,7 +199,7 @@ public class SendToDocs { } if (spreadsheetId == null) { - MyTracks.getInstance().setProgressValue(65); + progressIndicator.setProgressValue(65); // Waiting a few seconds and trying again. Maybe the server just had a // hickup (unfortunately that happens quite a lot...). try { @@ -217,7 +218,7 @@ public class SendToDocs { } // We were unable to find an existing spreadsheet, so create a new one. - MyTracks.getInstance().setProgressValue(70); + progressIndicator.setProgressValue(70); if (spreadsheetId == null) { Log.i(Constants.TAG, "Creating new spreadsheet: " + sheetTitle); @@ -229,11 +230,11 @@ public class SendToDocs { + sheetTitle, e); return false; } - MyTracks.getInstance().setProgressValue(80); + progressIndicator.setProgressValue(80); createdNewSpreadSheet = true; if (spreadsheetId == null) { - MyTracks.getInstance().setProgressValue(85); + progressIndicator.setProgressValue(85); // The previous creation might have succeeded even though GData // reported an error. Seems to be a know bug, // see http://code.google.com/p/gdata-issues/issues/detail?id=929 @@ -255,7 +256,7 @@ public class SendToDocs { } if (spreadsheetId == null) { - MyTracks.getInstance().setProgressValue(87); + progressIndicator.setProgressValue(87); // Re-try try { Thread.sleep(5000); @@ -290,7 +291,7 @@ public class SendToDocs { return false; } - MyTracks.getInstance().setProgressValue(90); + progressIndicator.setProgressValue(90); docsHelper.addTrackRow(activity, trixAuth, spreadsheetId, worksheetId, track, metricUnits); diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/SendToFusionTables.java b/MyTracks/src/com/google/android/apps/mytracks/io/SendToFusionTables.java index 134305b90..5e7ae4977 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/SendToFusionTables.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/SendToFusionTables.java @@ -50,6 +50,7 @@ import android.util.Log; import java.io.ByteArrayInputStream; import java.io.IOException; import java.net.URLEncoder; +import java.nio.charset.Charset; import java.util.ArrayList; import java.util.List; import java.util.Locale; @@ -432,10 +433,10 @@ public class SendToFusionTables implements Runnable { * @return the kml. */ private String getKmlPoint(Location location) { - StringBuilder builder = new StringBuilder(""); - appendCoordinate(location, builder); - builder.append(""); - return builder.toString(); + StringBuilder builder = new StringBuilder(""); + appendCoordinate(location, builder); + builder.append(""); + return builder.toString(); } /** @@ -611,7 +612,7 @@ public class SendToFusionTables implements Runnable { // I am leaving the number of waypoints very high which should not be a // problem because we don't try to load them into objects all at the // same time. - boolean success = true; + boolean success = true; Cursor c = null; try { c = providerUtils.getWaypointsCursor( @@ -625,9 +626,9 @@ public class SendToFusionTables implements Runnable { Waypoint wpt = providerUtils.createWaypoint(c); Log.d(Constants.TAG, "SendToFusionTables: Creating waypoint."); success = createNewPoint(wpt.getName(), wpt.getDescription(), wpt.getLocation(), - MARKER_TYPE_WAYPOINT); + MARKER_TYPE_WAYPOINT); if (!success) { - break; + break; } } } @@ -649,7 +650,7 @@ public class SendToFusionTables implements Runnable { // to fit the completion percentage range alloted to track data upload. double totalPercentage = (totalLocationsRead + totalLocationsPrepared + totalLocationsUploaded) - / (totalLocations * 3); + / (totalLocations * 3.0); double scaledPercentage = totalPercentage * (PROGRESS_UPLOAD_DATA_MAX - PROGRESS_UPLOAD_DATA_MIN) + PROGRESS_UPLOAD_DATA_MIN; @@ -693,8 +694,8 @@ public class SendToFusionTables implements Runnable { boolean success = response.isSuccessStatusCode; if (success) { byte[] result = new byte[1024]; - response.getContent().read(result); - String s = Strings.fromBytesUtf8(result); + int read = response.getContent().read(result); + String s = new String(result, 0, read, Charset.forName("UTF8")); String[] lines = s.split(Strings.LINE_SEPARATOR); if (lines[0].equals("tableid")) { tableId = lines[1]; diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/backup/ExternalFileBackup.java b/MyTracks/src/com/google/android/apps/mytracks/io/backup/ExternalFileBackup.java index 1fbcf9485..16f18d0df 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/backup/ExternalFileBackup.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/backup/ExternalFileBackup.java @@ -15,6 +15,8 @@ */ package com.google.android.apps.mytracks.io.backup; +import static com.google.android.apps.mytracks.Constants.TAG; + import com.google.android.apps.mytracks.Constants; import com.google.android.apps.mytracks.content.TrackPointsColumns; import com.google.android.apps.mytracks.content.TracksColumns; @@ -205,7 +207,9 @@ class ExternalFileBackup { } catch (IOException e) { // We tried to delete the partially created file, but do nothing // if that also fails. - outputFile.delete(); + if (!outputFile.delete()) { + Log.w(TAG, "Failed to delete file " + outputFile.getAbsolutePath()); + } throw e; } finally { diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/file/ImportActivity.java b/MyTracks/src/com/google/android/apps/mytracks/io/file/ImportActivity.java new file mode 100644 index 000000000..7fe66f3be --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/io/file/ImportActivity.java @@ -0,0 +1,173 @@ +/* + * Copyright 2011 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.io.file; + +import static com.google.android.apps.mytracks.Constants.TAG; + +import com.google.android.apps.mytracks.DialogManager; +import com.google.android.apps.mytracks.content.MyTracksProviderUtils; +import com.google.android.apps.mytracks.content.TracksColumns; +import com.google.android.apps.mytracks.util.UriUtils; +import com.google.android.maps.mytracks.R; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.AlertDialog.Builder; +import android.app.ProgressDialog; +import android.content.ContentUris; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.util.Log; + +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; + +import javax.xml.parsers.ParserConfigurationException; + +import org.xml.sax.SAXException; + +/** + * Helper activity which imports tracks from a file. + * + * @author Rodrigo Damazio + */ +public class ImportActivity extends Activity { + + private ProgressDialog progressDialog; + private MyTracksProviderUtils providerUtils; + + @Override + public void onCreate(Bundle savedState) { + super.onCreate(savedState); + + providerUtils = MyTracksProviderUtils.Factory.get(this); + } + + @Override + public void onStart() { + super.onStart(); + + Intent intent = getIntent(); + String action = intent.getAction(); + Uri data = intent.getData(); + if (!(Intent.ACTION_VIEW.equals(action) || Intent.ACTION_ATTACH_DATA.equals(action)) + || !UriUtils.isFileUri(data)) { + Log.e(TAG, "Received an intent with unsupported action or data: " + intent); + finish(); + return; + } + + Log.i(TAG, "Importing GPX file at " + data); + startTrackImport(data.getPath()); + } + + private void startTrackImport(final String fileName) { + progressDialog = new ProgressDialog(this); + progressDialog.setOwnerActivity(this); + progressDialog.setIcon(android.R.drawable.ic_dialog_info); + progressDialog.setTitle(R.string.progress_title); + progressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER); + progressDialog.setMessage(getString(R.string.import_progress_message)); + progressDialog.show(); + + 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(TAG, "Caught an unexpected exception.", e); + message = R.string.error_generic; + } catch (ParserConfigurationException e) { + Log.e(TAG, "Caught an unexpected exception.", e); + message = R.string.error_generic; + } catch (IOException e) { + Log.e(TAG, "Caught an unexpected exception.", e); + message = R.string.error_unable_to_read_file; + } catch (NullPointerException e) { + Log.e(TAG, "Caught an unexpected exception.", e); + message = R.string.error_invalid_gpx_format; + } catch (OutOfMemoryError e) { + Log.e(TAG, "Caught an unexpected exception.", e); + message = R.string.error_out_of_memory; + } + + boolean success = (trackIdsImported != null && trackIdsImported.length > 0); + onImportDone(success, message, trackIdsImported); + } finally { + runOnUiThread(new Runnable() { + public void run() { + progressDialog.dismiss(); + } + }); + } + } + }; + t.start(); + } + + private void onImportDone(boolean success, int message, final long[] trackIds) { + OnClickListener finishOnClick = new OnClickListener() { + @Override + public void onClick(DialogInterface arg0, int arg1) { + finish(); + } + }; + + if (!success) { + DialogManager.showMessageDialog(this, message, false /* success */, + finishOnClick); + return; + } + + // Show a dialog telling the user about the import, and asking if he wishes + // to open the track right away. + final Builder dialogBuilder = new AlertDialog.Builder(this); + dialogBuilder.setCancelable(true); + dialogBuilder.setMessage(getString(R.string.import_success, trackIds.length)); + dialogBuilder.setPositiveButton(android.R.string.ok, finishOnClick); + dialogBuilder.setNeutralButton(R.string.import_show_track, new OnClickListener() { + @Override + public void onClick(DialogInterface arg0, int arg1) { + long lastTrackId = trackIds[trackIds.length - 1]; + Uri trackUri = ContentUris.withAppendedId(TracksColumns.CONTENT_URI, lastTrackId); + + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setDataAndType(trackUri, TracksColumns.CONTENT_ITEMTYPE); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivity(intent); + finish(); + } + }); + + runOnUiThread(new Runnable() { + @Override + public void run() { + dialogBuilder.show(); + } + }); + } +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/file/SaveActivity.java b/MyTracks/src/com/google/android/apps/mytracks/io/file/SaveActivity.java new file mode 100644 index 000000000..d825c482c --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/io/file/SaveActivity.java @@ -0,0 +1,196 @@ +/* + * Copyright 2011 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.io.file; + +import static com.google.android.apps.mytracks.Constants.TAG; + +import com.google.android.apps.mytracks.Constants; +import com.google.android.apps.mytracks.DialogManager; +import com.google.android.apps.mytracks.content.MyTracksProviderUtils; +import com.google.android.apps.mytracks.content.TracksColumns; +import com.google.android.apps.mytracks.io.file.TrackWriterFactory.TrackFileFormat; +import com.google.android.apps.mytracks.util.FileUtils; +import com.google.android.apps.mytracks.util.UriUtils; +import com.google.android.maps.mytracks.R; + +import android.app.Activity; +import android.content.ContentUris; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.util.Log; + +import java.io.File; + +/** + * Activity for saving a track to a file (and optionally sending that file). + * + * @author Rodrigo Damazio + */ +public class SaveActivity extends Activity { + public static final String EXTRA_SHARE_FILE = "share_file"; + public static final String EXTRA_FILE_FORMAT = "file_format"; + + private MyTracksProviderUtils providerUtils; + private long trackId; + private TrackWriter writer; + private boolean shareFile; + private TrackFileFormat format; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + providerUtils = MyTracksProviderUtils.Factory.get(this); + } + + @Override + protected void onStart() { + super.onStart(); + + Intent intent = getIntent(); + String action = intent.getAction(); + String type = intent.getType(); + Uri data = intent.getData(); + if (!getString(R.string.save_intent_action).equals(action) || + !TracksColumns.CONTENT_ITEMTYPE.equals(type) || + !UriUtils.matchesContentUri(data, TracksColumns.CONTENT_URI)) { + Log.e(TAG, "Got bad save intent: " + intent); + finish(); + return; + } + + trackId = ContentUris.parseId(data); + + int formatIdx = intent.getIntExtra(EXTRA_FILE_FORMAT, -1); + format = TrackFileFormat.values()[formatIdx]; + shareFile = intent.getBooleanExtra(EXTRA_SHARE_FILE, false); + + writer = TrackWriterFactory.newWriter(this, providerUtils, trackId, format); + if (writer == null) { + Log.e(TAG, "Unable to build writer"); + finish(); + return; + } + + if (shareFile) { + // If the file is for sending, save it to a temporary location instead. + FileUtils fileUtils = new FileUtils(); + String extension = format.getExtension(); + String dirName = fileUtils.buildExternalDirectoryPath(extension, "tmp"); + + File dir = new File(dirName); + writer.setDirectory(dir); + } + + WriteProgressController controller = new WriteProgressController(this, writer); + controller.setOnCompletionListener(new WriteProgressController.OnCompletionListener() { + @Override + public void onComplete() { + onWriteComplete(); + } + }); + controller.startWrite(); + } + + private void onWriteComplete() { + if (shareFile) { + shareWrittenFile(); + } else { + showResultDialog(); + } + } + + private void shareWrittenFile() { + if (!writer.wasSuccess()) { + showResultDialog(); + return; + } + + // Share the file. + 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())); + } + + private void showResultDialog() { + DialogManager.showMessageDialog(this, writer.getErrorMessage(), writer.wasSuccess(), + new OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int arg1) { + dialog.dismiss(); + finish(); + } + }); + } + + public static void handleExportTrackAction(Context ctx, long trackId, int actionCode) { + if (trackId < 0) { + return; + } + + TrackFileFormat exportFormat = null; + switch (actionCode) { + case Constants.SAVE_GPX_FILE: + case Constants.SHARE_GPX_FILE: + exportFormat = TrackFileFormat.GPX; + break; + case Constants.SAVE_KML_FILE: + case Constants.SHARE_KML_FILE: + exportFormat = TrackFileFormat.KML; + break; + case Constants.SAVE_CSV_FILE: + case Constants.SHARE_CSV_FILE: + exportFormat = TrackFileFormat.CSV; + break; + case Constants.SAVE_TCX_FILE: + case Constants.SHARE_TCX_FILE: + exportFormat = TrackFileFormat.TCX; + break; + default: + throw new IllegalArgumentException("Warning unhandled action code: " + actionCode); + } + + boolean shareFile = false; + switch (actionCode) { + case Constants.SHARE_GPX_FILE: + case Constants.SHARE_KML_FILE: + case Constants.SHARE_CSV_FILE: + case Constants.SHARE_TCX_FILE: + shareFile = true; + } + + Uri uri = ContentUris.withAppendedId(TracksColumns.CONTENT_URI, trackId); + + Intent intent = new Intent(ctx, SaveActivity.class); + intent.setAction(ctx.getString(R.string.save_intent_action)); + intent.setDataAndType(uri, TracksColumns.CONTENT_ITEMTYPE); + intent.putExtra(EXTRA_FILE_FORMAT, exportFormat.ordinal()); + intent.putExtra(EXTRA_SHARE_FILE, shareFile); + ctx.startActivity(intent); + } +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/file/TempFileCleaner.java b/MyTracks/src/com/google/android/apps/mytracks/io/file/TempFileCleaner.java index 6f590342b..ec200f823 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/file/TempFileCleaner.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/file/TempFileCleaner.java @@ -1,12 +1,12 @@ /* * Copyright 2010 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 @@ -15,7 +15,10 @@ */ package com.google.android.apps.mytracks.io.file; +import static com.google.android.apps.mytracks.Constants.TAG; + import android.os.Environment; +import android.util.Log; import java.io.File; @@ -24,18 +27,18 @@ import java.io.File; * @author Sandor Dornbush */ public class TempFileCleaner { - + private long currentTimeMillis; public static void clean() { (new TempFileCleaner(System.currentTimeMillis())).cleanImpl(); } - + // @VisibleForTesting TempFileCleaner(long time) { currentTimeMillis = time; } - + private void cleanImpl() { if (!Environment.getExternalStorageState().equals( Environment.MEDIA_MOUNTED)) { @@ -64,7 +67,9 @@ public class TempFileCleaner { long oldest = currentTimeMillis - 1000 * 3600; for (File f : dir.listFiles()) { if (f.lastModified() < oldest) { - f.delete(); + if (!f.delete()) { + Log.w(TAG, "Failed to delete file " + f.getAbsolutePath()); + } count++; } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/file/TrackWriterImpl.java b/MyTracks/src/com/google/android/apps/mytracks/io/file/TrackWriterImpl.java index d74a9c8f4..dd18b5a11 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/file/TrackWriterImpl.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/file/TrackWriterImpl.java @@ -15,6 +15,8 @@ */ package com.google.android.apps.mytracks.io.file; +import static com.google.android.apps.mytracks.Constants.TAG; + import com.google.android.apps.mytracks.Constants; import com.google.android.apps.mytracks.content.MyTracksLocation; import com.google.android.apps.mytracks.content.MyTracksProviderUtils; @@ -120,7 +122,9 @@ class TrackWriterImpl implements TrackWriter { } catch (InterruptedException e) { Log.i(Constants.TAG, "The track write was interrupted"); if (file != null) { - file.delete(); + if (!file.delete()) { + Log.w(TAG, "Failed to delete file " + file.getAbsolutePath()); + } } success = false; errorMessage = R.string.error_operation_cancelled; @@ -195,7 +199,7 @@ class TrackWriterImpl implements TrackWriter { directory, track.getName(), writer.getExtension()); if (fileName == null) { Log.e(Constants.TAG, - "Unable to get a unique filename for " + fileName); + "Unable to get a unique filename for " + track.getName()); return false; } diff --git a/MyTracks/src/com/google/android/apps/mytracks/WriteProgressController.java b/MyTracks/src/com/google/android/apps/mytracks/io/file/WriteProgressController.java similarity index 95% rename from MyTracks/src/com/google/android/apps/mytracks/WriteProgressController.java rename to MyTracks/src/com/google/android/apps/mytracks/io/file/WriteProgressController.java index 3f0356756..861df8ddd 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/WriteProgressController.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/file/WriteProgressController.java @@ -13,9 +13,9 @@ * License for the specific language governing permissions and limitations under * the License. */ -package com.google.android.apps.mytracks; +package com.google.android.apps.mytracks.io.file; -import com.google.android.apps.mytracks.io.file.TrackWriter; +import com.google.android.apps.mytracks.DialogManager; import com.google.android.maps.mytracks.R; import android.app.Activity; @@ -41,7 +41,7 @@ class WriteProgressController { * dialog has been dismissed. Whether the write succeeded can be * determined by examining the {@link TrackWriter}. */ - public void onComplete(TrackWriter writer); + public void onComplete(); } private final Activity activity; @@ -104,7 +104,7 @@ class WriteProgressController { DialogManager.dismissDialogSafely(activity, dialog); if (onCompletionListener != null) { - onCompletionListener.onComplete(writer); + onCompletionListener.onComplete(); } } }; diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/gdata/AndroidGDataClient.java b/MyTracks/src/com/google/android/apps/mytracks/io/gdata/AndroidGDataClient.java index 7b037bb75..449056870 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/gdata/AndroidGDataClient.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/gdata/AndroidGDataClient.java @@ -1,12 +1,12 @@ /* * Copyright 2010 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 @@ -137,7 +137,7 @@ public class AndroidGDataClient implements GDataClient { /* * (non-Javadoc) - * + * * @see GDataClient#encodeUri(java.lang.String) */ public String encodeUri(String uri) { @@ -155,7 +155,7 @@ public class AndroidGDataClient implements GDataClient { /* * (non-Javadoc) - * + * * @see com.google.wireless.gdata.client.GDataClient#createQueryParams() */ public QueryParams createQueryParams() { @@ -273,7 +273,7 @@ public class AndroidGDataClient implements GDataClient { String errorMessage = null; HttpEntity entity = response.getEntity(); try { - if (response != null && entity != null) { + if (entity != null) { InputStream in = entity.getContent(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); byte[] buf = new byte[8192]; @@ -329,7 +329,7 @@ public class AndroidGDataClient implements GDataClient { /* * (non-Javadoc) - * + * * @see GDataClient#getFeedAsStream(java.lang.String, java.lang.String) */ public InputStream getFeedAsStream(String feedUrl, String authToken) @@ -358,7 +358,7 @@ public class AndroidGDataClient implements GDataClient { /* * (non-Javadoc) - * + * * @see GDataClient#createEntry */ public InputStream createEntry(String feedUrl, String authToken, @@ -377,7 +377,7 @@ public class AndroidGDataClient implements GDataClient { /* * (non-Javadoc) - * + * * @see GDataClient#updateEntry */ public InputStream updateEntry(String editUri, String authToken, @@ -395,7 +395,7 @@ public class AndroidGDataClient implements GDataClient { /* * (non-Javadoc) - * + * * @see GDataClient#deleteEntry */ public void deleteEntry(String editUri, String authToken) diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/mymaps/MyMapsFeature.java b/MyTracks/src/com/google/android/apps/mytracks/io/mymaps/MyMapsFeature.java index 2cd24d641..87aa2f236 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/mymaps/MyMapsFeature.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/mymaps/MyMapsFeature.java @@ -3,7 +3,7 @@ package com.google.android.apps.mytracks.io.mymaps; import com.google.android.maps.GeoPoint; -import java.io.Serializable; +import java.util.Random; import java.util.Vector; /** @@ -12,7 +12,7 @@ import java.util.Vector; * {@link MyMapsFeatureMetadata} object so that it can be more efficiently * transmitted to other activities. */ -class MyMapsFeature implements Serializable { +class MyMapsFeature { private static final long serialVersionUID = 8439035544430497236L; @@ -49,6 +49,8 @@ class MyMapsFeature implements Serializable { /** The metadata of this feature in a format efficient for transmission. */ private MyMapsFeatureMetadata featureInfo = new MyMapsFeatureMetadata(); + private final Random random = new Random(); + /** * Initializes a valid but empty feature. It will default to a * {@link #MARKER} with a blue placemark with a dot as an icon at the @@ -62,7 +64,7 @@ class MyMapsFeature implements Serializable { * * @param point The new point to add */ - public synchronized void addPoint(GeoPoint point) { + public void addPoint(GeoPoint point) { latitudeE6.add(point.getLatitudeE6()); longitudeE6.add(point.getLongitudeE6()); } @@ -71,10 +73,10 @@ class MyMapsFeature implements Serializable { * Generates a new local id for this feature based on the current time and * a random number. */ - public synchronized void generateAndroidId() { - Long time = System.currentTimeMillis(); - Integer random = (int) (Math.random() * 10000); - androidId = time.toString() + "." + random.toString(); + void generateAndroidId() { + long time = System.currentTimeMillis(); + int rand = random.nextInt(10000); + androidId = time + "." + rand; } /** @@ -82,7 +84,7 @@ class MyMapsFeature implements Serializable { * * @return The local id for this feature */ - public String getAndroidId() { + String getAndroidId() { return androidId; } @@ -102,7 +104,7 @@ class MyMapsFeature implements Serializable { * * @param description The new description of this feature */ - public synchronized void setDescription(String description) { + public void setDescription(String description) { featureInfo.setDescription(description); } @@ -196,7 +198,7 @@ class MyMapsFeature implements Serializable { * * @param title The new title of this feature */ - public synchronized void setTitle(String title) { + public void setTitle(String title) { featureInfo.setTitle(title); } @@ -207,7 +209,7 @@ class MyMapsFeature implements Serializable { * @param type The new type of the feature. That type must be one of * {@link #MARKER}, {@link #LINE}, or {@link #SHAPE} */ - public synchronized void setType(int type) { + public void setType(int type) { featureInfo.setType(type); } @@ -217,7 +219,7 @@ class MyMapsFeature implements Serializable { * * @param color The new ARGB color of this feature */ - public synchronized void setColor(int color) { + public void setColor(int color) { featureInfo.setColor(color); } @@ -227,7 +229,7 @@ class MyMapsFeature implements Serializable { * * @param url The new icon url of the feature */ - public synchronized void setIconUrl(String url) { + public void setIconUrl(String url) { featureInfo.setIconUrl(url); } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/mymaps/MyMapsFeatureMetadata.java b/MyTracks/src/com/google/android/apps/mytracks/io/mymaps/MyMapsFeatureMetadata.java index 7e94d24b9..acb1486c7 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/mymaps/MyMapsFeatureMetadata.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/mymaps/MyMapsFeatureMetadata.java @@ -82,6 +82,6 @@ class MyMapsFeatureMetadata { } public void setIconUrl(String url) { - iconUrl = new String(url); + iconUrl = url; } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/mymaps/MyMapsMapMetadata.java b/MyTracks/src/com/google/android/apps/mytracks/io/mymaps/MyMapsMapMetadata.java index 4ffb459b8..eab3b657d 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/mymaps/MyMapsMapMetadata.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/mymaps/MyMapsMapMetadata.java @@ -23,7 +23,7 @@ class MyMapsMapMetadata { } public void setTitle(String title) { - this.title = new String(title); + this.title = title; } public String getDescription() { @@ -31,7 +31,7 @@ class MyMapsMapMetadata { } public void setDescription(String description) { - this.description = new String(description); + this.description = description; } public boolean getSearchable() { @@ -47,6 +47,6 @@ class MyMapsMapMetadata { } public void setGDataEditUri(String editUri) { - this.gdataEditUri = new String(editUri); + this.gdataEditUri = editUri; } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/mymaps/XmlMapsGDataParserFactory.java b/MyTracks/src/com/google/android/apps/mytracks/io/mymaps/XmlMapsGDataParserFactory.java index 15c0a884e..3bba20e56 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/mymaps/XmlMapsGDataParserFactory.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/mymaps/XmlMapsGDataParserFactory.java @@ -85,8 +85,8 @@ class XmlMapsGDataParserFactory implements GDataParserFactory { @Override public GDataSerializer createSerializer(Entry en) { - if (en.getClass() == MapFeatureEntry.class) { - return new XmlMapsGDataSerializer(xmlFactory, en); + if (en instanceof MapFeatureEntry) { + return new XmlMapsGDataSerializer(xmlFactory, (MapFeatureEntry) en); } else { return new XmlEntryGDataSerializer(xmlFactory, en); } diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/mymaps/XmlMapsGDataSerializer.java b/MyTracks/src/com/google/android/apps/mytracks/io/mymaps/XmlMapsGDataSerializer.java index 80598788d..3251f2624 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/mymaps/XmlMapsGDataSerializer.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/mymaps/XmlMapsGDataSerializer.java @@ -1,7 +1,6 @@ // Copyright 2010 Google Inc. All Rights Reserved. package com.google.android.apps.mytracks.io.mymaps; -import com.google.wireless.gdata.data.Entry; import com.google.wireless.gdata.data.StringUtils; import com.google.wireless.gdata.parser.ParseException; import com.google.wireless.gdata.parser.xml.XmlGDataParser; @@ -10,14 +9,14 @@ import com.google.wireless.gdata.serializer.xml.XmlEntryGDataSerializer; import android.util.Log; -import org.xmlpull.v1.XmlPullParserException; -import org.xmlpull.v1.XmlSerializer; - import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; import java.util.Map; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlSerializer; + /** * Serializer of maps data for GData. */ @@ -29,11 +28,11 @@ class XmlMapsGDataSerializer extends XmlEntryGDataSerializer { private XmlParserFactory factory; private OutputStream stream; - public XmlMapsGDataSerializer(XmlParserFactory factory, Entry entry) { + public XmlMapsGDataSerializer(XmlParserFactory factory, MapFeatureEntry entry) { super(factory, entry); this.factory = factory; - this.entry = (MapFeatureEntry) entry; + this.entry = entry; } @Override diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/SendActivity.java b/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/SendActivity.java new file mode 100644 index 000000000..d19367402 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/SendActivity.java @@ -0,0 +1,685 @@ +/* + * Copyright 2011 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.io.sendtogoogle; + +import static com.google.android.apps.mytracks.Constants.TAG; + +import com.google.android.accounts.Account; +import com.google.android.apps.analytics.GoogleAnalyticsTracker; +import com.google.android.apps.mytracks.AccountChooser; +import com.google.android.apps.mytracks.Constants; +import com.google.android.apps.mytracks.DialogManager; +import com.google.android.apps.mytracks.MyMapsList; +import com.google.android.apps.mytracks.ProgressIndicator; +import com.google.android.apps.mytracks.content.MyTracksProviderUtils; +import com.google.android.apps.mytracks.content.Track; +import com.google.android.apps.mytracks.content.TracksColumns; +import com.google.android.apps.mytracks.io.AuthManager; +import com.google.android.apps.mytracks.io.AuthManagerFactory; +import com.google.android.apps.mytracks.io.SendToDocs; +import com.google.android.apps.mytracks.io.SendToFusionTables; +import com.google.android.apps.mytracks.io.SendToFusionTables.OnSendCompletedListener; +import com.google.android.apps.mytracks.io.SendToMyMaps; +import com.google.android.apps.mytracks.io.mymaps.MapsFacade; +import com.google.android.apps.mytracks.io.mymaps.MyMapsConstants; +import com.google.android.apps.mytracks.util.SystemUtils; +import com.google.android.apps.mytracks.util.UriUtils; +import com.google.android.maps.mytracks.R; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.ProgressDialog; +import android.content.ContentUris; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.content.Intent; +import android.content.SharedPreferences; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.os.HandlerThread; +import android.util.Log; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +/** + * Helper activity for managing the sending of tracks to Google services. + * + * @author Rodrigo Damazio + */ +public class SendActivity extends Activity implements ProgressIndicator { + public static final String EXTRA_SHARE_LINK = "share_link"; + + // Services + private MyTracksProviderUtils providerUtils; + private SharedPreferences sharedPreferences; + + private GoogleAnalyticsTracker tracker; + + // Authentication + private AuthManager lastAuth; + private final HashMap authMap = + new HashMap(); + private final AccountChooser accountChooser = new AccountChooser(); + + // Send request information. + private boolean shareRequested = false; + private long sendTrackId; + + // Send result information, used by SendToGoogleResultDialog. + private boolean sendToMyMapsSuccess = false; + private boolean sendToFusionTablesSuccess = false; + private boolean sendToDocsSuccess = false; + private String sendToMyMapsMapId; + private String sendToFusionTablesTableId; + + // TODO: Make these be used for showing results + @SuppressWarnings("unused") + private String sendToMyMapsMessage; + @SuppressWarnings("unused") + private String sendToFusionTablesMessage; + @SuppressWarnings("unused") + private String sendToDocsMessage; + + // State used while sending. + private SendDialog sendDialog; + private ProgressDialog progressDialog; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + providerUtils = MyTracksProviderUtils.Factory.get(this); + sharedPreferences = getSharedPreferences(Constants.SETTINGS_NAME, 0); + } + + @Override + protected void onStart() { + Log.d(TAG, "SendActivity.onStart"); + super.onStart(); + resetState(); + + tracker = GoogleAnalyticsTracker.getInstance(); + // Start the tracker in manual dispatch mode... + tracker.start(getString(R.string.google_analytics_id), getApplicationContext()); + tracker.setProductVersion("android-mytracks", SystemUtils.getMyTracksVersion(this)); + + Intent intent = getIntent(); + String action = intent.getAction(); + String type = intent.getType(); + Uri data = intent.getData(); + if (!Intent.ACTION_SEND.equals(action) || + !TracksColumns.CONTENT_ITEMTYPE.equals(type) || + !UriUtils.matchesContentUri(data, TracksColumns.CONTENT_URI)) { + Log.e(TAG, "Got bad send intent: " + intent); + finish(); + return; + } + + sendTrackId = ContentUris.parseId(data); + + sendDialog = new SendDialog(this); + sendDialog.setOwnerActivity(this); + sendDialog.setOnClickListener(new OnClickListener() { + @Override + public void onClick(DialogInterface arg0, int which) { + if (which != DialogInterface.BUTTON_POSITIVE) { + finish(); + return; + } + + doSend(); + } + }); + sendDialog.show(); + } + + @Override + protected void onStop() { + Log.d(TAG, "SendActivity.onStop"); + tracker.dispatch(); + tracker.stop(); + + super.onStop(); + } + + /** + * Initiates the process to send tracks to google. + * This is called once the user has selected sending options via the + * SendToGoogleDialog. + * + * TODO: Change this whole flow to an actual state machine. + */ + private void doSend() { + progressDialog = new ProgressDialog(this); + progressDialog.setIcon(android.R.drawable.ic_dialog_info); + progressDialog.setTitle(R.string.progress_title); + progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); + progressDialog.setMessage(""); + progressDialog.setMax(100); + progressDialog.setProgress(0); + progressDialog.show(); + + if (sendDialog.getSendToMyMaps()) { + sendToGoogleMapsOrPickMap(); + } else if (sendDialog.getSendToFusionTables()) { + authenticateToFusionTables(null); + } else if (sendDialog.getSendToDocs()) { + authenticateToGoogleDocs(); + } else { + Log.w(TAG, "Nowhere to upload to"); + onSendToGoogleDone(); + } + } + + private void sendToGoogleMapsOrPickMap() { + if (!sendDialog.getCreateNewMap()) { + // Ask the user to choose a map to upload into + Intent listIntent = new Intent(this, MyMapsList.class); + startActivityForResult(listIntent, Constants.GET_MAP); + // The callback for GET_MAP calls authenticateToGoogleMaps + } else { + authenticateToGoogleMaps(null); + } + } + + private void authenticateToGoogleMaps(Intent results) { + if (results == null) { results = new Intent(); } + + progressDialog.setProgress(0); + progressDialog.setMessage(getString( + R.string.progress_message_authenticating_mymaps)); + authenticate(results, Constants.AUTHENTICATE_TO_MY_MAPS, + MyMapsConstants.SERVICE_NAME); + // AUTHENTICATE_TO_MY_MAPS callback calls sendToGoogleMaps + } + + private void sendToGoogleMaps(String mapId) { + tracker.trackPageView("/send/maps"); + + SendToMyMaps.OnSendCompletedListener onCompletion = new SendToMyMaps.OnSendCompletedListener() { + @Override + public void onSendCompleted(String mapId, boolean success, int statusMessage) { + // TODO: Use this message + sendToMyMapsMessage = getString(statusMessage); + sendToMyMapsSuccess = success; + if (sendToMyMapsSuccess) { + sendToMyMapsMapId = mapId; + // Update the map id for this track: + try { + Track track = providerUtils.getTrack(sendTrackId); + if (track != null) { + track.setMapId(mapId); + providerUtils.updateTrack(track); + } else { + Log.w(TAG, "Updating map id failed."); + } + } catch (RuntimeException e) { + // If that fails whatever reasons we'll just log an error, but + // continue. + Log.w(TAG, "Updating map id failed.", e); + } + } + + onSendToGoogleMapsDone(); + } + }; + final SendToMyMaps sender = new SendToMyMaps(this, mapId, lastAuth, + sendTrackId, this /*progressIndicator*/, onCompletion); + + HandlerThread handlerThread = new HandlerThread("SendToMyMaps"); + handlerThread.start(); + Handler handler = new Handler(handlerThread.getLooper()); + handler.post(sender); + } + + private void onSendToGoogleMapsDone() { + if (sendDialog.getSendToFusionTables()) { + authenticateToFusionTables(null); + } else if (sendDialog.getSendToDocs()) { + authenticateToGoogleDocs(); + } else { + onSendToGoogleDone(); + } + } + + private void authenticateToFusionTables(Intent results) { + if (results == null) { results = new Intent(); } + + progressDialog.setProgress(0); + progressDialog.setMessage(getString( + R.string.progress_message_authenticating_fusiontables)); + authenticate(results, Constants.AUTHENTICATE_TO_FUSION_TABLES, + SendToFusionTables.SERVICE_ID); + // AUTHENTICATE_TO_FUSION_TABLES callback calls sendToFusionTables + } + + private void sendToFusionTables() { + tracker.trackPageView("/send/fusion_tables"); + + OnSendCompletedListener onCompletion = new OnSendCompletedListener() { + @Override + public void onSendCompleted(String tableId, boolean success, + int statusMessage) { + // TODO: Use this message + sendToFusionTablesMessage = getString(statusMessage); + sendToFusionTablesSuccess = success; + if (sendToFusionTablesSuccess) { + sendToFusionTablesTableId = tableId; + // Update the table id for this track: + try { + Track track = providerUtils.getTrack(sendTrackId); + if (track != null) { + track.setTableId(tableId); + providerUtils.updateTrack(track); + } else { + Log.w(TAG, "Updating table id failed."); + } + } catch (RuntimeException e) { + // If that fails whatever reasons we'll just log an error, but + // continue. + Log.w(TAG, "Updating table id failed.", e); + } + } + + onSendToFusionTablesDone(); + } + }; + final SendToFusionTables sender = new SendToFusionTables(this, lastAuth, + sendTrackId, this /*progressIndicator*/, onCompletion); + + HandlerThread handlerThread = new HandlerThread("SendToFusionTables"); + handlerThread.start(); + Handler handler = new Handler(handlerThread.getLooper()); + handler.post(sender); + } + + private void onSendToFusionTablesDone() { + if (sendDialog.getSendToDocs()) { + authenticateToGoogleDocs(); + } else { + onSendToGoogleDone(); + } + } + + private void authenticateToGoogleDocs() { + setProgressValue(0); + setProgressMessage( + R.string.progress_message_authenticating_docs); + authenticate(new Intent(), + Constants.AUTHENTICATE_TO_DOCLIST, + SendToDocs.GDATA_SERVICE_NAME_DOCLIST); + // AUTHENTICATE_TO_DOCLIST callback calls authenticateToGoogleTrix + } + + private void authenticateToGoogleTrix() { + setProgressValue(30); + setProgressMessage( + R.string.progress_message_authenticating_docs); + authenticate(new Intent(), + Constants.AUTHENTICATE_TO_TRIX, + SendToDocs.GDATA_SERVICE_NAME_TRIX); + // AUTHENTICATE_TO_TRIX callback calls sendToGoogleDocs + } + + private void sendToGoogleDocs() { + Log.d(TAG, "Sending to Docs...."); + tracker.trackPageView("/send/docs"); + + setProgressValue(50); + setProgressMessage(R.string.progress_message_sending_docs); + final SendToDocs sender = new SendToDocs(this, + authMap.get(SendToDocs.GDATA_SERVICE_NAME_TRIX), + authMap.get(SendToDocs.GDATA_SERVICE_NAME_DOCLIST), + this); + Runnable onCompletion = new Runnable() { + public void run() { + setProgressValue(100); + + // TODO: Use this message + sendToDocsMessage = sender.getStatusMessage(); + sendToDocsSuccess = sender.wasSuccess(); + + onSendToGoogleDocsDone(); + } + }; + sender.setOnCompletion(onCompletion); + sender.sendToDocs(sendTrackId); + } + + private void onSendToGoogleDocsDone() { + onSendToGoogleDone(); + } + + private void onSendToGoogleDone() { + tracker.dispatch(); + + final boolean sentToMyMaps = sendDialog.getSendToMyMaps(); + final boolean sentToFusionTables = sendDialog.getSendToFusionTables(); + List results = makeSendToGoogleResults(); + + runOnUiThread(new Runnable() { + @Override + public void run() { + progressDialog.dismiss(); + progressDialog = null; + sendDialog = null; + } + }); + + // We've finished sending the track to the user-selected services. Now + // we tell them the results of the upload, and optionally share the track. + // There are a few different paths through this code: + // + // 1. The user pre-requested a share (shareRequested == true). We're going + // to display the result dialog *without* the share button (the share + // listener will be null). The OK button listener will initiate the + // share. + // + // 2. The user did not pre-request a share, and the set of services to + // which we succeeded in uploading the track are compatible with + // sharing. We'll display a share button (the share listener will be + // non-null), and will share the link if the user clicks it. + // + // 3. The user did not pre-request a share, and the set of services to + // which we succeeded in uploading the track are incompatible with + // sharing. We won't display a share button. + + final boolean canShare = sendToFusionTablesTableId != null + || sendToMyMapsMapId != null; + + final OnClickListener finishListener = new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + finish(); + } + }; + + DialogInterface.OnClickListener doShareListener = null; + if (canShare) { + doShareListener = new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + if (!shareLinkToMap(sentToMyMaps, sentToFusionTables)) { + Log.w(TAG, "Failed to share link"); + } + + finishListener.onClick(dialog, which); + } + }; + } + + DialogInterface.OnClickListener onOkListener = (canShare && shareRequested) + ? doShareListener : finishListener; + DialogInterface.OnClickListener onShareListener = (canShare && !shareRequested) + ? doShareListener : null; + + AlertDialog sendToGoogleResultDialog = ResultDialogFactory.makeDialog(this, + results, onOkListener, onShareListener); + DialogManager.showDialogSafely(this, sendToGoogleResultDialog); + } + + boolean shareLinkToMap(boolean sentToMyMaps, boolean sentToFusionTables) { + String url = null; + if (sentToMyMaps && sendToMyMapsSuccess) { + // Prefer a link to My Maps + url = MapsFacade.buildMapUrl(sendToMyMapsMapId); + } else if (sentToFusionTables && sendToFusionTablesSuccess) { + // Otherwise try using the link to fusion tables + url = getFusionTablesUrl(sendTrackId); + } + + if (url != null) { + shareLinkToMap(url); + return true; + } + + return false; + } + + /** + * Shares a link to a My Map or Fusion Table via external app (email, gmail, ...) + * A chooser with apps that support text/plain will be shown to the user. + */ + private void shareLinkToMap(String url) { + boolean shareUrlOnly = sharedPreferences.getBoolean( + getString(R.string.share_url_only_key), false); + String msg = shareUrlOnly ? url : String.format( + getResources().getText(R.string.share_map_body_format).toString(), url); + + Intent shareIntent = new Intent(Intent.ACTION_SEND); + shareIntent.setType("text/plain"); + shareIntent.putExtra(Intent.EXTRA_SUBJECT, + getResources().getText(R.string.share_map_subject).toString()); + shareIntent.putExtra(Intent.EXTRA_TEXT, msg); + startActivity(Intent.createChooser(shareIntent, + getResources().getText(R.string.share_map).toString())); + } + + protected String getFusionTablesUrl(long trackId) { + Track track = providerUtils.getTrack(trackId); + return SendToFusionTables.getMapVisualizationUrl(track); + } + + /** + * Creates a list of {@link SendResult} instances based on the set of + * services selected in {@link SendDialog} and the results as known to + * this class. + */ + private List makeSendToGoogleResults() { + List results = new ArrayList(); + if (sendDialog.getSendToMyMaps()) { + results.add(new SendResult(SendType.MYMAPS, sendToMyMapsSuccess)); + } + if (sendDialog.getSendToFusionTables()) { + results.add(new SendResult(SendType.FUSION_TABLES, sendToFusionTablesSuccess)); + } + if (sendDialog.getSendToDocs()) { + results.add(new SendResult(SendType.DOCS, sendToDocsSuccess)); + } + + return results; + } + + /** + * 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) { + lastAuth = authMap.get(service); + if (lastAuth == null) { + Log.i(TAG, "Creating a new authentication for service: " + service); + lastAuth = AuthManagerFactory.getAuthManager(this, + Constants.GET_LOGIN, + null, + true, + service); + authMap.put(service, lastAuth); + } + + Log.d(TAG, "Logging in to " + service + "..."); + if (AuthManagerFactory.useModernAuthManager()) { + runOnUiThread(new Runnable() { + @Override + public void run() { + chooseAccount(results, requestCode, service); + } + }); + } else { + doLogin(results, requestCode, service, null); + } + } + + private void chooseAccount(final Intent results, final int requestCode, + final String service) { + accountChooser.chooseAccount(SendActivity.this, + new AccountChooser.AccountHandler() { + @Override + public void handleAccountSelected(Account account) { + if (account == null) { + progressDialog.dismiss(); + progressDialog = null; + finish(); + } + + doLogin(results, requestCode, service, account); + } + }); + } + + private void doLogin(final Intent results, final int requestCode, + final String service, final Account account) { + // There's a chance that we go to a different activity, so close the dialog. + lastAuth.doLogin(new Runnable() { + public void run() { + Log.i(TAG, "Login success for " + service + "!"); + onActivityResult(requestCode, RESULT_OK, results); + } + }, account); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, + final Intent results) { + switch (requestCode) { + case Constants.GET_LOGIN: { + // TODO: This is a result from inside the auth manager, + // make this return path explicit. + if (resultCode != RESULT_OK || lastAuth == null || + !lastAuth.authResult(resultCode, results)) { + progressDialog.dismiss(); + } + break; + } + case Constants.GET_MAP: { + // User picked a map to upload to + if (resultCode == RESULT_OK) { + results.putExtra("trackid", sendTrackId); + if (results.hasExtra("mapid")) { + sendToMyMapsMapId = results.getStringExtra("mapid"); + } + authenticateToGoogleMaps(results); + } else { + onSendToGoogleDone(); + } + break; + } + case Constants.AUTHENTICATE_TO_MY_MAPS: { + // Authenticated with Google My Maps + if (results != null && resultCode == RESULT_OK) { + final String mapId; + if (results.hasExtra("mapid")) { + mapId = results.getStringExtra("mapid"); + } else { + mapId = "new"; + } + + sendToGoogleMaps(mapId); + } else { + onSendToGoogleDone(); + } + break; + } + case Constants.AUTHENTICATE_TO_FUSION_TABLES: { + // Authenticated with Google Fusion Tables + if (results != null && resultCode == RESULT_OK) { + sendToFusionTables(); + } else { + onSendToGoogleDone(); + } + break; + } + case Constants.AUTHENTICATE_TO_DOCLIST: { + // Authenticated with Google Docs + if (resultCode == RESULT_OK) { + authenticateToGoogleTrix(); + } else { + onSendToGoogleDone(); + } + break; + } + case Constants.AUTHENTICATE_TO_TRIX: { + // Authenticated with Trix + if (resultCode == RESULT_OK) { + sendToGoogleDocs(); + } else { + onSendToGoogleDone(); + } + break; + } + + default: { + Log.w(TAG, "Warning unhandled request code: " + requestCode); + } + } + } + + /** + * Resets status information for sending to MyMaps/Docs. + */ + private void resetState() { + sendToMyMapsMapId = null; + sendToMyMapsMessage = ""; + sendToMyMapsSuccess = true; + sendToFusionTablesMessage = ""; + sendToFusionTablesSuccess = true; + sendToDocsMessage = ""; + sendToDocsSuccess = true; + sendToFusionTablesTableId = null; + } + + @Override + public void setProgressMessage(final int resId) { + runOnUiThread(new Runnable() { + public void run() { + if (progressDialog != null) { + progressDialog.setMessage(getString(resId)); + } + } + }); + } + + @Override + public void setProgressValue(final int percent) { + runOnUiThread(new Runnable() { + public void run() { + if (progressDialog != null) { + progressDialog.setProgress(percent); + } + } + }); + } + + @Override + public void clearProgressMessage() { + progressDialog.setMessage(""); + } + + public static void sendToGoogle(Context ctx, long trackId, boolean shareLink) { + Uri uri = ContentUris.withAppendedId(TracksColumns.CONTENT_URI, trackId); + + Intent intent = new Intent(ctx, SendActivity.class); + intent.setAction(Intent.ACTION_SEND); + intent.setDataAndType(uri, TracksColumns.CONTENT_ITEMTYPE); + intent.putExtra(SendActivity.EXTRA_SHARE_LINK, shareLink); + ctx.startActivity(intent); + } +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/SendDialog.java b/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/SendDialog.java index 16df9c5e3..868e25ed1 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/SendDialog.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/SendDialog.java @@ -16,8 +16,6 @@ package com.google.android.apps.mytracks.io.sendtogoogle; import com.google.android.apps.mytracks.Constants; -import com.google.android.apps.mytracks.DialogManager; -import com.google.android.apps.mytracks.MyTracks; import com.google.android.maps.mytracks.R; import android.app.Dialog; @@ -42,15 +40,13 @@ import android.widget.RadioGroup; */ public class SendDialog extends Dialog { - private RadioGroup groupMyMaps; private RadioButton createNewMapRadioButton; private RadioButton pickMapRadioButton; private CheckBox sendToMyMapsCheckBox; private CheckBox sendToFusionTablesCheckBox; private CheckBox sendToDocsCheckBox; - private RadioButton sendStatsRadioButton; private RadioButton sendStatsAndPointsRadioButton; - private Button sendButton; + private OnClickListener clickListener; public SendDialog(Context context) { super(context); @@ -62,18 +58,38 @@ public class SendDialog extends Dialog { requestWindowFeature(Window.FEATURE_NO_TITLE); setContentView(R.layout.mytracks_send_to_google); + final Button sendButton = (Button) findViewById(R.id.sendtogoogle_send_now); + final RadioGroup groupMyMaps = (RadioGroup) findViewById(R.id.sendtogoogle_group_mymaps); + sendToMyMapsCheckBox = + (CheckBox) findViewById(R.id.sendtogoogle_google_mymaps); + sendToFusionTablesCheckBox = + (CheckBox) findViewById(R.id.sendtogoogle_google_fusiontables); + sendToDocsCheckBox = (CheckBox) findViewById(R.id.sendtogoogle_google_docs); + createNewMapRadioButton = + (RadioButton) findViewById(R.id.sendtogoogle_create_new_map); + pickMapRadioButton = + (RadioButton) findViewById(R.id.sendtogoogle_pick_existing_map); + RadioButton sendStatsRadioButton = (RadioButton) findViewById(R.id.sendtogoogle_send_stats); + sendStatsAndPointsRadioButton = (RadioButton) findViewById( + R.id.sendtogoogle_send_stats_and_points); + Button cancel = (Button) findViewById(R.id.sendtogoogle_cancel); cancel.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { - MyTracks.getInstance().dismissDialog(DialogManager.DIALOG_SEND_TO_GOOGLE); + if (clickListener != null) { + clickListener.onClick(SendDialog.this, BUTTON_NEGATIVE); + } + dismiss(); } }); Button send = (Button) findViewById(R.id.sendtogoogle_send_now); send.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { + if (clickListener != null) { + clickListener.onClick(SendDialog.this, BUTTON_POSITIVE); + } dismiss(); - MyTracks.getInstance().sendToGoogle(); } }); @@ -86,25 +102,9 @@ public class SendDialog extends Dialog { groupMyMaps.setVisibility(sendToMyMapsCheckBox.isChecked() ? View.VISIBLE : View.INVISIBLE); } }; - - sendButton = (Button) findViewById(R.id.sendtogoogle_send_now); - groupMyMaps = (RadioGroup) findViewById(R.id.sendtogoogle_group_mymaps); - sendToMyMapsCheckBox = - (CheckBox) findViewById(R.id.sendtogoogle_google_mymaps); sendToMyMapsCheckBox.setOnCheckedChangeListener(checkBoxListener); - sendToFusionTablesCheckBox = - (CheckBox) findViewById(R.id.sendtogoogle_google_fusiontables); sendToFusionTablesCheckBox.setOnCheckedChangeListener(checkBoxListener); - sendToDocsCheckBox = (CheckBox) findViewById(R.id.sendtogoogle_google_docs); sendToDocsCheckBox.setOnCheckedChangeListener(checkBoxListener); - createNewMapRadioButton = - (RadioButton) findViewById(R.id.sendtogoogle_create_new_map); - pickMapRadioButton = - (RadioButton) findViewById(R.id.sendtogoogle_pick_existing_map); - sendStatsRadioButton = - (RadioButton) findViewById(R.id.sendtogoogle_send_stats); - sendStatsAndPointsRadioButton = (RadioButton) findViewById( - R.id.sendtogoogle_send_stats_and_points); SharedPreferences prefs = getContext().getSharedPreferences(Constants.SETTINGS_NAME, 0); @@ -163,6 +163,10 @@ public class SendDialog extends Dialog { super.onStop(); } + public void setOnClickListener(OnClickListener clickListener) { + this.clickListener = clickListener; + } + public boolean getSendToMyMaps() { return sendToMyMapsCheckBox.isChecked(); } @@ -182,4 +186,4 @@ public class SendDialog extends Dialog { public boolean getSendStatsAndPoints() { return sendStatsAndPointsRadioButton.isChecked(); } -} +} \ No newline at end of file diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/PreferenceManager.java b/MyTracks/src/com/google/android/apps/mytracks/services/PreferenceManager.java index 3f5b666d5..efe060df5 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/PreferenceManager.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/PreferenceManager.java @@ -93,13 +93,13 @@ public class PreferenceManager implements OnSharedPreferenceChangeListener { return; } if (key == null || key.equals(minRecordingDistanceKey)) { - service.setMinRecordingDistance( - sharedPreferences.getInt( - minRecordingDistanceKey, - Constants.DEFAULT_MIN_RECORDING_DISTANCE)); + int minRecordingDistance = sharedPreferences.getInt( + minRecordingDistanceKey, + Constants.DEFAULT_MIN_RECORDING_DISTANCE); + service.setMinRecordingDistance(minRecordingDistance); Log.d(Constants.TAG, "TrackRecordingService: minRecordingDistance = " - + service.getMinRecordingDistance()); + + minRecordingDistance); } if (key == null || key.equals(maxRecordingDistanceKey)) { service.setMaxRecordingDistance(sharedPreferences.getInt( diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/ServiceUtils.java b/MyTracks/src/com/google/android/apps/mytracks/services/ServiceUtils.java new file mode 100644 index 000000000..5268ed7f2 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/services/ServiceUtils.java @@ -0,0 +1,88 @@ +/* + * Copyright 2011 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.services; + +import static com.google.android.apps.mytracks.Constants.TAG; + +import com.google.android.apps.mytracks.Constants; +import com.google.android.maps.mytracks.R; + +import android.app.ActivityManager; +import android.app.ActivityManager.RunningServiceInfo; +import android.content.ComponentName; +import android.content.Context; +import android.content.SharedPreferences; +import android.os.RemoteException; +import android.util.Log; + +import java.util.List; + +/** + * Helper for reading service state. + * + * @author Rodrigo Damazio + */ +public class ServiceUtils { + + /** + * Checks whether we're currently recording. + * The checking is done by calling the service, if provided, or alternatively by reading + * recording state saved to preferences. + * + * @param ctx the current context + * @param service the service, or null if not bound to it + * @param preferences the preferences, or null if not available + * @return true if the service is recording (or supposed to be recording), false otherwise + */ + public static boolean isRecording(Context ctx, ITrackRecordingService service, SharedPreferences preferences) { + if (service != null) { + try { + return service.isRecording(); + } catch (RemoteException e) { + Log.e(TAG, "Failed to check if service is recording", e); + } catch (IllegalStateException e) { + Log.e(TAG, "Failed to check if service is recording", e); + } + } + + if (preferences == null) { + preferences = ctx.getSharedPreferences(Constants.SETTINGS_NAME, 0); + } + return preferences.getLong(ctx.getString(R.string.recording_track_key), -1) > 0; + } + + /** + * Checks whether the recording service is currently running. + * + * @param ctx the current context + * @return true if the service is running, false otherwise + */ + public static boolean isServiceRunning(Context ctx) { + ActivityManager activityManager = (ActivityManager) ctx.getSystemService(Context.ACTIVITY_SERVICE); + List services = activityManager.getRunningServices(Integer.MAX_VALUE); + + for (RunningServiceInfo serviceInfo : services) { + ComponentName componentName = serviceInfo.service; + String serviceName = componentName.getClassName(); + if (serviceName.equals(TrackRecordingService.class.getName())) { + return true; + } + } + return false; + } + + private ServiceUtils() {} +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/TrackRecordingService.java b/MyTracks/src/com/google/android/apps/mytracks/services/TrackRecordingService.java index 6dd5a76f3..d1e1a5bf7 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/TrackRecordingService.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/TrackRecordingService.java @@ -15,20 +15,20 @@ */ package com.google.android.apps.mytracks.services; -import static com.google.android.apps.mytracks.Constants.TAG; import static com.google.android.apps.mytracks.Constants.RESUME_TRACK_EXTRA_NAME; +import static com.google.android.apps.mytracks.Constants.TAG; -import com.google.android.apps.mytracks.MyTracks; import com.google.android.apps.mytracks.Constants; +import com.google.android.apps.mytracks.MyTracks; import com.google.android.apps.mytracks.content.MyTracksLocation; import com.google.android.apps.mytracks.content.MyTracksProviderUtils; import com.google.android.apps.mytracks.content.Sensor; +import com.google.android.apps.mytracks.content.Sensor.SensorDataSet; import com.google.android.apps.mytracks.content.Track; import com.google.android.apps.mytracks.content.TracksColumns; import com.google.android.apps.mytracks.content.Waypoint; import com.google.android.apps.mytracks.content.WaypointCreationRequest; import com.google.android.apps.mytracks.content.WaypointsColumns; -import com.google.android.apps.mytracks.content.Sensor.SensorDataSet; import com.google.android.apps.mytracks.services.sensors.SensorManager; import com.google.android.apps.mytracks.services.sensors.SensorManagerFactory; import com.google.android.apps.mytracks.services.tasks.PeriodicTaskExecutor; @@ -74,7 +74,7 @@ import java.util.concurrent.Executors; * * @author Leif Hendrik Wilden */ -public class TrackRecordingService extends Service implements LocationListener { +public class TrackRecordingService extends Service { static final int MAX_AUTO_RESUME_TRACK_RETRY_ATTEMPTS = 3; @@ -89,8 +89,8 @@ public class TrackRecordingService extends Service implements LocationListener { private int minRequiredAccuracy = Constants.DEFAULT_MIN_REQUIRED_ACCURACY; private int autoResumeTrackTimeout = - Constants.DEFAULT_AUTO_RESUME_TRACK_TIMEOUT; - + Constants.DEFAULT_AUTO_RESUME_TRACK_TIMEOUT; + private long recordingTrackId = -1; private long currentWaypointId = -1; @@ -120,9 +120,9 @@ public class TrackRecordingService extends Service implements LocationListener { private PeriodicTaskExecutor splitExecutor; private SensorManager sensorManager; - + private PreferenceManager prefManager; - + /** * The interval in milliseconds that we have requested to be notified of gps * readings. @@ -135,6 +135,34 @@ public class TrackRecordingService extends Service implements LocationListener { private LocationListenerPolicy locationListenerPolicy = new AbsoluteLocationListenerPolicy(0); + private LocationListener locationListener = new LocationListener() { + @Override + public void onProviderDisabled(String provider) { + // Do nothing + } + + @Override + public void onProviderEnabled(String provider) { + // Do nothing + } + + @Override + public void onStatusChanged(String provider, int status, Bundle extras) { + // Do nothing + } + + @Override + public void onLocationChanged(final Location location) { + executorService.submit( + new Runnable() { + @Override + public void run() { + onLocationChangedAsync(location); + } + }); + } + }; + /** * Task invoked by a timer periodically to make sure the location listener is * still registered. @@ -171,7 +199,7 @@ public class TrackRecordingService extends Service implements LocationListener { * The most recent recording track. */ private Track recordingTrack; - + /** * Is the service currently recording a track? */ @@ -192,10 +220,556 @@ public class TrackRecordingService extends Service implements LocationListener { */ private ExecutorService executorService; + private ServiceBinder binder = new ServiceBinder(this); + /* - * Utility functions + * Application lifetime events: */ + @Override + public void onCreate() { + super.onCreate(); + Log.d(TAG, "TrackRecordingService.onCreate"); + providerUtils = MyTracksProviderUtils.Factory.get(this); + notificationManager = + (NotificationManager) getSystemService(NOTIFICATION_SERVICE); + locationManager = (LocationManager) getSystemService(LOCATION_SERVICE); + + setUpTaskExecutors(); + executorService = Executors.newSingleThreadExecutor(); + + prefManager = new PreferenceManager(this); + + registerLocationListener(); + + /* + * After 5 min, check every minute that location listener still is + * registered and spit out additional debugging info to the logs: + */ + timer.schedule(checkLocationListener, 1000 * 60 * 5, 1000 * 60); + + // Try to restore previous recording state in case this service has been + // restarted by the system, which can sometimes happen. + recordingTrack = getRecordingTrack(); + if (recordingTrack != null) { + restoreStats(recordingTrack); + isRecording = true; + } else { + if (recordingTrackId != -1) { + // Make sure we have consistent state in shared preferences. + Log.w(TAG, "TrackRecordingService.onCreate: " + + "Resetting an orphaned recording track = " + recordingTrackId); + } + prefManager.setRecordingTrack(recordingTrackId = -1); + } + showNotification(); + } + + @Override + public void onStart(Intent intent, int startId) { + handleStartCommand(intent, startId); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + handleStartCommand(intent, startId); + return START_STICKY; + } + + private void handleStartCommand(Intent intent, int startId) { + Log.d(TAG, + "TrackRecordingService.handleStartCommand: " + startId); + + // Check if called on phone reboot with resume intent. + if (intent != null && + intent.getBooleanExtra(RESUME_TRACK_EXTRA_NAME, false)) { + Log.d(TAG, "TrackRecordingService: requested resume"); + + // Make sure that the current track exists and is fresh enough. + if (recordingTrack == null || !shouldResumeTrack(recordingTrack)) { + Log.i(TAG, + "TrackRecordingService: Not resuming, because the previous track (" + + recordingTrack + ") doesn't exist or is too old"); + isRecording = false; + prefManager.setRecordingTrack(recordingTrackId = -1); + stopSelfResult(startId); + return; + } + + Log.i(TAG, "TrackRecordingService: resuming"); + } + } + + @Override + public IBinder onBind(Intent intent) { + Log.d(TAG, "TrackRecordingService.onBind"); + return binder; + } + + @Override + public boolean onUnbind(Intent intent) { + Log.d(TAG, "TrackRecordingService.onUnbind"); + return super.onUnbind(intent); + } + + @Override + public void onDestroy() { + Log.d(TAG, "TrackRecordingService.onDestroy"); + + isRecording = false; + showNotification(); + prefManager.shutdown(); + prefManager = null; + checkLocationListener.cancel(); + checkLocationListener = null; + timer.cancel(); + timer.purge(); + unregisterLocationListener(); + shutdownTaskExecutors(); + executorService.shutdown(); + if (sensorManager != null) { + sensorManager.shutdown(); + sensorManager = null; + } + + // Make sure we have no indirect references to this service. + locationManager = null; + notificationManager = null; + providerUtils = null; + binder.detachFromService(); + binder = null; + + // This should be the last operation. + releaseWakeLock(); + + super.onDestroy(); + } + + private void setAutoResumeTrackRetries(SharedPreferences sharedPreferences, int retryAttempts) { + Log.d(TAG, "Updating auto-resume retry attempts to: " + retryAttempts); + prefManager.setAutoResumeTrackCurrentRetry(retryAttempts); + } + + private boolean shouldResumeTrack(Track track) { + Log.d(TAG, "shouldResumeTrack: autoResumeTrackTimeout = " + + autoResumeTrackTimeout); + + // Check if we haven't exceeded the maximum number of retry attempts. + SharedPreferences sharedPreferences = + getSharedPreferences(Constants.SETTINGS_NAME, 0); + int retries = sharedPreferences.getInt( + getString(R.string.auto_resume_track_current_retry_key), 0); + Log.d(TAG, + "shouldResumeTrack: Attempting to auto-resume the track (" + + (retries + 1) + "/" + MAX_AUTO_RESUME_TRACK_RETRY_ATTEMPTS + ")"); + if (retries >= MAX_AUTO_RESUME_TRACK_RETRY_ATTEMPTS) { + Log.i(TAG, + "shouldResumeTrack: Not resuming because exceeded the maximum " + + "number of auto-resume retries"); + return false; + } + + // Increase number of retry attempts. + setAutoResumeTrackRetries(sharedPreferences, retries + 1); + + // Check for special cases. + if (autoResumeTrackTimeout == 0) { + // Never resume. + Log.d(TAG, + "shouldResumeTrack: Auto-resume disabled (never resume)"); + return false; + } else if (autoResumeTrackTimeout == -1) { + // Always resume. + Log.d(TAG, + "shouldResumeTrack: Auto-resume forced (always resume)"); + return true; + } + + // Check if the last modified time is within the acceptable range. + long lastModified = + track.getStatistics() != null ? track.getStatistics().getStopTime() : 0; + Log.d(TAG, + "shouldResumeTrack: lastModified = " + lastModified + + ", autoResumeTrackTimeout: " + autoResumeTrackTimeout); + return lastModified > 0 && System.currentTimeMillis() - lastModified <= + autoResumeTrackTimeout * 60L * 1000L; + } + + /* + * Setup/shutdown methods. + */ + + /** + * Tries to acquire a partial wake lock if not already acquired. Logs errors + * and gives up trying in case the wake lock cannot be acquired. + */ + private void acquireWakeLock() { + try { + PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); + if (pm == null) { + Log.e(TAG, + "TrackRecordingService: Power manager not found!"); + return; + } + if (wakeLock == null) { + wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, + TAG); + if (wakeLock == null) { + Log.e(TAG, + "TrackRecordingService: Could not create wake lock (null)."); + return; + } + } + if (!wakeLock.isHeld()) { + wakeLock.acquire(); + if (!wakeLock.isHeld()) { + Log.e(TAG, + "TrackRecordingService: Could not acquire wake lock."); + } + } + } catch (RuntimeException e) { + Log.e(TAG, + "TrackRecordingService: Caught unexpected exception: " + + e.getMessage(), e); + } + } + + /** + * Releases the wake lock if it's currently held. + */ + private void releaseWakeLock() { + if (wakeLock != null && wakeLock.isHeld()) { + wakeLock.release(); + wakeLock = null; + } + } + + /** + * Shows the notification message and icon in the notification bar. + */ + private void showNotification() { + final ApiPlatformAdapter apiPlatformAdapter = + ApiFeatures.getInstance().getApiPlatformAdapter(); + if (isRecording) { + Notification notification = new Notification( + R.drawable.arrow_320, null /* tickerText */, + System.currentTimeMillis()); + PendingIntent contentIntent = PendingIntent.getActivity( + this, 0 /* requestCode */, new Intent(this, MyTracks.class), + 0 /* flags */); + notification.setLatestEventInfo(this, getString(R.string.app_name), + getString(R.string.recording_your_track), contentIntent); + notification.flags += Notification.FLAG_NO_CLEAR; + apiPlatformAdapter.startForeground(this, notificationManager, 1, + notification); + } else { + apiPlatformAdapter.stopForeground(this, notificationManager, 1); + } + } + + private void setUpTaskExecutors() { + announcementExecutor = new PeriodicTaskExecutor( + this, new StatusAnnouncerFactory(ApiFeatures.getInstance())); + splitExecutor = new PeriodicTaskExecutor(this, new SplitTask.Factory()); + } + + private void shutdownTaskExecutors() { + Log.d(TAG, "TrackRecordingService.shutdownExecuters"); + try { + announcementExecutor.shutdown(); + } finally { + announcementExecutor = null; + } + try { + splitExecutor.shutdown(); + } finally { + splitExecutor = null; + } + } + + private void registerLocationListener() { + if (locationManager == null) { + Log.e(TAG, + "TrackRecordingService: Do not have any location manager."); + return; + } + Log.d(TAG, + "Preparing to register location listener w/ TrackRecordingService..."); + try { + long desiredInterval = locationListenerPolicy.getDesiredPollingInterval(); + locationManager.requestLocationUpdates( + LocationManager.GPS_PROVIDER, desiredInterval, + locationListenerPolicy.getMinDistance(), + // , 0 /* minDistance, get all updates to properly time pauses */ + locationListener); + currentRecordingInterval = desiredInterval; + Log.d(TAG, + "...location listener now registered w/ TrackRecordingService @ " + + currentRecordingInterval); + } catch (RuntimeException e) { + Log.e(TAG, + "Could not register location listener: " + e.getMessage(), e); + } + } + + private void unregisterLocationListener() { + if (locationManager == null) { + Log.e(TAG, + "TrackRecordingService: Do not have any location manager."); + return; + } + locationManager.removeUpdates(locationListener); + Log.d(TAG, + "Location listener now unregistered w/ TrackRecordingService."); + } + + /* + * Recording lifecycle. + */ + + public long startNewTrack() { + Log.d(TAG, "TrackRecordingService.startNewTrack"); + if (recordingTrackId != -1 || isRecording) { + throw new IllegalStateException("A track is already in progress!"); + } + + long startTime = System.currentTimeMillis(); + acquireWakeLock(); + + Track track = new Track(); + TripStatistics trackStats = track.getStatistics(); + trackStats.setStartTime(startTime); + track.setStartId(-1); + Uri trackUri = providerUtils.insertTrack(track); + recordingTrackId = Long.parseLong(trackUri.getLastPathSegment()); + track.setId(recordingTrackId); + track.setName(new DefaultTrackNameFactory(this).newTrackName( + recordingTrackId, startTime)); + isRecording = true; + isMoving = true; + + providerUtils.updateTrack(track); + statsBuilder = new TripStatisticsBuilder(startTime); + statsBuilder.setMinRecordingDistance(minRecordingDistance); + waypointStatsBuilder = new TripStatisticsBuilder(startTime); + waypointStatsBuilder.setMinRecordingDistance(minRecordingDistance); + currentWaypointId = insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS); + length = 0; + showNotification(); + registerLocationListener(); + sensorManager = SensorManagerFactory.getSensorManager(this); + if (sensorManager != null) { + sensorManager.onStartTrack(); + } + + // Reset the number of auto-resume retries. + setAutoResumeTrackRetries( + getSharedPreferences(Constants.SETTINGS_NAME, 0), 0); + // Persist the current recording track. + prefManager.setRecordingTrack(recordingTrackId); + + // Notify the world that we're now recording. + sendTrackBroadcast( + R.string.track_started_broadcast_action, recordingTrackId); + announcementExecutor.restore(); + splitExecutor.restore(); + + return recordingTrackId; + } + + private void restoreStats(Track track) { + Log.d(TAG, + "Restoring stats of track with ID: " + track.getId()); + + TripStatistics stats = track.getStatistics(); + statsBuilder = new TripStatisticsBuilder(stats.getStartTime()); + statsBuilder.setMinRecordingDistance(minRecordingDistance); + + length = 0; + lastValidLocation = null; + + Waypoint waypoint = providerUtils.getFirstWaypoint(recordingTrackId); + if (waypoint != null && waypoint.getStatistics() != null) { + currentWaypointId = waypoint.getId(); + waypointStatsBuilder = new TripStatisticsBuilder( + waypoint.getStatistics()); + } else { + // This should never happen, but we got to do something so life goes on: + waypointStatsBuilder = new TripStatisticsBuilder(stats.getStartTime()); + currentWaypointId = -1; + } + waypointStatsBuilder.setMinRecordingDistance(minRecordingDistance); + + Cursor cursor = null; + try { + cursor = providerUtils.getLocationsCursor( + recordingTrackId, -1, Constants.MAX_LOADED_TRACK_POINTS, + true); + if (cursor != null) { + if (cursor.moveToLast()) { + do { + Location location = providerUtils.createLocation(cursor); + if (LocationUtils.isValidLocation(location)) { + statsBuilder.addLocation(location, location.getTime()); + if (lastValidLocation != null) { + length += location.distanceTo(lastValidLocation); + } + lastValidLocation = location; + } + } while (cursor.moveToPrevious()); + } + statsBuilder.getStatistics().setMovingTime(stats.getMovingTime()); + statsBuilder.pauseAt(stats.getStopTime()); + statsBuilder.resumeAt(System.currentTimeMillis()); + } else { + Log.e(TAG, "Could not get track points cursor."); + } + } catch (RuntimeException e) { + Log.e(TAG, "Error while restoring track.", e); + } finally { + if (cursor != null) { + cursor.close(); + } + } + + announcementExecutor.restore(); + splitExecutor.restore(); + } + + private void onLocationChangedAsync(Location location) { + Log.d(TAG, "TrackRecordingService.onLocationChanged"); + + try { + // Don't record if the service has been asked to pause recording: + if (!isRecording) { + Log.w(TAG, + "Not recording because recording has been paused."); + return; + } + + // This should never happen, but just in case (we really don't want the + // service to crash): + if (location == null) { + Log.w(TAG, + "Location changed, but location is null."); + return; + } + + // Don't record if the accuracy is too bad: + if (location.getAccuracy() > minRequiredAccuracy) { + Log.d(TAG, + "Not recording. Bad accuracy."); + return; + } + + // At least one track must be available for appending points: + recordingTrack = getRecordingTrack(); + if (recordingTrack == null) { + Log.d(TAG, + "Not recording. No track to append to available."); + return; + } + + // Update the idle time if needed. + locationListenerPolicy.updateIdleTime(statsBuilder.getIdleTime()); + addLocationToStats(location); + if (currentRecordingInterval != + locationListenerPolicy.getDesiredPollingInterval()) { + registerLocationListener(); + } + + Location lastRecordedLocation = providerUtils.getLastLocation(); + long lastRecordedLocationId = + providerUtils.getLastLocationId(recordingTrackId); + double distanceToLastRecorded = Double.POSITIVE_INFINITY; + if (lastRecordedLocation != null) { + distanceToLastRecorded = location.distanceTo(lastRecordedLocation); + } + double distanceToLast = Double.POSITIVE_INFINITY; + if (lastLocation != null) { + distanceToLast = location.distanceTo(lastLocation); + } + boolean hasSensorData = sensorManager != null + && sensorManager.isEnabled() + && sensorManager.getSensorDataSet() != null + && sensorManager.isDataValid(); + + // If the user has been stationary for two recording just record the first + // two and ignore the rest. This code will only have an effect if the + // maxRecordingDistance = 0 + if (distanceToLast == 0 && !hasSensorData) { + if (isMoving) { + Log.d(TAG, "Found two identical locations."); + isMoving = false; + if (lastLocation != null && lastRecordedLocation != null + && !lastRecordedLocation.equals(lastLocation)) { + // Need to write the last location. This will happen when + // lastRecordedLocation.distance(lastLocation) < + // minRecordingDistance + if (!insertLocation(recordingTrack, lastLocation, + lastRecordedLocation, lastRecordedLocationId, + recordingTrackId)) { + return; + } + } + } else { + Log.d(TAG, + "Not recording. More than two identical locations."); + } + } else if (distanceToLastRecorded > minRecordingDistance + || hasSensorData) { + if (lastLocation != null && !isMoving) { + // Last location was the last stationary location. Need to go back and + // add it. + if (!insertLocation(recordingTrack, lastLocation, + lastRecordedLocation, lastRecordedLocationId, recordingTrackId)) { + return; + } + lastRecordedLocationId++; + isMoving = true; + } + + // If separation from last recorded point is too large insert a + // separator to indicate end of a segment: + boolean startNewSegment = + lastRecordedLocation != null + && lastRecordedLocation.getLatitude() < 90 + && distanceToLastRecorded > maxRecordingDistance + && recordingTrack.getStartId() >= 0; + if (startNewSegment) { + // Insert a separator point to indicate start of new track: + Log.d(TAG, "Inserting a separator."); + Location separator = new Location(LocationManager.GPS_PROVIDER); + separator.setLongitude(0); + separator.setLatitude(100); + separator.setTime(lastRecordedLocation.getTime()); + providerUtils.insertTrackPoint(separator, recordingTrackId); + } + + if (!insertLocation(recordingTrack, location, lastRecordedLocation, + lastRecordedLocationId, recordingTrackId)) { + return; + } + } else { + Log.d(TAG, String.format( + "Not recording. Distance to last recorded point (%f m) is less than" + + " %d m.", distanceToLastRecorded, minRecordingDistance)); + // Return here so that the location is NOT recorded as the last location. + return; + } + } catch (Error e) { + // Probably important enough to rethrow. + Log.e(TAG, "Error in onLocationChanged", e); + throw e; + } catch (RuntimeException e) { + // Safe usually to trap exceptions. + Log.e(TAG, + "Trapping exception in onLocationChanged", e); + throw e; + } + lastLocation = location; + } + /** * Inserts a new location in the track points db and updates the corresponding * track in the track db. @@ -308,326 +882,6 @@ public class TrackRecordingService extends Service implements LocationListener { } } - /** - * Tries to acquire a partial wake lock if not already acquired. Logs errors - * and gives up trying in case the wake lock cannot be acquired. - */ - private void acquireWakeLock() { - try { - PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); - if (pm == null) { - Log.e(TAG, - "TrackRecordingService: Power manager not found!"); - return; - } - if (wakeLock == null) { - wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, - TAG); - if (wakeLock == null) { - Log.e(TAG, - "TrackRecordingService: Could not create wake lock (null)."); - return; - } - } - if (!wakeLock.isHeld()) { - wakeLock.acquire(); - if (!wakeLock.isHeld()) { - Log.e(TAG, - "TrackRecordingService: Could not acquire wake lock."); - } - } - } catch (RuntimeException e) { - Log.e(TAG, - "TrackRecordingService: Caught unexpected exception: " - + e.getMessage(), e); - } - } - - /** - * Releases the wake lock if it's currently held. - */ - private void releaseWakeLock() { - if (wakeLock != null && wakeLock.isHeld()) { - wakeLock.release(); - wakeLock = null; - } - } - - /** - * Shows the notification message and icon in the notification bar. - */ - public void showNotification() { - final ApiPlatformAdapter apiPlatformAdapter = - ApiFeatures.getInstance().getApiPlatformAdapter(); - if (isRecording) { - Notification notification = new Notification( - R.drawable.arrow_320, null /* tickerText */, - System.currentTimeMillis()); - PendingIntent contentIntent = PendingIntent.getActivity( - this, 0 /* requestCode */, new Intent(this, MyTracks.class), - 0 /* flags */); - notification.setLatestEventInfo(this, getString(R.string.app_name), - getString(R.string.recording_your_track), contentIntent); - notification.flags += Notification.FLAG_NO_CLEAR; - apiPlatformAdapter.startForeground(this, notificationManager, 1, - notification); - } else { - apiPlatformAdapter.stopForeground(this, notificationManager, 1); - } - } - - public void registerLocationListener() { - if (locationManager == null) { - Log.e(TAG, - "TrackRecordingService: Do not have any location manager."); - return; - } - Log.d(TAG, - "Preparing to register location listener w/ TrackRecordingService..."); - try { - long desiredInterval = locationListenerPolicy.getDesiredPollingInterval(); - locationManager.requestLocationUpdates( - LocationManager.GPS_PROVIDER, desiredInterval, - locationListenerPolicy.getMinDistance(), - // , 0 /* minDistance, get all updates to properly time pauses */ - TrackRecordingService.this); - currentRecordingInterval = desiredInterval; - Log.d(TAG, - "...location listener now registered w/ TrackRecordingService @ " - + currentRecordingInterval); - } catch (RuntimeException e) { - Log.e(TAG, - "Could not register location listener: " + e.getMessage(), e); - } - } - - public void unregisterLocationListener() { - if (locationManager == null) { - Log.e(TAG, - "TrackRecordingService: Do not have any location manager."); - return; - } - locationManager.removeUpdates(this); - Log.d(TAG, - "Location listener now unregistered w/ TrackRecordingService."); - } - - private Track getRecordingTrack() { - if (recordingTrackId < 0) { - return null; - } - - return providerUtils.getTrack(recordingTrackId); - } - - private void restoreStats(Track track) { - Log.d(TAG, - "Restoring stats of track with ID: " + track.getId()); - - TripStatistics stats = track.getStatistics(); - statsBuilder = new TripStatisticsBuilder(stats.getStartTime()); - statsBuilder.setMinRecordingDistance(minRecordingDistance); - - length = 0; - lastValidLocation = null; - - Waypoint waypoint = providerUtils.getFirstWaypoint(recordingTrackId); - if (waypoint != null && waypoint.getStatistics() != null) { - currentWaypointId = waypoint.getId(); - waypointStatsBuilder = new TripStatisticsBuilder( - waypoint.getStatistics()); - } else { - // This should never happen, but we got to do something so life goes on: - waypointStatsBuilder = new TripStatisticsBuilder(stats.getStartTime()); - currentWaypointId = -1; - } - waypointStatsBuilder.setMinRecordingDistance(minRecordingDistance); - - Cursor cursor = null; - try { - cursor = providerUtils.getLocationsCursor( - recordingTrackId, -1, Constants.MAX_LOADED_TRACK_POINTS, - true); - if (cursor != null) { - if (cursor.moveToLast()) { - do { - Location location = providerUtils.createLocation(cursor); - if (LocationUtils.isValidLocation(location)) { - statsBuilder.addLocation(location, location.getTime()); - if (lastValidLocation != null) { - length += location.distanceTo(lastValidLocation); - } - lastValidLocation = location; - } - } while (cursor.moveToPrevious()); - } - statsBuilder.getStatistics().setMovingTime(stats.getMovingTime()); - statsBuilder.pauseAt(stats.getStopTime()); - statsBuilder.resumeAt(System.currentTimeMillis()); - } else { - Log.e(TAG, "Could not get track points cursor."); - } - } catch (RuntimeException e) { - Log.e(TAG, "Error while restoring track.", e); - } finally { - if (cursor != null) { - cursor.close(); - } - } - - announcementExecutor.restore(); - splitExecutor.restore(); - } - - /* - * Location listener implementation: ================================= - */ - @Override - public void onLocationChanged(final Location location) { - executorService.submit( - new Runnable() { - @Override - public void run() { - onLocationChangedAsync(location); - } - }); - } - - private void onLocationChangedAsync(Location location) { - Log.d(TAG, "TrackRecordingService.onLocationChanged"); - - try { - // Don't record if the service has been asked to pause recording: - if (!isRecording) { - Log.w(TAG, - "Not recording because recording has been paused."); - return; - } - - // This should never happen, but just in case (we really don't want the - // service to crash): - if (location == null) { - Log.w(TAG, - "Location changed, but location is null."); - return; - } - - // Don't record if the accuracy is too bad: - if (location.getAccuracy() > minRequiredAccuracy) { - Log.d(TAG, - "Not recording. Bad accuracy."); - return; - } - - // At least one track must be available for appending points: - recordingTrack = getRecordingTrack(); - if (recordingTrack == null) { - Log.d(TAG, - "Not recording. No track to append to available."); - return; - } - - // Update the idle time if needed. - locationListenerPolicy.updateIdleTime(statsBuilder.getIdleTime()); - addLocationToStats(location); - if (currentRecordingInterval != - locationListenerPolicy.getDesiredPollingInterval()) { - registerLocationListener(); - } - - Location lastRecordedLocation = providerUtils.getLastLocation(); - long lastRecordedLocationId = - providerUtils.getLastLocationId(recordingTrackId); - double distanceToLastRecorded = Double.POSITIVE_INFINITY; - if (lastRecordedLocation != null) { - distanceToLastRecorded = location.distanceTo(lastRecordedLocation); - } - double distanceToLast = Double.POSITIVE_INFINITY; - if (lastLocation != null) { - distanceToLast = location.distanceTo(lastLocation); - } - boolean hasSensorData = sensorManager != null - && sensorManager.isEnabled() - && sensorManager.getSensorDataSet() != null - && sensorManager.isDataValid(); - - // If the user has been stationary for two recording just record the first - // two and ignore the rest. This code will only have an effect if the - // maxRecordingDistance = 0 - if (distanceToLast == 0 && !hasSensorData) { - if (isMoving) { - Log.d(TAG, "Found two identical locations."); - isMoving = false; - if (lastLocation != null && lastRecordedLocation != null - && !lastRecordedLocation.equals(lastLocation)) { - // Need to write the last location. This will happen when - // lastRecordedLocation.distance(lastLocation) < - // minRecordingDistance - if (!insertLocation(recordingTrack, lastLocation, - lastRecordedLocation, lastRecordedLocationId, - recordingTrackId)) { - return; - } - lastRecordedLocationId++; - } - } else { - Log.d(TAG, - "Not recording. More than two identical locations."); - } - } else if (distanceToLastRecorded > minRecordingDistance - || hasSensorData) { - if (lastLocation != null && !isMoving) { - // Last location was the last stationary location. Need to go back and - // add it. - if (!insertLocation(recordingTrack, lastLocation, - lastRecordedLocation, lastRecordedLocationId, recordingTrackId)) { - return; - } - lastRecordedLocationId++; - isMoving = true; - } - - // If separation from last recorded point is too large insert a - // separator to indicate end of a segment: - boolean startNewSegment = - lastRecordedLocation != null - && lastRecordedLocation.getLatitude() < 90 - && distanceToLastRecorded > maxRecordingDistance - && recordingTrack.getStartId() >= 0; - if (startNewSegment) { - // Insert a separator point to indicate start of new track: - Log.d(TAG, "Inserting a separator."); - Location separator = new Location(LocationManager.GPS_PROVIDER); - separator.setLongitude(0); - separator.setLatitude(100); - separator.setTime(lastRecordedLocation.getTime()); - providerUtils.insertTrackPoint(separator, recordingTrackId); - } - - if (!insertLocation(recordingTrack, location, lastRecordedLocation, - lastRecordedLocationId, recordingTrackId)) { - return; - } - } else { - Log.d(TAG, String.format( - "Not recording. Distance to last recorded point (%f m) is less than" - + " %d m.", distanceToLastRecorded, minRecordingDistance)); - // Return here so that the location is NOT recorded as the last location. - return; - } - } catch (Error e) { - // Probably important enough to rethrow. - Log.e(TAG, "Error in onLocationChanged", e); - throw e; - } catch (RuntimeException e) { - // Safe usually to trap exceptions. - Log.e(TAG, - "Trapping exception in onLocationChanged", e); - throw e; - } - lastLocation = location; - } - private void addLocationToStats(Location location) { if (LocationUtils.isValidLocation(location)) { long now = System.currentTimeMillis(); @@ -636,227 +890,10 @@ public class TrackRecordingService extends Service implements LocationListener { } } - @Override - public void onProviderDisabled(String provider) { - // Do nothing - } - - @Override - public void onProviderEnabled(String provider) { - // Do nothing - } - - @Override - public void onStatusChanged(String provider, int status, Bundle extras) { - // Do nothing - } - /* * Application lifetime events: ============================ */ - @Override - public void onCreate() { - super.onCreate(); - Log.d(TAG, "TrackRecordingService.onCreate"); - providerUtils = MyTracksProviderUtils.Factory.get(this); - notificationManager = - (NotificationManager) getSystemService(NOTIFICATION_SERVICE); - locationManager = (LocationManager) getSystemService(LOCATION_SERVICE); - - setUpTaskExecutors(); - prefManager = new PreferenceManager(this); - registerLocationListener(); - - /* - * After 5 min, check every minute that location listener still is - * registered and spit out additional debugging info to the logs: - */ - timer.schedule(checkLocationListener, 1000 * 60 * 5, 1000 * 60); - - // Try to restore previous recording state in case this service has been - // restarted by the system, which can sometimes happen. - recordingTrack = getRecordingTrack(); - if (recordingTrack != null) { - restoreStats(recordingTrack); - isRecording = true; - } else { - if (recordingTrackId != -1) { - // Make sure we have consistent state in shared preferences. - Log.w(TAG, "TrackRecordingService.onCreate: " - + "Resetting an orphaned recording track = " + recordingTrackId); - } - prefManager.setRecordingTrack(recordingTrackId = -1); - } - showNotification(); - executorService = Executors.newSingleThreadExecutor(); - } - - /** - * Creates the periodic task executors. - */ - private void setUpTaskExecutors() { - announcementExecutor = new PeriodicTaskExecutor( - this, new StatusAnnouncerFactory(ApiFeatures.getInstance())); - splitExecutor = new PeriodicTaskExecutor(this, new SplitTask.Factory()); - } - - private void shutdownTaskExecutors() { - Log.d(TAG, "TrackRecordingService.shutdownExecuters"); - try { - announcementExecutor.shutdown(); - } finally { - announcementExecutor = null; - } - try { - splitExecutor.shutdown(); - } finally { - splitExecutor = null; - } - } - - @Override - public void onDestroy() { - Log.d(TAG, "TrackRecordingService.onDestroy"); - - isRecording = false; - showNotification(); - prefManager.shutdown(); - prefManager = null; - checkLocationListener.cancel(); - checkLocationListener = null; - timer.cancel(); - timer.purge(); - unregisterLocationListener(); - shutdownTaskExecutors(); - if (sensorManager != null) { - sensorManager.shutdown(); - sensorManager = null; - } - - // Make sure we have no indirect references to this service. - locationManager = null; - notificationManager = null; - providerUtils = null; - binder.detachFromService(); - binder = null; - - // This should be the last operation. - releaseWakeLock(); - - super.onDestroy(); - } - - @Override - public IBinder onBind(Intent intent) { - Log.d(TAG, "TrackRecordingService.onBind"); - return binder; - } - - @Override - public boolean onUnbind(Intent intent) { - Log.d(TAG, "TrackRecordingService.onUnbind"); - return super.onUnbind(intent); - } - - @Override - public boolean stopService(Intent name) { - Log.d(TAG, "TrackRecordingService.stopService"); - unregisterLocationListener(); - return super.stopService(name); - } - - @Override - public void onStart(Intent intent, int startId) { - handleStartCommand(intent, startId); - } - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - handleStartCommand(intent, startId); - return START_STICKY; - } - - private void handleStartCommand(Intent intent, int startId) { - Log.d(TAG, - "TrackRecordingService.handleStartCommand: " + startId); - - // Check if called on phone reboot with resume intent. - if (intent != null && - intent.getBooleanExtra(RESUME_TRACK_EXTRA_NAME, false)) { - Log.d(TAG, "TrackRecordingService: requested resume"); - - // Make sure that the current track exists and is fresh enough. - if (recordingTrack == null || !shouldResumeTrack(recordingTrack)) { - Log.i(TAG, - "TrackRecordingService: Not resuming, because the previous track (" - + recordingTrack + ") doesn't exist or is too old"); - isRecording = false; - prefManager.setRecordingTrack(recordingTrackId = -1); - stopSelfResult(startId); - return; - } - - Log.i(TAG, "TrackRecordingService: resuming"); - } - } - - private void setAutoResumeTrackRetries( - SharedPreferences sharedPreferences, int retryAttempts) { - Log.d(TAG, - "Updating auto-resume retry attempts to: " + retryAttempts); - prefManager.setAutoResumeTrackCurrentRetry(retryAttempts); - } - - private boolean shouldResumeTrack(Track track) { - Log.d(TAG, "shouldResumeTrack: autoResumeTrackTimeout = " - + autoResumeTrackTimeout); - - // Check if we haven't exceeded the maximum number of retry attempts. - SharedPreferences sharedPreferences = - getSharedPreferences(Constants.SETTINGS_NAME, 0); - int retries = sharedPreferences.getInt( - getString(R.string.auto_resume_track_current_retry_key), 0); - Log.d(TAG, - "shouldResumeTrack: Attempting to auto-resume the track (" - + (retries + 1) + "/" + MAX_AUTO_RESUME_TRACK_RETRY_ATTEMPTS + ")"); - if (retries >= MAX_AUTO_RESUME_TRACK_RETRY_ATTEMPTS) { - Log.i(TAG, - "shouldResumeTrack: Not resuming because exceeded the maximum " - + "number of auto-resume retries"); - return false; - } - - // Increase number of retry attempts. - setAutoResumeTrackRetries(sharedPreferences, retries + 1); - - // Check for special cases. - if (autoResumeTrackTimeout == 0) { - // Never resume. - Log.d(TAG, - "shouldResumeTrack: Auto-resume disabled (never resume)"); - return false; - } else if (autoResumeTrackTimeout == -1) { - // Always resume. - Log.d(TAG, - "shouldResumeTrack: Auto-resume forced (always resume)"); - return true; - } - - // Check if the last modified time is within the acceptable range. - long lastModified = - track.getStatistics() != null ? track.getStatistics().getStopTime() : 0; - Log.d(TAG, - "shouldResumeTrack: lastModified = " + lastModified - + ", autoResumeTrackTimeout: " + autoResumeTrackTimeout); - return lastModified > 0 && System.currentTimeMillis() - lastModified <= - autoResumeTrackTimeout * 60 * 1000; - } - - public boolean isRecording() { - return isRecording; - } - public long insertWaypoint(WaypointCreationRequest request) { if (!isRecording()) { throw new IllegalStateException( @@ -940,171 +977,6 @@ public class TrackRecordingService extends Service implements LocationListener { // Create a new stats keeper for the next marker. waypointStatsBuilder = new TripStatisticsBuilder(time); } - - private ServiceBinder binder = new ServiceBinder(this); - - /** - * TODO: There is a bug in Android that leaks Binder instances. This bug is - * especially visible if we have a non-static class, as there is no way to - * nullify reference to the outer class (the service). - * A workaround is to use a static class and explicitly clear service - * and detach it from the underlying Binder. With this approach, we minimize - * the leak to 24 bytes per each service instance. - * - * For more details, see the following bug: - * http://code.google.com/p/android/issues/detail?id=6426. - */ - private static class ServiceBinder extends ITrackRecordingService.Stub { - private TrackRecordingService service; - - public ServiceBinder(TrackRecordingService service) { - this.service = service; - } - - /** - * Clears the reference to the outer class to minimize the leak. - */ - public void detachFromService() { - this.service = null; - attachInterface(null, null); - } - - @Override - public boolean isRecording() { - checkService(); - return service.isRecording(); - } - - private void checkService() { - if (service == null) { - throw new IllegalStateException("The service has been already detached!"); - } - } - - @Override - public long getRecordingTrackId() { - checkService(); - return service.recordingTrackId; - } - - @Override - public boolean hasRecorded() { - checkService(); - return service.providerUtils.getLastTrackId() >= 0; - } - - @Override - public long startNewTrack() { - checkService(); - return service.startNewTrack(); - } - - /** - * Inserts a waypoint marker in the track being recorded. - * - * @param request Details of the waypoint to insert - * @return the unique ID of the inserted marker - */ - public long insertWaypoint(WaypointCreationRequest request) { - checkService(); - return service.insertWaypoint(request); - } - - @Override - public void endCurrentTrack() { - checkService(); - service.endCurrentTrack(); - } - - @Override - public void deleteAllTracks() { - checkService(); - if (isRecording()) { - throw new IllegalStateException("Cannot delete all tracks while recording!"); - } - service.providerUtils.deleteAllTracks(); - } - - @Override - public void recordLocation(Location loc) { - checkService(); - service.onLocationChanged(loc); - } - - @Override - public byte[] getSensorData() { - checkService(); - if (service.sensorManager == null) { - Log.d(TAG, "No sensor manager for data."); - return null; - } - if (service.sensorManager.getSensorDataSet() == null) { - Log.d(TAG, "Sensor data set is null."); - return null; - } - return service.sensorManager.getSensorDataSet().toByteArray(); - } - - @Override - public int getSensorState() { - checkService(); - if (service.sensorManager == null) { - Log.d(TAG, "No sensor manager for data."); - return Sensor.SensorState.NONE.getNumber(); - } - return service.sensorManager.getSensorState().getNumber(); - } - } - - public long startNewTrack() { - Log.d(TAG, "TrackRecordingService.startNewTrack"); - if (recordingTrackId != -1 || isRecording) { - throw new IllegalStateException("A track is already in progress!"); - } - - long startTime = System.currentTimeMillis(); - acquireWakeLock(); - - Track track = new Track(); - TripStatistics trackStats = track.getStatistics(); - trackStats.setStartTime(startTime); - track.setStartId(-1); - Uri trackUri = providerUtils.insertTrack(track); - recordingTrackId = Long.parseLong(trackUri.getLastPathSegment()); - track.setId(recordingTrackId); - track.setName(new DefaultTrackNameFactory(this).newTrackName( - recordingTrackId, startTime)); - isRecording = true; - isMoving = true; - - providerUtils.updateTrack(track); - statsBuilder = new TripStatisticsBuilder(startTime); - statsBuilder.setMinRecordingDistance(minRecordingDistance); - waypointStatsBuilder = new TripStatisticsBuilder(startTime); - waypointStatsBuilder.setMinRecordingDistance(minRecordingDistance); - currentWaypointId = insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS); - length = 0; - showNotification(); - registerLocationListener(); - sensorManager = SensorManagerFactory.getSensorManager(this); - if (sensorManager != null) { - sensorManager.onStartTrack(); - } - - // Reset the number of auto-resume retries. - setAutoResumeTrackRetries( - getSharedPreferences(Constants.SETTINGS_NAME, 0), 0); - // Persist the current recording track. - prefManager.setRecordingTrack(recordingTrackId); - - // Notify the world that we're now recording. - sendTrackBroadcast( - R.string.track_started_broadcast_action, recordingTrackId); - announcementExecutor.restore(); - splitExecutor.restore(); - - return recordingTrackId; - } private void endCurrentTrack() { Log.d(TAG, "TrackRecordingService.endCurrentTrack"); @@ -1135,17 +1007,19 @@ public class TrackRecordingService extends Service implements LocationListener { showNotification(); long recordedTrackId = recordingTrackId; prefManager.setRecordingTrack(recordingTrackId = -1); - + if (sensorManager != null) { sensorManager.shutdown(); sensorManager = null; } - + releaseWakeLock(); // Notify the world that we're no longer recording. sendTrackBroadcast( R.string.track_stopped_broadcast_action, recordedTrackId); + + stopSelf(); } private void sendTrackBroadcast(int actionResId, long trackId) { @@ -1157,6 +1031,22 @@ public class TrackRecordingService extends Service implements LocationListener { getString(R.string.broadcast_notifications_permission)); } + /* + * Data/state access. + */ + + private Track getRecordingTrack() { + if (recordingTrackId < 0) { + return null; + } + + return providerUtils.getTrack(recordingTrackId); + } + + public boolean isRecording() { + return isRecording; + } + public TripStatistics getTripStatistics() { return statsBuilder.getStatistics(); } @@ -1169,23 +1059,15 @@ public class TrackRecordingService extends Service implements LocationListener { return recordingTrackId; } - public void setRecordingTrackId(long recordingTrackId) { + void setRecordingTrackId(long recordingTrackId) { this.recordingTrackId = recordingTrackId; } - public int getMaxRecordingDistance() { - return maxRecordingDistance; - } - - public void setMaxRecordingDistance(int maxRecordingDistance) { + void setMaxRecordingDistance(int maxRecordingDistance) { this.maxRecordingDistance = maxRecordingDistance; } - public int getMinRecordingDistance() { - return minRecordingDistance; - } - - public void setMinRecordingDistance(int minRecordingDistance) { + void setMinRecordingDistance(int minRecordingDistance) { this.minRecordingDistance = minRecordingDistance; if (statsBuilder != null && waypointStatsBuilder != null) { statsBuilder.setMinRecordingDistance(minRecordingDistance); @@ -1193,41 +1075,163 @@ public class TrackRecordingService extends Service implements LocationListener { } } - public int getMinRequiredAccuracy() { - return minRequiredAccuracy; - } - - public void setMinRequiredAccuracy(int minRequiredAccuracy) { + void setMinRequiredAccuracy(int minRequiredAccuracy) { this.minRequiredAccuracy = minRequiredAccuracy; } - public LocationListenerPolicy getLocationListenerPolicy() { - return locationListenerPolicy; - } - - public void setLocationListenerPolicy( - LocationListenerPolicy locationListenerPolicy) { + void setLocationListenerPolicy(LocationListenerPolicy locationListenerPolicy) { this.locationListenerPolicy = locationListenerPolicy; } - - public int getAutoResumeTrackTimeout() { - return autoResumeTrackTimeout; - } - - public void setAutoResumeTrackTimeout(int autoResumeTrackTimeout) { + + void setAutoResumeTrackTimeout(int autoResumeTrackTimeout) { this.autoResumeTrackTimeout = autoResumeTrackTimeout; } - public void setAnnouncementFrequency(int announcementFrequency) { + void setAnnouncementFrequency(int announcementFrequency) { announcementExecutor.setTaskFrequency(announcementFrequency); } - public void setSplitFrequency(int frequency) { + void setSplitFrequency(int frequency) { splitExecutor.setTaskFrequency(frequency); } - public void setMetricUnits(boolean metric) { + void setMetricUnits(boolean metric) { announcementExecutor.setMetricUnits(metric); splitExecutor.setMetricUnits(metric); } + + + /** + * TODO: There is a bug in Android that leaks Binder instances. This bug is + * especially visible if we have a non-static class, as there is no way to + * nullify reference to the outer class (the service). + * A workaround is to use a static class and explicitly clear service + * and detach it from the underlying Binder. With this approach, we minimize + * the leak to 24 bytes per each service instance. + * + * For more details, see the following bug: + * http://code.google.com/p/android/issues/detail?id=6426. + */ + private static class ServiceBinder extends ITrackRecordingService.Stub { + private TrackRecordingService service; + private DeathRecipient deathRecipient; + + public ServiceBinder(TrackRecordingService service) { + this.service = service; + } + + // Logic for letting the actual service go up and down. + + @Override + public boolean isBinderAlive() { + // Pretend dead if the service went down. + return service != null; + } + + @Override + public boolean pingBinder() { + return isBinderAlive(); + } + + @Override + public void linkToDeath(DeathRecipient recipient, int flags) { + deathRecipient = recipient; + } + + @Override + public boolean unlinkToDeath(DeathRecipient recipient, int flags) { + if (!isBinderAlive()) { + return false; + } + + deathRecipient = null; + return true; + } + + /** + * Clears the reference to the outer class to minimize the leak. + */ + private void detachFromService() { + this.service = null; + attachInterface(null, null); + + if (deathRecipient != null) { + deathRecipient.binderDied(); + } + } + + private void checkService() { + if (service == null) { + throw new IllegalStateException("The service has been already detached!"); + } + } + + // Service method delegates. + + @Override + public boolean isRecording() { + checkService(); + return service.isRecording(); + } + + @Override + public long getRecordingTrackId() { + checkService(); + return service.recordingTrackId; + } + + @Override + public long startNewTrack() { + checkService(); + return service.startNewTrack(); + } + + /** + * Inserts a waypoint marker in the track being recorded. + * + * @param request Details of the waypoint to insert + * @return the unique ID of the inserted marker + */ + public long insertWaypoint(WaypointCreationRequest request) { + checkService(); + return service.insertWaypoint(request); + } + + @Override + public void endCurrentTrack() { + checkService(); + service.endCurrentTrack(); + } + + @Override + public void recordLocation(Location loc) { + checkService(); + service.locationListener.onLocationChanged(loc); + } + + @Override + public byte[] getSensorData() { + checkService(); + if (service.sensorManager == null) { + Log.d(TAG, "No sensor manager for data."); + return null; + } + if (service.sensorManager.getSensorDataSet() == null) { + Log.d(TAG, "Sensor data set is null."); + return null; + } + return service.sensorManager.getSensorDataSet().toByteArray(); + } + + @Override + public int getSensorState() { + checkService(); + if (service.sensorManager == null) { + Log.d(TAG, "No sensor manager for data."); + return Sensor.SensorState.NONE.getNumber(); + } + return service.sensorManager.getSensorState().getNumber(); + } + } + } diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/TrackRecordingServiceConnection.java b/MyTracks/src/com/google/android/apps/mytracks/services/TrackRecordingServiceConnection.java new file mode 100644 index 000000000..f646ab1cd --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/services/TrackRecordingServiceConnection.java @@ -0,0 +1,170 @@ +/* + * Copyright 2011 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.services; + +import static com.google.android.apps.mytracks.Constants.TAG; + +import com.google.android.apps.mytracks.util.SystemUtils; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.IBinder; +import android.os.IBinder.DeathRecipient; +import android.os.RemoteException; +import android.util.Log; + +/** + * Wrapper for the connection to the track recording service. + * This handles connection/disconnection internally, only returning a real + * service for use if one is available and connected. + * + * @author Rodrigo Damazio + */ +public class TrackRecordingServiceConnection { + private ITrackRecordingService boundService; + + private final DeathRecipient deathRecipient = new DeathRecipient() { + @Override + public void binderDied() { + Log.d(TAG, "Service died"); + setBoundService(null); + } + }; + + private final ServiceConnection serviceConnection = new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName className, IBinder service) { + Log.i(TAG, "Connected to service"); + try { + service.linkToDeath(deathRecipient, 0); + } catch (RemoteException e) { + Log.e(TAG, "Failed to bind a death recipient", e); + } + + setBoundService(ITrackRecordingService.Stub.asInterface(service)); + } + + @Override + public void onServiceDisconnected(ComponentName className) { + Log.i(TAG, "Disconnected from service"); + setBoundService(null); + } + }; + + private final Context context; + + private final Runnable bindChangedCallback; + + /** + * Constructor. + * + * @param context the current context + * @param bindChangedCallback a callback to be executed when the state of the + * service binding changes + */ + public TrackRecordingServiceConnection(Context context, Runnable bindChangedCallback) { + this.context = context; + this.bindChangedCallback = bindChangedCallback; + } + + /** + * Binds to the service, starting it if necessary. + */ + public void startAndBind() { + bindService(true); + } + + /** + * Binds to the service, only if it's already running. + */ + public void bindIfRunning() { + bindService(false); + } + + /** + * Unbinds from and stops the service. + */ + public void stop() { + unbind(); + + Log.d(TAG, "Stopping service"); + Intent intent = new Intent(context, TrackRecordingService.class); + context.stopService(intent); + } + + /** + * Unbinds from the service (but leaves it running). + */ + public void unbind() { + Log.d(TAG, "Unbinding from the service"); + try { + context.unbindService(serviceConnection); + } catch (IllegalArgumentException e) { + // Means we weren't bound, which is ok. + } + + setBoundService(null); + } + + /** + * Returns the service if connected to it, or null if not connected. + */ + public ITrackRecordingService getServiceIfBound() { + checkBindingAlive(); + + return boundService; + } + + private void checkBindingAlive() { + if (boundService != null && + !boundService.asBinder().isBinderAlive()) { + setBoundService(null); + } + } + + private void bindService(boolean startIfNeeded) { + if (boundService != null) { + // Already bound. + return; + } + + if (!startIfNeeded && !ServiceUtils.isServiceRunning(context)) { + // Not running, start not requested. + Log.d(TAG, "Service not running, not binding to it."); + return; + } + + if (startIfNeeded) { + Log.i(TAG, "Starting the service"); + Intent intent = new Intent(context, TrackRecordingService.class); + context.startService(intent); + } + + Log.i(TAG, "Binding to the service"); + Intent intent = new Intent(context, TrackRecordingService.class); + int flags = SystemUtils.isRelease(context) ? 0 : Context.BIND_DEBUG_UNBIND; + context.bindService(intent, serviceConnection, flags); + } + + private void setBoundService(ITrackRecordingService service) { + boundService = service; + if (bindChangedCallback != null) { + bindChangedCallback.run(); + } + } +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/sensors/SensorManagerFactory.java b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/SensorManagerFactory.java index 43e0a4be0..7c468be2c 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/sensors/SensorManagerFactory.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/SensorManagerFactory.java @@ -1,12 +1,12 @@ /* * Copyright 2009 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 @@ -45,7 +45,9 @@ public class SensorManagerFactory { if (prefs == null) { return null; } - + + context = context.getApplicationContext(); + String sensor = prefs.getString(context.getString(R.string.sensor_type_key), null); Log.i(Constants.TAG, "Creating sensor of type: " + sensor); diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/tasks/PeriodicTaskExecutor.java b/MyTracks/src/com/google/android/apps/mytracks/services/tasks/PeriodicTaskExecutor.java index f307e4741..74dfaf641 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/tasks/PeriodicTaskExecutor.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/tasks/PeriodicTaskExecutor.java @@ -1,12 +1,12 @@ /* * Copyright 2010 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 @@ -67,7 +67,7 @@ public class PeriodicTaskExecutor { if (!service.isRecording()) { return; } - + if (!isTimeFrequency()) { if (timerExecutor != null) { timerExecutor.shutdown(); @@ -77,7 +77,7 @@ public class PeriodicTaskExecutor { if (taskFrequency == 0) { return; } - + // Try to make the task. task = factory.create(service); // Returning null is ok. @@ -90,7 +90,7 @@ public class PeriodicTaskExecutor { if (timerExecutor == null) { timerExecutor = new TimerTaskExecutor(task, service); } - timerExecutor.scheduleTask(taskFrequency * 60000); + timerExecutor.scheduleTask(taskFrequency * 60000L); } else { // For distance based splits. calculateNextTaskDistance(); @@ -119,7 +119,7 @@ public class PeriodicTaskExecutor { if (!service.isRecording() || task == null) { return; } - + if (!isDistanceFrequency()) { nextTaskDistance = Double.MAX_VALUE; Log.d(TAG, "SplitManager: Distance splits disabled."); diff --git a/MyTracks/src/com/google/android/apps/mytracks/util/ApiPlatformAdapter.java b/MyTracks/src/com/google/android/apps/mytracks/util/ApiPlatformAdapter.java index c68832735..f432fe8fa 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/util/ApiPlatformAdapter.java +++ b/MyTracks/src/com/google/android/apps/mytracks/util/ApiPlatformAdapter.java @@ -19,7 +19,6 @@ import android.app.Notification; import android.app.NotificationManager; import android.app.Service; import android.content.SharedPreferences; -import android.os.HandlerThread; /** * A set of methods that may be implemented in a platform specific way. @@ -51,14 +50,6 @@ public interface ApiPlatformAdapter { void stopForeground(Service service, NotificationManager notificationManager, int id); - /** - * Terminates the given handler thread. - * - * @param handlerThread the thread to be terminated. - * @return true whether the handler has been stopped or not. - */ - boolean stopHandlerThread(HandlerThread handlerThread); - /** * Applies all changes done to the given preferences editor. * Changes may or may not be applied immediately. diff --git a/MyTracks/src/com/google/android/apps/mytracks/util/CupcakePlatformAdapter.java b/MyTracks/src/com/google/android/apps/mytracks/util/CupcakePlatformAdapter.java index abdd9dfb7..fe6ec0b47 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/util/CupcakePlatformAdapter.java +++ b/MyTracks/src/com/google/android/apps/mytracks/util/CupcakePlatformAdapter.java @@ -16,11 +16,11 @@ package com.google.android.apps.mytracks.util; import static com.google.android.apps.mytracks.Constants.TAG; + import android.app.Notification; import android.app.NotificationManager; import android.app.Service; import android.content.SharedPreferences.Editor; -import android.os.HandlerThread; import android.util.Log; import java.lang.reflect.InvocationTargetException; @@ -70,12 +70,6 @@ public class CupcakePlatformAdapter implements ApiPlatformAdapter { } } - @Override - public boolean stopHandlerThread(HandlerThread handlerThread) { - // Do nothing, as Cupcake doesn't provide quit(). - return false; - } - @Override public void applyPreferenceChanges(Editor editor) { editor.commit(); diff --git a/MyTracks/src/com/google/android/apps/mytracks/util/EclairPlatformAdapter.java b/MyTracks/src/com/google/android/apps/mytracks/util/EclairPlatformAdapter.java index 91c1ea1d3..55540e1a4 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/util/EclairPlatformAdapter.java +++ b/MyTracks/src/com/google/android/apps/mytracks/util/EclairPlatformAdapter.java @@ -19,7 +19,6 @@ import android.app.Notification; import android.app.NotificationManager; import android.app.Service; import android.content.SharedPreferences.Editor; -import android.os.HandlerThread; /** * The Eclair (API level 5) specific implementation of the @@ -42,11 +41,6 @@ public class EclairPlatformAdapter implements ApiPlatformAdapter { service.stopForeground(id != -1); } - @Override - public boolean stopHandlerThread(HandlerThread handlerThread) { - return handlerThread.quit(); - } - @Override public void applyPreferenceChanges(Editor editor) { editor.commit(); diff --git a/MyTracks/src/com/google/android/apps/mytracks/util/ResourceUtils.java b/MyTracks/src/com/google/android/apps/mytracks/util/ResourceUtils.java index 884f4b95a..1d9603919 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/util/ResourceUtils.java +++ b/MyTracks/src/com/google/android/apps/mytracks/util/ResourceUtils.java @@ -1,12 +1,12 @@ /* * 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 @@ -57,10 +57,11 @@ public class ResourceUtils { public static void readBinaryFileToOutputStream( Context activity, int id, OutputStream os) { BufferedInputStream in = null; + BufferedOutputStream out = null; try { in = new BufferedInputStream( activity.getResources().openRawResource(id)); - BufferedOutputStream out = new BufferedOutputStream(os); + out = new BufferedOutputStream(os); int b; while ((b = in.read()) != -1) { out.write(b); @@ -76,6 +77,14 @@ public class ResourceUtils { // Ignore } } + + if (out != null) { + try { + out.close(); + } catch (IOException e) { + // Ignore + } + } } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/util/UriUtils.java b/MyTracks/src/com/google/android/apps/mytracks/util/UriUtils.java new file mode 100644 index 000000000..bc49f323a --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/util/UriUtils.java @@ -0,0 +1,60 @@ +/* + * Copyright 2011 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.util; + +import android.net.Uri; + +import java.util.List; + +/** + * Utilities for dealing with content and other types of URIs. + * + * @author Rodrigo Damazio + */ +public class UriUtils { + + public static boolean matchesContentUri(Uri uri, Uri baseContentUri) { + if (uri == null) { + return false; + } + + // Check that scheme and authority are the same. + if (!uri.getScheme().equals(baseContentUri.getScheme()) || + !uri.getAuthority().equals(baseContentUri.getAuthority())) { + return false; + } + + // Checks that all the base path components are in the URI. + List uriPathSegments = uri.getPathSegments(); + List basePathSegments = baseContentUri.getPathSegments(); + if (basePathSegments.size() > uriPathSegments.size()) { + return false; + } + for (int i = 0; i < basePathSegments.size(); i++) { + if (!uriPathSegments.get(i).equals(basePathSegments.get(i))) { + return false; + } + } + + return true; + } + + public static boolean isFileUri(Uri uri) { + return "file".equals(uri.getScheme()); + } + + private UriUtils() {} +} diff --git a/MyTracksLib/src/com/google/android/apps/mytracks/services/ITrackRecordingService.aidl b/MyTracksLib/src/com/google/android/apps/mytracks/services/ITrackRecordingService.aidl index b4ffcec0e..f6b946a4c 100644 --- a/MyTracksLib/src/com/google/android/apps/mytracks/services/ITrackRecordingService.aidl +++ b/MyTracksLib/src/com/google/android/apps/mytracks/services/ITrackRecordingService.aidl @@ -1,12 +1,12 @@ /* * 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 @@ -66,17 +66,6 @@ interface ITrackRecordingService { */ void endCurrentTrack(); - /** - * Returns whether at least one track has been or is being recorded (and was - * not deleted). - */ - boolean hasRecorded(); - - /** - * Deletes all the stored tracks. - */ - void deleteAllTracks(); - /** * The current sensor data. * The data is returned as a byte array which is a binary version of a diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/DialogManagerTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/DialogManagerTest.java deleted file mode 100644 index 1b23a0315..000000000 --- a/MyTracksTest/src/com/google/android/apps/mytracks/DialogManagerTest.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright 2010 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.apps.mytracks.io.sendtogoogle.SendDialog; - -import android.app.Dialog; -import android.app.ProgressDialog; -import android.test.ActivityInstrumentationTestCase2; - -/** - * @author Sandor Dornbush - */ -public class DialogManagerTest - extends ActivityInstrumentationTestCase2 { - - public DialogManagerTest() { - super(MyTracks.class); - } - - @Override - protected void setUp() throws Exception { - super.setUp(); - MyTracks.clearInstance(); - assertNull(MyTracks.getInstance()); - } - - public void test_onCreateChartSettings() { - Dialog d = - getActivity().onCreateDialog(DialogManager.DIALOG_CHART_SETTINGS, null); - assertNotNull(d); - assertTrue(d instanceof ChartSettingsDialog); - } - - public void test_onCreateImportProgress() { - Dialog d = - getActivity().onCreateDialog( - DialogManager.DIALOG_IMPORT_PROGRESS, null); - assertNotNull(d); - assertTrue(d instanceof ProgressDialog); - } - - public void test_onCreateProgress() { - Dialog d = - getActivity().onCreateDialog(DialogManager.DIALOG_PROGRESS, null); - assertNotNull(d); - assertTrue(d instanceof ProgressDialog); - ProgressDialog pd = (ProgressDialog) d; - assertEquals(100, pd.getMax()); - assertEquals(10, pd.getProgress()); - } - - public void test_onCreateSendToGoogle() { - Dialog d = - getActivity().onCreateDialog(DialogManager.DIALOG_SEND_TO_GOOGLE, null); - assertNotNull(d); - assertTrue(d instanceof SendDialog); - } -} diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/MyTracksTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/MyTracksTest.java index 7c5151944..a5581edc7 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/MyTracksTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/MyTracksTest.java @@ -1,12 +1,12 @@ /* * Copyright 2010 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 @@ -15,6 +15,8 @@ */ package com.google.android.apps.mytracks; +import com.google.android.apps.mytracks.services.ServiceUtils; +import com.google.android.apps.mytracks.services.TrackRecordingServiceConnection; import com.google.android.maps.mytracks.R; import android.app.Activity; @@ -32,60 +34,53 @@ import java.util.concurrent.atomic.AtomicBoolean; /** * A unit test for {@link MyTracks} activity. - * + * * @author Bartlomiej Niechwiej */ public class MyTracksTest extends ActivityInstrumentationTestCase2{ + private SharedPreferences sharedPreferences; + private TrackRecordingServiceConnection serviceConnection; + public MyTracksTest() { super(MyTracks.class); } - @Override - protected void setUp() throws Exception { - super.setUp(); - MyTracks.clearInstance(); - assertNull(MyTracks.getInstance()); - } - @Override protected void tearDown() throws Exception { clearSelectedAndRecordingTracks(); waitForIdle(); + super.tearDown(); } - + public void testInitialization_mainAction() { // Make sure we can start MyTracks and the activity doesn't start recording. - assertNotNull(getActivity()); - assertNotNull(MyTracks.getInstance()); - assertNotNull(getActivity().getSharedPreferences()); - + assertInitialized(); + // Check if not recording. - assertFalse(getActivity().isRecording()); - assertEquals(-1, getActivity().getRecordingTrackId()); - long selectedTrackId = getActivity().getSharedPreferences().getLong( + assertFalse(isRecording()); + assertEquals(-1, getRecordingTrackId()); + long selectedTrackId = getSharedPreferences().getLong( getActivity().getString(R.string.selected_track_key), -1); assertEquals(selectedTrackId, getActivity().getSelectedTrackId()); } - + public void testInitialization_viewActionWithNoData() { // Simulate start with ACTION_VIEW intent. Intent startIntent = new Intent(); startIntent.setAction(Intent.ACTION_VIEW); setActivityIntent(startIntent); - - assertNotNull(getActivity()); - assertNotNull(MyTracks.getInstance()); - assertNotNull(getActivity().getSharedPreferences()); - + + assertInitialized(); + // Check if not recording. - assertFalse(getActivity().isRecording()); - assertEquals(-1, getActivity().getRecordingTrackId()); - long selectedTrackId = getActivity().getSharedPreferences().getLong( + assertFalse(isRecording()); + assertEquals(-1, getRecordingTrackId()); + long selectedTrackId = getSharedPreferences().getLong( getActivity().getString(R.string.selected_track_key), -1); assertEquals(selectedTrackId, getActivity().getSelectedTrackId()); } - + public void testInitialization_viewActionWithValidData() throws Exception { // Simulate start with ACTION_VIEW intent. Intent startIntent = new Intent(); @@ -93,24 +88,22 @@ public class MyTracksTest extends ActivityInstrumentationTestCase2{ Uri uri = Uri.fromFile(File.createTempFile("valid", ".gpx")); // TODO: Add a valid GPX. - + startIntent.setData(uri); setActivityIntent(startIntent); - - assertNotNull(getActivity()); - assertNotNull(MyTracks.getInstance()); - assertNotNull(getActivity().getSharedPreferences()); - + + assertInitialized(); + // Check if not recording. - assertFalse(getActivity().isRecording()); - assertEquals(-1, getActivity().getRecordingTrackId()); - long selectedTrackId = getActivity().getSharedPreferences().getLong( + assertFalse(isRecording()); + assertEquals(-1, getRecordingTrackId()); + long selectedTrackId = getSharedPreferences().getLong( getActivity().getString(R.string.selected_track_key), -1); assertEquals(selectedTrackId, getActivity().getSelectedTrackId()); - - // TODO: Finish this test. + + // TODO: Finish this test. } - + public void testInitialization_viewActionWithInvalidData() throws Exception { // Simulate start with ACTION_VIEW intent. Intent startIntent = new Intent(); @@ -118,66 +111,62 @@ public class MyTracksTest extends ActivityInstrumentationTestCase2{ Uri uri = Uri.fromFile(File.createTempFile("invalid", ".gpx")); startIntent.setData(uri); setActivityIntent(startIntent); - - assertNotNull(getActivity()); - assertNotNull(MyTracks.getInstance()); - assertNotNull(getActivity().getSharedPreferences()); - + + assertInitialized(); + // Check if not recording. - assertFalse(getActivity().isRecording()); - assertEquals(-1, getActivity().getRecordingTrackId()); - long selectedTrackId = getActivity().getSharedPreferences().getLong( + assertFalse(isRecording()); + assertEquals(-1, getRecordingTrackId()); + long selectedTrackId = getSharedPreferences().getLong( getActivity().getString(R.string.selected_track_key), -1); assertEquals(selectedTrackId, getActivity().getSelectedTrackId()); - + // TODO: Finish this test. } - + public void testRecording_startAndStop() throws Exception { - // Make sure we can start MyTracks and the activity doesn't start recording. - assertNotNull(getActivity()); - assertNotNull(MyTracks.getInstance()); - assertNotNull(getActivity().getSharedPreferences()); + assertInitialized(); // Check if not recording. - clearSelectedAndRecordingTracks(); + clearSelectedAndRecordingTracks(); waitForIdle(); - assertFalse(getActivity().isRecording()); - assertEquals(-1, getActivity().getRecordingTrackId()); - long selectedTrackId = getActivity().getSharedPreferences().getLong( + assertFalse(isRecording()); + assertEquals(-1, getRecordingTrackId()); + long selectedTrackId = getSharedPreferences().getLong( getActivity().getString(R.string.selected_track_key), -1); assertEquals(selectedTrackId, getActivity().getSelectedTrackId()); // Start a new track. getActivity().startRecording(); + serviceConnection.bindIfRunning(); long recordingTrackId = awaitRecordingStatus(5000, true); assertTrue(recordingTrackId >= 0); - + // Wait until we are done and make sure that selectedTrack = recordingTrack. waitForIdle(); - assertEquals(recordingTrackId, getActivity().getSharedPreferences().getLong( + assertEquals(recordingTrackId, getSharedPreferences().getLong( getActivity().getString(R.string.recording_track_key), -1)); - selectedTrackId = getActivity().getSharedPreferences().getLong( + selectedTrackId = getSharedPreferences().getLong( getActivity().getString(R.string.selected_track_key), -1); assertEquals(recordingTrackId, selectedTrackId); assertEquals(selectedTrackId, getActivity().getSelectedTrackId()); - - // Watch for MyTracksDetails activity. + + // Watch for MyTracksDetails activity. ActivityMonitor monitor = getInstrumentation().addMonitor( TrackDetails.class.getName(), null, false); // Now, stop the track and make sure that it is still selected, but // no longer recording. getActivity().stopRecording(); - - // Check if we got back MyTracksDetails activity. + + // Check if we got back MyTracksDetails activity. Activity activity = getInstrumentation().waitForMonitor(monitor); assertTrue(activity instanceof TrackDetails); - + // TODO: Update track name and other properties and test if they were // properly saved. - + // Simulate a click on Save button. Button save = (Button) activity.findViewById(R.id.trackdetails_save); save.performClick(); @@ -185,18 +174,24 @@ public class MyTracksTest extends ActivityInstrumentationTestCase2{ // Check the remaining properties. recordingTrackId = awaitRecordingStatus(5000, false); assertEquals(-1, recordingTrackId); - assertEquals(recordingTrackId, getActivity().getRecordingTrackId()); - assertEquals(recordingTrackId, getActivity().getSharedPreferences().getLong( + assertEquals(recordingTrackId, getRecordingTrackId()); + assertEquals(recordingTrackId, getSharedPreferences().getLong( getActivity().getString(R.string.recording_track_key), -1)); // Make sure this is the same track as the last recording track ID. assertEquals(selectedTrackId, getActivity().getSelectedTrackId()); } - + + private void assertInitialized() { + assertNotNull(getActivity()); + + serviceConnection = new TrackRecordingServiceConnection(getActivity(), null); + } + /** * Waits until the UI thread becomes idle. */ private void waitForIdle() throws InterruptedException { - // Note: We can't use getInstrumentation().waitForIdleSync() here. + // Note: We can't use getInstrumentation().waitForIdleSync() here. final Object semaphore = new Object(); synchronized (semaphore) { final AtomicBoolean isIdle = new AtomicBoolean(); @@ -216,20 +211,20 @@ public class MyTracksTest extends ActivityInstrumentationTestCase2{ } /** - * Clears {selected,recording}TrackId in the {@link SharedPreferences}. + * Clears {selected,recording}TrackId in the {@link getSharedPreferences()}. */ private void clearSelectedAndRecordingTracks() { - Editor editor = getActivity().getSharedPreferences().edit(); + Editor editor = getSharedPreferences().edit(); editor.putLong(getActivity().getString(R.string.selected_track_key), -1); editor.putLong(getActivity().getString(R.string.recording_track_key), -1); - + editor.clear(); editor.commit(); } - + /** * Waits until the recording state changes to the given status. - * + * * @param timeout the maximum time to wait, in milliseconds. * @param isRecording the final status to await. * @return the recording track ID. @@ -237,14 +232,30 @@ public class MyTracksTest extends ActivityInstrumentationTestCase2{ private long awaitRecordingStatus(long timeout, boolean isRecording) throws TimeoutException, InterruptedException { long startTime = System.nanoTime(); - while (getActivity().isRecording() != isRecording) { + while (isRecording() != isRecording) { if (System.nanoTime() - startTime > timeout * 1000000) { throw new TimeoutException("Timeout while waiting for recording!"); } Thread.sleep(20); } waitForIdle(); - assertEquals(isRecording, getActivity().isRecording()); - return getActivity().getRecordingTrackId(); + assertEquals(isRecording, isRecording()); + return getRecordingTrackId(); + } + + private long getRecordingTrackId() { + return getSharedPreferences().getLong(getActivity().getString(R.string.recording_track_key), -1); + } + + private SharedPreferences getSharedPreferences() { + if (sharedPreferences == null) { + sharedPreferences = getActivity().getSharedPreferences(Constants.SETTINGS_NAME, 0); + } + return sharedPreferences; + } + + private boolean isRecording() { + return ServiceUtils.isRecording(getActivity(), + serviceConnection.getServiceIfBound(), getSharedPreferences()); } } diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/content/TrackDataHubTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/content/TrackDataHubTest.java index 9f69c79f5..ce77bb917 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/content/TrackDataHubTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/content/TrackDataHubTest.java @@ -91,7 +91,12 @@ public class TrackDataHubTest extends AndroidTestCase { dataSources = AndroidMock.createNiceMock("dataSources", DataSourcesWrapper.class); listeners = new TrackDataListeners(); - hub = new TrackDataHub(context, dataSources, listeners, prefs, providerUtils, TARGET_POINTS) { + hub = new TrackDataHub(context, listeners, prefs, providerUtils, TARGET_POINTS) { + @Override + protected DataSourcesWrapper newDataSources() { + return dataSources; + } + @Override protected void runInListenerThread(Runnable runnable) { // Run everything in the same thread. @@ -124,7 +129,7 @@ public class TrackDataHubTest extends AndroidTestCase { AndroidMock.replay(dataSources); hub.stop(); - hub.destroy(); + hub = null; super.tearDown(); } diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/WriteProgressControllerTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/io/file/WriteProgressControllerTest.java similarity index 92% rename from MyTracksTest/src/com/google/android/apps/mytracks/WriteProgressControllerTest.java rename to MyTracksTest/src/com/google/android/apps/mytracks/io/file/WriteProgressControllerTest.java index 2f4349b32..19d251281 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/WriteProgressControllerTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/io/file/WriteProgressControllerTest.java @@ -13,10 +13,11 @@ * License for the specific language governing permissions and limitations under * the License. */ -package com.google.android.apps.mytracks; +package com.google.android.apps.mytracks.io.file; -import com.google.android.apps.mytracks.io.file.MockTrackWriter; +import com.google.android.apps.mytracks.MyTracks; import com.google.android.apps.mytracks.io.file.TrackWriter; +import com.google.android.apps.mytracks.io.file.WriteProgressController; import android.app.ProgressDialog; import android.test.ActivityInstrumentationTestCase2; @@ -60,7 +61,7 @@ public class WriteProgressControllerTest extends ActivityInstrumentationTestCase getActivity(), mockWriter); controller.setOnCompletionListener(new WriteProgressController.OnCompletionListener() { @Override - public void onComplete(TrackWriter writer) { + public void onComplete() { controllerDoneRef.set(true); } }); diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/services/TrackRecordingServiceTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/services/TrackRecordingServiceTest.java index 0f4c13ca8..64444be9f 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/services/TrackRecordingServiceTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/services/TrackRecordingServiceTest.java @@ -49,9 +49,9 @@ import java.util.List; /** * Tests for the MyTracks track recording service. - * + * * @author Bartlomiej Niechwiej - * + * * TODO: The original class, ServiceTestCase, has a few limitations, e.g. * it's not possible to properly shutdown the service, unless tearDown() * is called, which prevents from testing multiple scenarios in a single @@ -63,7 +63,7 @@ public class TrackRecordingServiceTest private Context context; private MyTracksProviderUtils providerUtils; private SharedPreferences sharedPreferences; - + /* * In order to support starting and binding to the service in the same * unit test, we provide a workaround, as the original class doesn't allow @@ -72,19 +72,19 @@ public class TrackRecordingServiceTest private boolean bound; private Intent serviceIntent; - + public TrackRecordingServiceTest() { super(TrackRecordingService.class); } /** * A context wrapper with the user provided {@link ContentResolver}. - * + * * TODO: Move to test utils package. */ public static class MockContext extends ContextWrapper { private final ContentResolver contentResolver; - + public MockContext(ContentResolver contentResolver, Context base) { super(base); this.contentResolver = contentResolver; @@ -108,7 +108,7 @@ public class TrackRecordingServiceTest return 4; } } - + @Override protected IBinder bindService(Intent intent) { if (getService() != null) { @@ -190,7 +190,7 @@ public class TrackRecordingServiceTest startService(startIntent); assertNotNull(getService()); - // We expect to resume the previous track. + // We expect to resume the previous track. assertTrue(getService().isRecording()); ITrackRecordingService service = bindAndGetService(createStartIntent()); assertEquals(123, service.getRecordingTrackId()); @@ -293,7 +293,7 @@ public class TrackRecordingServiceTest assertTrue(tracks.isEmpty()); ITrackRecordingService service = bindAndGetService(createStartIntent()); - // Test if we start in no-recording mode by default. + // Test if we start in no-recording mode by default. assertFalse(service.isRecording()); assertEquals(-1, service.getRecordingTrackId()); } @@ -463,92 +463,19 @@ public class TrackRecordingServiceTest context.getString(R.string.recording_track_key), 0)); assertEquals(-1, service.getRecordingTrackId()); } - + @MediumTest public void testIntegration_completeRecordingSession() throws Exception { List tracks = providerUtils.getAllTracks(); assertTrue(tracks.isEmpty()); fullRecordingSession(); } - - @MediumTest - public void testDeleteAllTracks_noRecording() throws Exception { - createDummyTrack(123, -1, false); - assertEquals(1, providerUtils.getAllTracks().size()); - - ITrackRecordingService service = bindAndGetService(createStartIntent()); - assertFalse(service.isRecording()); - // Deleting all tracks should succeed. - service.deleteAllTracks(); - assertFalse(service.isRecording()); - assertTrue(providerUtils.getAllTracks().isEmpty()); - } - - @MediumTest - public void testDeleteAllTracks_noTracks() throws Exception { - assertTrue(providerUtils.getAllTracks().isEmpty()); - - ITrackRecordingService service = bindAndGetService(createStartIntent()); - assertFalse(service.isRecording()); - - // Deleting all tracks should succeed. - service.deleteAllTracks(); - assertFalse(service.isRecording()); - assertTrue(providerUtils.getAllTracks().isEmpty()); - } - - @MediumTest - public void testDeleteAllTracks_trackInProgress() throws Exception { - createDummyTrack(123, -1, true); - assertEquals(1, providerUtils.getAllTracks().size()); - - ITrackRecordingService service = bindAndGetService(createStartIntent()); - assertTrue(service.isRecording()); - - // Since we have a track in progress, we expect to fail. - try { - service.deleteAllTracks(); - fail("Expecting IllegalStateException"); - } catch (IllegalStateException e) { - // Expected. - } - assertTrue(service.isRecording()); - assertEquals(1, providerUtils.getAllTracks().size()); - } - - @MediumTest - public void testHasRecorded_noTracks() throws Exception { - ITrackRecordingService service = bindAndGetService(createStartIntent()); - assertFalse(service.isRecording()); - assertFalse(service.hasRecorded()); - } - - @MediumTest - public void testHasRecorded_trackInProgress() throws Exception { - createDummyTrack(123, -1, true); - assertEquals(1, providerUtils.getAllTracks().size()); - - ITrackRecordingService service = bindAndGetService(createStartIntent()); - assertTrue(service.isRecording()); - assertTrue(service.hasRecorded()); - } - - @MediumTest - public void testHasRecorded_oldTracks() throws Exception { - createDummyTrack(123, -1, false); - assertEquals(1, providerUtils.getAllTracks().size()); - - ITrackRecordingService service = bindAndGetService(createStartIntent()); - assertFalse(service.isRecording()); - assertTrue(service.hasRecorded()); - } - @MediumTest public void testInsertStatisticsMarker_noRecordingTrack() throws Exception { ITrackRecordingService service = bindAndGetService(createStartIntent()); assertFalse(service.isRecording()); - + try { service.insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS); fail("Expecting IllegalStateException"); @@ -556,17 +483,17 @@ public class TrackRecordingServiceTest // Expected. } } - + @MediumTest public void testInsertStatisticsMarker_validLocation() throws Exception { createDummyTrack(123, -1, true); - + ITrackRecordingService service = bindAndGetService(createStartIntent()); assertTrue(service.isRecording()); - + assertEquals(1, service.insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS)); assertEquals(2, service.insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS)); - + Waypoint wpt = providerUtils.getWaypoint(1); assertEquals(getContext().getString(R.string.stats_icon_url), wpt.getIcon()); @@ -586,7 +513,7 @@ public class TrackRecordingServiceTest public void testInsertWaypointMarker_noRecordingTrack() throws Exception { ITrackRecordingService service = bindAndGetService(createStartIntent()); assertFalse(service.isRecording()); - + try { service.insertWaypoint(WaypointCreationRequest.DEFAULT_MARKER); fail("Expecting IllegalStateException"); @@ -594,14 +521,14 @@ public class TrackRecordingServiceTest // Expected. } } - + @MediumTest public void testInsertWaypointMarker_validWaypoint() throws Exception { createDummyTrack(123, -1, true); - + ITrackRecordingService service = bindAndGetService(createStartIntent()); assertTrue(service.isRecording()); - + assertEquals(1, service.insertWaypoint(WaypointCreationRequest.DEFAULT_MARKER)); Waypoint wpt = providerUtils.getWaypoint(1); assertEquals(getContext().getString(R.string.waypoint_icon_url), @@ -614,7 +541,7 @@ public class TrackRecordingServiceTest assertNotNull(wpt.getLocation()); assertNull(wpt.getStatistics()); } - + @MediumTest public void testWithProperties_noAnnouncementFreq() throws Exception { functionalTest(R.string.announcement_frequency_key, (Object) null); @@ -624,7 +551,7 @@ public class TrackRecordingServiceTest public void testWithProperties_defaultAnnouncementFreq() throws Exception { functionalTest(R.string.announcement_frequency_key, 1); } - + @MediumTest public void testWithProperties_noMaxRecordingDist() throws Exception { functionalTest(R.string.max_recording_distance_key, (Object) null); @@ -654,7 +581,7 @@ public class TrackRecordingServiceTest public void testWithProperties_defaultSplitFreqByDist() throws Exception { functionalTest(R.string.split_frequency_key, 5); } - + @MediumTest public void testWithProperties_defaultSplitFreqByTime() throws Exception { functionalTest(R.string.split_frequency_key, -2); @@ -674,7 +601,7 @@ public class TrackRecordingServiceTest public void testWithProperties_metricUnitsDisabled() throws Exception { functionalTest(R.string.metric_units_key, false); } - + @MediumTest public void testWithProperties_noMinRecordingInterval() throws Exception { functionalTest(R.string.min_recording_interval_key, (Object) null); @@ -690,23 +617,23 @@ public class TrackRecordingServiceTest public void testWithProperties_noMinRequiredAccuracy() throws Exception { functionalTest(R.string.min_required_accuracy_key, (Object) null); } - + @MediumTest public void testWithProperties_defaultMinRequiredAccuracy() throws Exception { functionalTest(R.string.min_required_accuracy_key, 500); } - + @MediumTest public void testWithProperties_noSensorType() throws Exception { functionalTest(R.string.sensor_type_key, (Object) null); } - + @MediumTest public void testWithProperties_zephyrSensorType() throws Exception { functionalTest(R.string.sensor_type_key, - context.getString(R.string.zephyr_sensor_type)); + context.getString(R.string.zephyr_sensor_type)); } - + private ITrackRecordingService bindAndGetService(Intent intent) { ITrackRecordingService service = ITrackRecordingService.Stub.asInterface( bindService(intent)); @@ -724,7 +651,7 @@ public class TrackRecordingServiceTest addTrack(dummyTrack, isRecording); return dummyTrack; } - + private void updateAutoResumePrefs(int attempts, int timeoutMins) { Editor editor = sharedPreferences.edit(); editor.putInt(context.getString( @@ -733,20 +660,20 @@ public class TrackRecordingServiceTest R.string.auto_resume_track_timeout_key), timeoutMins); editor.commit(); } - + private Intent createStartIntent() { Intent startIntent = new Intent(); startIntent.setClass(context, TrackRecordingService.class); return startIntent; } - + private void addTrack(Track track, boolean isRecording) { assertTrue(track.getId() >= 0); providerUtils.insertTrack(track); assertEquals(track.getId(), providerUtils.getTrack(track.getId()).getId()); setRecordingTrack(isRecording ? track.getId() : -1); } - + private void setRecordingTrack(long id) { Editor editor = sharedPreferences.edit(); editor.putLong(context.getString(R.string.recording_track_key), id); @@ -760,11 +687,11 @@ public class TrackRecordingServiceTest throws Exception { final String key = context.getString(resourceId); for (Object value : values) { - // Remove all properties and set the property for the given key. + // Remove all properties and set the property for the given key. Editor editor = sharedPreferences.edit(); editor.clear(); if (value instanceof String) { - editor.putString(key, (String) value); + editor.putString(key, (String) value); } else if (value instanceof Long) { editor.putLong(key, (Long) value); } else if (value instanceof Integer) { @@ -775,15 +702,15 @@ public class TrackRecordingServiceTest // Do nothing, as clear above has already removed this property. } editor.commit(); - + fullRecordingSession(); } } - + private void fullRecordingSession() throws Exception { ITrackRecordingService service = bindAndGetService(createStartIntent()); assertFalse(service.isRecording()); - + // Start a track. long id = service.startNewTrack(); assertTrue(id >= 0); @@ -806,14 +733,14 @@ public class TrackRecordingServiceTest loc.setTime(startTime + i * 10000); loc.setBearing(3.0f); service.recordLocation(loc); - + if (i % 10 == 0) { service.insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS); } else if (i % 7 == 0) { service.insertWaypoint(WaypointCreationRequest.DEFAULT_MARKER); } } - + // Stop the track. Validate if it has correct data. service.endCurrentTrack(); assertFalse(service.isRecording());