Merge with head.

This commit is contained in:
Sandor Dornbush
2011-07-05 16:33:45 -07:00
parent b588bb02f4
commit 29df8762e2
83 changed files with 3928 additions and 2855 deletions
+104 -39
View File
@@ -44,6 +44,109 @@ limitations under the License.
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data android:mimeType="vnd.android.cursor.item/vnd.google.track" />
</intent-filter>
</activity>
<activity android:name="com.google.android.apps.mytracks.MapActivity"
android:label="MyTracks"
android:exported="false">
<intent-filter>
<category android:name="android.intent.category.TAB" />
</intent-filter>
</activity>
<activity android:name="com.google.android.apps.mytracks.MyMapsList"
android:label="@string/map_list_title"
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|orientation|fontScale"
android:exported="false" />
<!-- TODO: VIEW and PICK for track cursor dir -->
<activity android:name="com.google.android.apps.mytracks.TrackList"
android:label="Recorded Tracks" />
<!-- TODO: VIEW and PICK for waypoint cursor dir -->
<activity android:name="com.google.android.apps.mytracks.WaypointsList"
android:label="Recorded Waypoints" />
<activity android:name="com.google.android.apps.mytracks.TrackDetails"
android:label="@string/trackdetails_title">
<intent-filter>
<action android:name="android.intent.action.EDIT" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:mimeType="vnd.android.cursor.item/vnd.google.track" />
</intent-filter>
</activity>
<activity android:name="com.google.android.apps.mytracks.WaypointDetails"
android:label="@string/waypointdetails_title">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<action android:name="android.intent.action.EDIT" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:mimeType="vnd.android.cursor.item/vnd.google.waypoint" />
</intent-filter>
</activity>
<activity android:name="com.google.android.apps.mytracks.SettingsActivity"
android:label="@string/mytracks_settings" >
<intent-filter>
<category android:name="android.intent.category.PREFERENCE" />
</intent-filter>
</activity>
<activity android:name="com.google.android.apps.mytracks.AggregatedStatsActivity"
android:label="@string/aggregated_stats" />
<activity android:name="com.google.android.apps.mytracks.StatsActivity"
android:label="@string/statistics"
android:exported="false">
<intent-filter>
<category android:name="android.intent.category.TAB" />
</intent-filter>
</activity>
<activity android:name="com.google.android.apps.mytracks.SensorStateActivity"
android:label="@string/sensor_state" />
<activity android:name="com.google.android.apps.mytracks.ChartActivity"
android:label="@string/elevation_label"
android:exported="false">
<intent-filter>
<category android:name="android.intent.category.TAB" />
</intent-filter>
</activity>
<activity android:name="com.google.android.apps.mytracks.io.sendtogoogle.SendActivity"
android:label="@string/sendtogoogle_title"
android:theme="@android:style/Theme.Translucent.NoTitleBar">
<intent-filter>
<action android:name="android.intent.action.SEND" />
<data android:mimeType="vnd.android.cursor.item/vnd.google.track" />
</intent-filter>
</activity>
<activity android:name="com.google.android.apps.mytracks.io.file.SaveActivity"
android:label="@string/tracklist_write_to_sd"
android:theme="@android:style/Theme.Translucent.NoTitleBar"
android:noHistory="true">
<intent-filter>
<action android:name="@string/save_intent_action" />
<data android:mimeType="vnd.android.cursor.item/vnd.google.track" />
</intent-filter>
</activity>
<activity android:name="com.google.android.apps.mytracks.io.file.ImportActivity"
android:label="@string/import_title"
android:theme="@android:style/Theme.Translucent.NoTitleBar"
android:noHistory="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<action android:name="android.intent.action.ATTACH_DATA" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="file" /> <data android:scheme="file" />
<data android:mimeType="application/gpx+xml" /> <data android:mimeType="application/gpx+xml" />
<data android:mimeType="application/gpx" /> <data android:mimeType="application/gpx" />
@@ -52,44 +155,6 @@ limitations under the License.
</intent-filter> </intent-filter>
</activity> </activity>
<activity android:name="com.google.android.apps.mytracks.MapActivity"
android:label="MyTracks"
android:exported="false" />
<activity android:name="com.google.android.apps.mytracks.MyMapsList"
android:label="@string/map_list_title"
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|orientation|fontScale"
android:exported="false" />
<activity android:name="com.google.android.apps.mytracks.TrackList"
android:label="Recorded Tracks" />
<activity android:name="com.google.android.apps.mytracks.WaypointsList"
android:label="Recorded Waypoints" />
<activity android:name="com.google.android.apps.mytracks.TrackDetails"
android:label="@string/trackdetails_title" />
<activity android:name="com.google.android.apps.mytracks.WaypointDetails"
android:label="@string/waypointdetails_title" />
<activity android:name="com.google.android.apps.mytracks.SettingsActivity"
android:label="@string/mytracks_settings" />
<activity android:name="com.google.android.apps.mytracks.AggregatedStatsActivity"
android:label="@string/aggregated_stats" />
<activity android:name="com.google.android.apps.mytracks.StatsActivity"
android:label="@string/statistics"
android:exported="false" />
<activity android:name="com.google.android.apps.mytracks.SensorStateActivity"
android:label="@string/sensor_state" />
<activity android:name="com.google.android.apps.mytracks.ChartActivity"
android:label="@string/elevation_label"
android:exported="false" />
<activity android:name="com.google.android.apps.mytracks.WelcomeActivity" <activity android:name="com.google.android.apps.mytracks.WelcomeActivity"
android:label="@string/welcome" android:label="@string/welcome"
android:exported="false" /> android:exported="false" />
@@ -104,7 +169,7 @@ limitations under the License.
<receiver android:name="com.google.android.apps.mytracks.BootReceiver"> <receiver android:name="com.google.android.apps.mytracks.BootReceiver">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" /> <action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter> </intent-filter>
</receiver> </receiver>
<receiver android:name="com.google.android.apps.mytracks.widgets.TrackWidgetProvider"> <receiver android:name="com.google.android.apps.mytracks.widgets.TrackWidgetProvider">
-7
View File
@@ -284,11 +284,4 @@ limitations under the License.
<item>punt de referència</item> <item>punt de referència</item>
</array> </array>
<array name="sensor_type_options">
<item>Cap</item>
<item>ANT+ Sensors</item>
<item>SRM ANT+ Bridge</item>
<item>Zephyr Bluetooth</item>
</array>
</resources> </resources>
+2
View File
@@ -409,4 +409,6 @@ limitations under the License.
<string name="searching">S\'està cercant</string> <string name="searching">S\'està cercant</string>
<string name="sending">S\'està enviant</string> <string name="sending">S\'està enviant</string>
<!-- Sensor Type Options -->
<string name="no_sensor_option">Cap</string>
</resources> </resources>
-7
View File
@@ -143,11 +143,4 @@ limitations under the License.
<item>camino</item> <item>camino</item>
</array> </array>
<array name="sensor_type_options">
<item>Ninguna</item>
<item>ANT+ Sensores</item>
<item>SRM ANT+ Puente</item>
<item>Zephyr Bluetooth</item>
</array>
</resources> </resources>
+4
View File
@@ -128,4 +128,8 @@ limitations under the License.
$6 : Speed units (mph) : Pace units (per mile) $6 : Speed units (mph) : Pace units (per mile)
--> -->
<!-- Sensor Type Options -->
<string name="no_sensor_option">Ninguna</string>
<string name="ant_sensor_option">ANT+ Sensores</string>
<string name="ant_bridge_sensor_option">SRM ANT+ Puente</string>
</resources> </resources>
-7
View File
@@ -291,11 +291,4 @@ limitations under the License.
<item>point de passage</item> <item>point de passage</item>
</array> </array>
<array name="sensor_type_options">
<item>Aucun</item>
<item>Capteur ANT+</item>
<item>Pont SRM ANT+</item>
<item>Bluetooth Zephyr</item>
</array>
</resources> </resources>
+7
View File
@@ -397,6 +397,13 @@ limitations under the License.
<string name="searching">Recherche</string> <string name="searching">Recherche</string>
<string name="sending">Envoi</string> <string name="sending">Envoi</string>
<!-- Sensor Type Options -->
<string name="no_sensor_option">Aucun</string>
<string name="ant_sensor_option">Capteur ANT+</string>
<string name="ant_bridge_sensor_option">Pont SRM ANT+</string>
<string name="polar_bt_sensor_option">Bluetooth Polar</string>
<string name="zephyr_bt_sensor_option">Bluetooth Zephyr</string>
<!-- Manifest description (not seen inside the app) --> <!-- Manifest description (not seen inside the app) -->
<string name="notification_permission_label">notifications lors de l\'enregistrement de parcours</string> <string name="notification_permission_label">notifications lors de l\'enregistrement de parcours</string>
<string name="read_tracks_permission_label">lecture des parcours enregistrés</string> <string name="read_tracks_permission_label">lecture des parcours enregistrés</string>
-7
View File
@@ -272,11 +272,4 @@ limitations under the License.
<item>ウェイポイント</item> <item>ウェイポイント</item>
</array> </array>
<array name="sensor_type_options">
<item>なし</item>
<item>ANT+ センサー</item>
<item>SRM ANT+ ブリッジ</item>
<item>Zephyr Bluetooth</item>
</array>
</resources> </resources>
+7 -2
View File
@@ -159,8 +159,8 @@ limitations under the License.
$2 : Attempted imports $2 : Attempted imports
$3 : MyTracks gpx path $3 : MyTracks gpx path
--> -->
<string name="import_done">%3$s からGPXファイル %2$d件のうち %1$d件をインポートしました</string> <string name="import_multi_done">%3$s からGPXファイル %2$d件のうち %1$d件をインポートしました</string>
<string name="import_empty">%s ではGPXファイルがありません.SDカードを読み込めないかディレクトリにGPXファイルが無いかも知れません.</string> <string name="import_multi_empty">%s ではGPXファイルがありません.SDカードを読み込めないかディレクトリにGPXファイルが無いかも知れません.</string>
<string name="import_error">ファイル %s のインポートエラー</string> <string name="import_error">ファイル %s のインポートエラー</string>
<!-- Waypoints list --> <!-- Waypoints list -->
@@ -463,6 +463,11 @@ limitations under the License.
<string name="searching">検索中</string> <string name="searching">検索中</string>
<string name="sending">送信中</string> <string name="sending">送信中</string>
<!-- Sensor Type Options -->
<string name="no_sensor_option">なし</string>
<string name="ant_sensor_option">ANT+ センサー</string>
<string name="ant_bridge_sensor_option">SRM ANT+ ブリッジ</string>
<!-- Manifest description (not seen inside the app) --> <!-- Manifest description (not seen inside the app) -->
<string name="notification_permission_label">トラック記録通知受信</string> <string name="notification_permission_label">トラック記録通知受信</string>
<string name="read_tracks_permission_label">トラック読み込み</string> <string name="read_tracks_permission_label">トラック読み込み</string>
-7
View File
@@ -272,11 +272,4 @@ limitations under the License.
<item>путевая точка</item> <item>путевая точка</item>
</array> </array>
<array name="sensor_type_options">
<item>Нет</item>
<item>ANT+ Sensors</item>
<item>SRM ANT+ Bridge</item>
<item>Zephyr Bluetooth</item>
</array>
</resources> </resources>
+7 -2
View File
@@ -162,8 +162,8 @@ limitations under the License.
$2 : Attempted imports $2 : Attempted imports
$3 : MyTracks gpx path $3 : MyTracks gpx path
--> -->
<string name="import_done">Импортировано %1$d из %2$d GPX файлов с %3$s</string> <string name="import_multi_done">Импортировано %1$d из %2$d GPX файлов с %3$s</string>
<string name="import_empty">GPX файлы не найденны в %s. Возможно не читается SD-карта или каталог не содержит GPX файлы.</string> <string name="import_multi_empty">GPX файлы не найденны в %s. Возможно не читается SD-карта или каталог не содержит GPX файлы.</string>
<string name="import_error">Ошибка импорта файла %s</string> <string name="import_error">Ошибка импорта файла %s</string>
<!-- Waypoints list --> <!-- Waypoints list -->
@@ -490,6 +490,11 @@ limitations under the License.
<string name="searching">Поиск</string> <string name="searching">Поиск</string>
<string name="sending">Отправка</string> <string name="sending">Отправка</string>
<!-- Sensor Type Options -->
<string name="no_sensor_option">Нет</string>
<string name="ant_sensor_option">ANT+ Датчик</string>
<string name="ant_bridge_sensor_option">SRM ANT+ Bridge</string>
<!-- Manifest description (not seen inside the app) --> <!-- Manifest description (not seen inside the app) -->
<string name="notification_permission_label">получать уведомления о записи трека</string> <string name="notification_permission_label">получать уведомления о записи трека</string>
<string name="read_tracks_permission_label">чтение записанных данных трека</string> <string name="read_tracks_permission_label">чтение записанных данных трека</string>
-7
View File
@@ -277,11 +277,4 @@ limitations under the License.
<item>way point</item> <item>way point</item>
</array> </array>
<array name="sensor_type_options">
<item>None</item>
<item>ANT+ Sensors</item>
<item>SRM ANT+ Bridge</item>
<item>Zephyr Bluetooth</item>
</array>
</resources> </resources>
@@ -98,10 +98,19 @@ limitations under the License.
<item>TCX</item> <item>TCX</item>
</array> </array>
<array name="sensor_type_options">
<item>@string/no_sensor_option</item>
<item>@string/ant_sensor_option</item>
<item>@string/ant_bridge_sensor_option</item>
<item>@string/polar_bt_sensor_option</item>
<item>@string/zephyr_bt_sensor_option</item>
</array>
<array name="sensor_type_values"> <array name="sensor_type_values">
<item>@string/none</item> <item>@string/none</item>
<item>@string/ant_sensor_type</item> <item>@string/ant_sensor_type</item>
<item>@string/srm_ant_bridge_sensor_type</item> <item>@string/srm_ant_bridge_sensor_type</item>
<item>@string/polar_sensor_type</item>
<item>@string/zephyr_sensor_type</item> <item>@string/zephyr_sensor_type</item>
</array> </array>
+4
View File
@@ -33,6 +33,7 @@ limitations under the License.
<string name="none">NONE</string> <string name="none">NONE</string>
<string name="ant_sensor_type">ANT</string> <string name="ant_sensor_type">ANT</string>
<string name="srm_ant_bridge_sensor_type">SRM_ANT_BRIDGE</string> <string name="srm_ant_bridge_sensor_type">SRM_ANT_BRIDGE</string>
<string name="polar_sensor_type">POLAR</string>
<string name="zephyr_sensor_type">ZEPHYR</string> <string name="zephyr_sensor_type">ZEPHYR</string>
<!-- Send to Google URLs --> <!-- Send to Google URLs -->
@@ -42,4 +43,7 @@ limitations under the License.
<!-- ID for Google Analytics --> <!-- ID for Google Analytics -->
<string name="google_analytics_id">UA-7222692-2</string> <string name="google_analytics_id">UA-7222692-2</string>
<!-- Intent actions -->
<string name="save_intent_action">android.intent.action.SAVE</string>
</resources> </resources>
+13 -2
View File
@@ -163,9 +163,12 @@ limitations under the License.
$2 : Attempted imports $2 : Attempted imports
$3 : MyTracks gpx path $3 : MyTracks gpx path
--> -->
<string name="import_done">Imported %1$d of %2$d GPX files from %3$s</string> <string name="import_multi_done">Imported %1$d of %2$d GPX files from %3$s</string>
<string name="import_empty">No GPX files found in %s. SD card may be unreadable or directory may not contain GPX files.</string> <string name="import_multi_empty">No GPX files found in %s. SD card may be unreadable or directory may not contain GPX files.</string>
<string name="import_error">Import error for file %s</string> <string name="import_error">Import error for file %s</string>
<string name="import_title">Import into My Tracks</string>
<string name="import_success">Successfully imported %1$d track(s) from file.</string>
<string name="import_show_track">Show track</string>
<!-- Waypoints list --> <!-- Waypoints list -->
<string name="waypointslist_btn_delete_all">Delete all markers</string> <string name="waypointslist_btn_delete_all">Delete all markers</string>
@@ -498,4 +501,12 @@ limitations under the License.
<string name="notification_permission_description">Be notified when a track starts or stops being recorded.</string> <string name="notification_permission_description">Be notified when a track starts or stops being recorded.</string>
<string name="read_tracks_permission_description">Be able to read track data, such as points and waypoints.</string> <string name="read_tracks_permission_description">Be able to read track data, such as points and waypoints.</string>
<string name="write_tracks_permission_description">Be able to write track data, such as starting recording or adding waypoints.</string> <string name="write_tracks_permission_description">Be able to write track data, such as starting recording or adding waypoints.</string>
<!-- Sensor Type Options -->
<string name="no_sensor_option">NONE</string>
<string name="ant_sensor_option">ANT+ Sensors</string>
<string name="ant_bridge_sensor_option">SRM ANT+ Bridge</string>
<string name="polar_bt_sensor_option">Polar Bluetooth</string>
<string name="zephyr_bt_sensor_option">Zephyr Bluetooth</string>
</resources> </resources>
+4 -4
View File
@@ -61,10 +61,10 @@ public class AntInterface {
private static Object INSTANCE_LOCK = new Object(); private static Object INSTANCE_LOCK = new Object();
/** The context to use. */ /** The context to use. */
private static Context sContext = null; private Context sContext = null;
/** Listens to changes to service connection status. */ /** Listens to changes to service connection status. */
private static ServiceListener sServiceListener; private ServiceListener sServiceListener;
/** Is the ANT Radio Proxy Service connected. */ /** Is the ANT Radio Proxy Service connected. */
private static boolean sServiceConnected = false; private static boolean sServiceConnected = false;
@@ -210,7 +210,7 @@ public class AntInterface {
/** /**
* Class for interacting with the ANT interface. * 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) { public void onServiceConnected(ComponentName pClassName, IBinder pService) {
// This is called when the connection with the service has been // This is called when the connection with the service has been
// established, giving us the service object we can use to // established, giving us the service object we can use to
@@ -1223,7 +1223,7 @@ public class AntInterface {
*/ */
public boolean requestForceClaimInterface(String appName) throws AntInterfaceException public boolean requestForceClaimInterface(String appName) throws AntInterfaceException
{ {
if((null == appName) || ("" == appName)) if((null == appName) || ("".equals(appName)))
{ {
throw new IllegalArgumentException(); throw new IllegalArgumentException();
} }
@@ -1,12 +1,12 @@
/* /*
* Copyright 2010 Google Inc. * Copyright 2010 Google Inc.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may not * 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 * use this file except in compliance with the License. You may obtain a copy of
* the License at * the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
@@ -29,7 +29,7 @@ import android.util.Log;
* @author Sandor Dornbush * @author Sandor Dornbush
*/ */
public class AccountChooser { public class AccountChooser {
/** /**
* The last selected account. * The last selected account.
*/ */
@@ -44,7 +44,7 @@ public class AccountChooser {
* Handle the account being selected. * Handle the account being selected.
* @param account The selected account or null if none could be found * @param account The selected account or null if none could be found
*/ */
public void handleAccountSelected(Account account); public void onAccountSelected(Account account);
} }
/** /**
@@ -52,7 +52,7 @@ public class AccountChooser {
* If no account is found the user will be alerted. * If no account is found the user will be alerted.
* If only one account is found that will be used. * If only one account is found that will be used.
* If multiple accounts are found the user will be allowed to choose. * If multiple accounts are found the user will be allowed to choose.
* *
* @param activity The parent activity * @param activity The parent activity
* @param handler The handler to be notified when an account has been selected * @param handler The handler to be notified when an account has been selected
*/ */
@@ -65,16 +65,16 @@ public class AccountChooser {
return; return;
} }
if (accounts.length == 1) { if (accounts.length == 1) {
handler.handleAccountSelected(accounts[0]); handler.onAccountSelected(accounts[0]);
return; return;
} }
// TODO This should be read out of a preference. // TODO This should be read out of a preference.
if (selectedAccount != null) { if (selectedAccount != null) {
handler.handleAccountSelected(selectedAccount); handler.onAccountSelected(selectedAccount);
return; return;
} }
// Let the user choose. // Let the user choose.
Log.e(Constants.TAG, "Multiple matching accounts found."); Log.e(Constants.TAG, "Multiple matching accounts found.");
final AlertDialog.Builder builder = new AlertDialog.Builder(activity); final AlertDialog.Builder builder = new AlertDialog.Builder(activity);
@@ -83,14 +83,20 @@ public class AccountChooser {
builder.setPositiveButton(R.string.ok, builder.setPositiveButton(R.string.ok,
new DialogInterface.OnClickListener() { new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) { public void onClick(DialogInterface dialog, int which) {
selectedAccount = accounts[selectedAccountIndex]; dialog.dismiss();
handler.handleAccountSelected(selectedAccount);
if (selectedAccountIndex >= 0) {
selectedAccount = accounts[selectedAccountIndex];
}
handler.onAccountSelected(selectedAccount);
} }
}); });
builder.setNegativeButton(R.string.cancel, builder.setNegativeButton(R.string.cancel,
new DialogInterface.OnClickListener() { new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) { public void onClick(DialogInterface dialog, int which) {
handler.handleAccountSelected(null); dialog.dismiss();
handler.onAccountSelected(null);
} }
}); });
String[] choices = new String[accounts.length]; String[] choices = new String[accounts.length];
@@ -120,7 +126,8 @@ public class AccountChooser {
builder.setNegativeButton(R.string.ok, builder.setNegativeButton(R.string.ok,
new DialogInterface.OnClickListener() { new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) { public void onClick(DialogInterface dialog, int which) {
handler.handleAccountSelected(null); dialog.dismiss();
handler.onAccountSelected(null);
} }
}); });
builder.show(); builder.show();
@@ -19,9 +19,9 @@ import java.util.List;
/** /**
* Activity for viewing the combined statistics for all the recorded tracks. * Activity for viewing the combined statistics for all the recorded tracks.
* *
* Other features to add - menu items to change setings. * Other features to add - menu items to change setings.
* *
* @author Fergus Nelson * @author Fergus Nelson
*/ */
public class AggregatedStatsActivity extends Activity implements public class AggregatedStatsActivity extends Activity implements
@@ -43,7 +43,7 @@ public class AggregatedStatsActivity extends Activity implements
Log.d(Constants.TAG, "StatsActivity: onSharedPreferences changed " Log.d(Constants.TAG, "StatsActivity: onSharedPreferences changed "
+ key); + key);
if (key != null) { if (key != null) {
if (key.equals(R.string.metric_units_key)) { if (key.equals(getString(R.string.metric_units_key))) {
metricUnits = sharedPreferences.getBoolean( metricUnits = sharedPreferences.getBoolean(
getString(R.string.metric_units_key), true); getString(R.string.metric_units_key), true);
utils.setMetricUnits(metricUnits); utils.setMetricUnits(metricUnits);
@@ -86,8 +86,8 @@ public class AggregatedStatsActivity extends Activity implements
} }
/** /**
* 1. Reads tracks from the db * 1. Reads tracks from the db
* 2. Merges the trip stats from the tracks * 2. Merges the trip stats from the tracks
* 3. Updates the view * 3. Updates the view
*/ */
private void loadAggregatedStats() { private void loadAggregatedStats() {
@@ -35,6 +35,9 @@ import com.google.android.apps.mytracks.util.UnitConversions;
import com.google.android.maps.mytracks.R; import com.google.android.maps.mytracks.R;
import android.app.Activity; import android.app.Activity;
import android.app.Dialog;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.location.Location; import android.location.Location;
import android.os.Bundle; import android.os.Bundle;
import android.util.Log; import android.util.Log;
@@ -58,6 +61,7 @@ import java.util.EnumSet;
*/ */
public class ChartActivity extends Activity implements TrackDataListener { public class ChartActivity extends Activity implements TrackDataListener {
private static final int CHART_SETTINGS_DIALOG = 1;
private final DoubleBuffer elevationBuffer = private final DoubleBuffer elevationBuffer =
new DoubleBuffer(Constants.ELEVATION_SMOOTHING_FACTOR); new DoubleBuffer(Constants.ELEVATION_SMOOTHING_FACTOR);
private final DoubleBuffer speedBuffer = private final DoubleBuffer speedBuffer =
@@ -73,7 +77,6 @@ public class ChartActivity extends Activity implements TrackDataListener {
private double trackMaxSpeed; private double trackMaxSpeed;
// Modes of operation // Modes of operation
private Mode mode = Mode.BY_DISTANCE;
private boolean metricUnits; private boolean metricUnits;
private boolean reportSpeed; private boolean reportSpeed;
@@ -93,6 +96,10 @@ public class ChartActivity extends Activity implements TrackDataListener {
private final Runnable updateChart = new Runnable() { private final Runnable updateChart = new Runnable() {
@Override @Override
public void run() { public void run() {
if (isFinishing()) {
return;
}
busyPane.setVisibility(View.GONE); busyPane.setVisibility(View.GONE);
zoomControls.setIsZoomInEnabled(chartView.canZoomIn()); zoomControls.setIsZoomInEnabled(chartView.canZoomIn());
zoomControls.setIsZoomOutEnabled(chartView.canZoomOut()); zoomControls.setIsZoomOutEnabled(chartView.canZoomOut());
@@ -105,8 +112,6 @@ public class ChartActivity extends Activity implements TrackDataListener {
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
Log.w(TAG, "ChartActivity.onCreate"); Log.w(TAG, "ChartActivity.onCreate");
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
MyTracks.getInstance().setChartActivity(this);
dataHub = MyTracks.getInstance().getDataHub();
// The volume we want to control is the Text-To-Speech volume // The volume we want to control is the Text-To-Speech volume
int volumeStream = int volumeStream =
@@ -117,7 +122,6 @@ public class ChartActivity extends Activity implements TrackDataListener {
setContentView(R.layout.mytracks_charts); setContentView(R.layout.mytracks_charts);
ViewGroup layout = (ViewGroup) findViewById(R.id.elevation_chart); ViewGroup layout = (ViewGroup) findViewById(R.id.elevation_chart);
chartView = new ChartView(this); chartView = new ChartView(this);
chartView.setMode(this.mode);
LayoutParams params = LayoutParams params =
new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT); new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT);
layout.addView(chartView, params); layout.addView(chartView, params);
@@ -142,6 +146,7 @@ public class ChartActivity extends Activity implements TrackDataListener {
protected void onResume() { protected void onResume() {
super.onResume(); super.onResume();
dataHub = TrackDataHub.getStartedInstance();
dataHub.registerTrackDataListener(this, EnumSet.of( dataHub.registerTrackDataListener(this, EnumSet.of(
ListenerDataType.SELECTED_TRACK_CHANGED, ListenerDataType.SELECTED_TRACK_CHANGED,
ListenerDataType.TRACK_UPDATES, ListenerDataType.TRACK_UPDATES,
@@ -154,6 +159,7 @@ public class ChartActivity extends Activity implements TrackDataListener {
@Override @Override
protected void onPause() { protected void onPause() {
dataHub.unregisterTrackDataListener(this); dataHub.unregisterTrackDataListener(this);
dataHub = null;
super.onPause(); super.onPause();
} }
@@ -170,27 +176,13 @@ public class ChartActivity extends Activity implements TrackDataListener {
zoomControls.setIsZoomOutEnabled(chartView.canZoomOut()); zoomControls.setIsZoomOutEnabled(chartView.canZoomOut());
} }
public void setMode(Mode newMode) { private void setMode(Mode newMode) {
if (this.mode != newMode) { if (chartView.getMode() != newMode) {
this.mode = newMode; chartView.setMode(newMode);
chartView.setMode(this.mode);
dataHub.reloadDataForListener(this); dataHub.reloadDataForListener(this);
} }
} }
public Mode getMode() {
return mode;
}
public void setSeriesEnabled(int index, boolean enabled) {
chartView.getChartValueSeries(index).setEnabled(enabled);
runOnUiThread(updateChart);
}
public boolean isSeriesEnabled(int index) {
return chartView.getChartValueSeries(index).isEnabled();
}
@Override @Override
public boolean onCreateOptionsMenu(Menu menu) { public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu); super.onCreateOptionsMenu(menu);
@@ -205,13 +197,52 @@ public class ChartActivity extends Activity implements TrackDataListener {
public boolean onOptionsItemSelected(MenuItem item) { public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) { switch (item.getItemId()) {
case Constants.MENU_CHART_SETTINGS: case Constants.MENU_CHART_SETTINGS:
MyTracks.getInstance().getDialogManager().showDialogSafely( showDialog(CHART_SETTINGS_DIALOG);
DialogManager.DIALOG_CHART_SETTINGS);
return true; return true;
} }
return super.onOptionsItemSelected(item); return super.onOptionsItemSelected(item);
} }
@Override
protected Dialog onCreateDialog(int id) {
if (id == CHART_SETTINGS_DIALOG) {
final ChartSettingsDialog settingsDialog = new ChartSettingsDialog(this);
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.setChartValueSeriesEnabled(i, settingsDialog.isSeriesEnabled(i));
}
setMode(settingsDialog.getMode());
chartView.postInvalidate();
}
});
return settingsDialog;
}
return super.onCreateDialog(id);
}
@Override
protected void onPrepareDialog(int id, Dialog dialog) {
super.onPrepareDialog(id, dialog);
if (id == CHART_SETTINGS_DIALOG) {
prepareSettingsDialog((ChartSettingsDialog) dialog);
}
}
private void prepareSettingsDialog(ChartSettingsDialog settingsDialog) {
settingsDialog.setMode(chartView.getMode());
for (int i = 0; i < ChartView.NUM_SERIES; i++) {
settingsDialog.setSeriesEnabled(i, chartView.isChartValueSeriesEnabled(i));
}
}
/** /**
* Given a location, creates a new data point for the chart. A data point is * Given a location, creates a new data point for the chart. A data point is
* an array double[3 or 6], where: * an array double[3 or 6], where:
@@ -258,6 +289,7 @@ public class ChartActivity extends Activity implements TrackDataListener {
} }
// TODO: Account for segment splits? // TODO: Account for segment splits?
Mode mode = chartView.getMode();
switch (mode) { switch (mode) {
case BY_DISTANCE: case BY_DISTANCE:
timeOrDistance = profileLength / 1000.0; timeOrDistance = profileLength / 1000.0;
@@ -1,12 +1,12 @@
/* /*
* Copyright 2009 Google Inc. * Copyright 2009 Google Inc.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may not * 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 * use this file except in compliance with the License. You may obtain a copy of
* the License at * the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
@@ -36,6 +36,7 @@ import android.widget.RadioGroup;
public class ChartSettingsDialog extends Dialog { public class ChartSettingsDialog extends Dialog {
private RadioButton distance; private RadioButton distance;
private CheckBox[] series; private CheckBox[] series;
private OnClickListener clickListener;
public ChartSettingsDialog(Context context) { public ChartSettingsDialog(Context context) {
super(context); super(context);
@@ -44,23 +45,31 @@ public class ChartSettingsDialog extends Dialog {
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE); requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.chart_settings); setContentView(R.layout.chart_settings);
Button cancel = (Button) findViewById(R.id.chart_settings_cancel); Button cancel = (Button) findViewById(R.id.chart_settings_cancel);
cancel.setOnClickListener(new View.OnClickListener() { cancel.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) { public void onClick(View v) {
if (clickListener != null) {
clickListener.onClick(ChartSettingsDialog.this, BUTTON_NEGATIVE);
}
dismiss(); 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) { 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); distance = (RadioButton) findViewById(R.id.chart_settings_by_distance);
series = new CheckBox[ChartView.NUM_SERIES]; series = new CheckBox[ChartView.NUM_SERIES];
series[ChartView.ELEVATION_SERIES] = series[ChartView.ELEVATION_SERIES] =
(CheckBox) findViewById(R.id.chart_settings_elevation); (CheckBox) findViewById(R.id.chart_settings_elevation);
@@ -74,29 +83,30 @@ public class ChartSettingsDialog extends Dialog {
(CheckBox) findViewById(R.id.chart_settings_heart_rate); (CheckBox) findViewById(R.id.chart_settings_heart_rate);
} }
public void setup(ChartActivity chart) { public void setMode(Mode mode) {
if (chart == null) {
return;
}
RadioGroup rd = (RadioGroup) findViewById(R.id.chart_settings_x); 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_distance
: R.id.chart_settings_by_time); : R.id.chart_settings_by_time);
for (int i = 0; i < ChartView.NUM_SERIES; i++) {
series[i].setChecked(chart.isSeriesEnabled(i));
}
} }
private void handleOk() { public void setSeriesEnabled(int seriesIdx, boolean enabled) {
ChartActivity chart = MyTracks.getInstance().getChartActivity(); series[seriesIdx].setChecked(enabled);
chart.setMode(distance.isChecked() ? Mode.BY_DISTANCE : Mode.BY_TIME); }
// TODO: check that something is visible. public Mode getMode() {
for (int i = 0; i < ChartView.NUM_SERIES; i++) { if (distance == null) return Mode.BY_DISTANCE;
chart.setSeriesEnabled(i, series[i].isChecked());
}
dismiss(); return distance.isChecked() ? 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;
} }
} }
@@ -49,6 +49,7 @@ import java.util.ArrayList;
* @author Leif Hendrik Wilden * @author Leif Hendrik Wilden
*/ */
public class ChartView extends View { public class ChartView extends View {
private static final int MIN_ZOOM_LEVEL = 1;
/* /*
* Scrolling logic: * Scrolling logic:
@@ -62,7 +63,6 @@ public class ChartView extends View {
* Zoom logic: * Zoom logic:
*/ */
private int zoomLevel = 1; private int zoomLevel = 1;
private final int minZoomLevel = 1;
private int maxZoomLevel = 10; private int maxZoomLevel = 10;
private static final int MAX_INTERVALS = 5; private static final int MAX_INTERVALS = 5;
@@ -199,7 +199,7 @@ public class ChartView extends View {
updateDimensions(); updateDimensions();
} }
public void setUpChartValueSeries(Context context) { private void setUpChartValueSeries(Context context) {
series = new ChartValueSeries[NUM_SERIES]; series = new ChartValueSeries[NUM_SERIES];
// Create the value series. // Create the value series.
@@ -333,7 +333,7 @@ public class ChartView extends View {
* @return true if the chart can be zoomed out * @return true if the chart can be zoomed out
*/ */
public boolean canZoomOut() { public boolean canZoomOut() {
return zoomLevel > minZoomLevel; return zoomLevel > MIN_ZOOM_LEVEL;
} }
/** /**
@@ -401,6 +401,7 @@ public class ChartView extends View {
/** /**
* Sets the display mode (by distance, by time). * Sets the display mode (by distance, by time).
* It is expected that after the mode change, data will be reloaded.
*/ */
public void setMode(Mode mode) { public void setMode(Mode mode) {
this.mode = mode; this.mode = mode;
@@ -593,7 +594,7 @@ public class ChartView extends View {
final float x = getWaypointX(waypoint); final float x = getWaypointX(waypoint);
c.drawLine(x, h - bottomBorder, x, topBorder, gridPaint); 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) { if (waypoints.get(i).getType() == Waypoint.TYPE_STATISTICS) {
statsMarker.draw(c); statsMarker.draw(c);
} else { } else {
@@ -865,14 +866,24 @@ public class ChartView extends View {
} }
/** /**
* Gets one of the chart value series. * Returns whether a given time series is enabled for drawing.
* The index should be one of the following values:
* ELEVATION_SERIES or SPEED_SERIES
* *
* @param index of the value series * @param index the time series, one of {@link #ELEVATION_SERIES},
* @return The chart series at the index * {@link #SPEED_SERIES}, {@link #POWER_SERIES}, etc.
* @return true if drawn, false otherwise
*/ */
public ChartValueSeries getChartValueSeries(int index) { public boolean isChartValueSeriesEnabled(int index) {
return series[index]; return series[index].isEnabled();
}
/**
* Sets whether a given time series will be enabled for drawing.
*
* @param index the time series, one of {@link #ELEVATION_SERIES},
* {@link #SPEED_SERIES}, {@link #POWER_SERIES}, etc.
* @return true to be drawn, false otherwise
*/
public void setChartValueSeriesEnabled(int index, boolean enabled) {
series[index].setEnabled(enabled);
} }
} }
@@ -50,7 +50,7 @@ public abstract class Constants {
public static final int AUTHENTICATE_TO_DOCLIST = 10; public static final int AUTHENTICATE_TO_DOCLIST = 10;
public static final int AUTHENTICATE_TO_TRIX = 11; public static final int AUTHENTICATE_TO_TRIX = 11;
public static final int DELETE_TRACK = 13; 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_LINK = 15;
public static final int SHARE_GPX_FILE = 16; public static final int SHARE_GPX_FILE = 16;
public static final int SHARE_KML_FILE = 17; public static final int SHARE_KML_FILE = 17;
@@ -190,7 +190,7 @@ public abstract class Constants {
public static int getActionFromMenuId(int menuId) { public static int getActionFromMenuId(int menuId) {
switch (menuId) { switch (menuId) {
case Constants.MENU_SEND_TO_GOOGLE: case Constants.MENU_SEND_TO_GOOGLE:
return Constants.SEND_TO_GOOGLE_DIALOG; return Constants.SEND_TO_GOOGLE;
case Constants.MENU_EDIT: case Constants.MENU_EDIT:
return Constants.EDIT_DETAILS; return Constants.EDIT_DETAILS;
case Constants.MENU_DELETE: case Constants.MENU_DELETE:
@@ -17,14 +17,8 @@ package com.google.android.apps.mytracks;
import static com.google.android.apps.mytracks.Constants.TAG; 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.Activity;
import android.app.AlertDialog;
import android.app.Dialog; import android.app.Dialog;
import android.app.ProgressDialog;
import android.os.Bundle;
import android.util.Log; import android.util.Log;
import android.view.WindowManager.BadTokenException; import android.view.WindowManager.BadTokenException;
@@ -35,145 +29,16 @@ import android.view.WindowManager.BadTokenException;
*/ */
public class DialogManager { 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) {
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) {
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);
}
}
});
}
/** /**
* The equivalent of {@link #showDialogSafely(int)}, but for a specific * The equivalent of {@link #showDialogSafely(int)}, but for a specific
* dialog instance. * dialog instance.
*/ */
public static void showDialogSafely(Activity activity, final Dialog dialog) { public static void showDialogSafely(Activity activity, final Dialog dialog) {
if (activity.isFinishing()) {
Log.w(TAG, "Activity finishing - not showing dialog");
return;
}
activity.runOnUiThread(new Runnable() { activity.runOnUiThread(new Runnable() {
public void run() { public void run() {
try { try {
@@ -187,26 +52,16 @@ public class DialogManager {
}); });
} }
/**
* Dismisses the progress dialog if it is showing. Executed on the UI thread.
*/
public void dismissDialogSafely(final int id) {
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 * The equivalent of {@link #dismissDialogSafely(int)}, but for a specific
* dialog instance. * dialog instance.
*/ */
public static void dismissDialogSafely(Activity activity, final Dialog dialog) { public static void dismissDialogSafely(Activity activity, final Dialog dialog) {
if (activity.isFinishing()) {
Log.w(TAG, "Activity finishing - not dismissing dialog");
return;
}
activity.runOnUiThread(new Runnable() { activity.runOnUiThread(new Runnable() {
public void run() { public void run() {
try { try {
@@ -217,4 +72,6 @@ public class DialogManager {
} }
}); });
} }
private DialogManager() {}
} }
@@ -29,8 +29,6 @@ import android.app.ProgressDialog;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.database.Cursor; import android.database.Cursor;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.PowerManager.WakeLock; import android.os.PowerManager.WakeLock;
import android.util.Log; import android.util.Log;
import android.widget.Toast; import android.widget.Toast;
@@ -91,12 +89,7 @@ public class ExportAllTracks {
new DialogInterface.OnClickListener() { new DialogInterface.OnClickListener() {
@Override @Override
public void onClick(DialogInterface dialog, int which) { public void onClick(DialogInterface dialog, int which) {
HandlerThread handlerThread; new Thread(runner, "SendToMyMaps").start();
Handler handler;
handlerThread = new HandlerThread("SendToMyMaps");
handlerThread.start();
handler = new Handler(handlerThread.getLooper());
handler.post(runner);
} }
}; };
@@ -117,7 +110,7 @@ public class ExportAllTracks {
long recordingTrackId = -1; long recordingTrackId = -1;
if (prefs != null) { if (prefs != null) {
recordingTrackId = recordingTrackId =
prefs.getLong(activity.getString(R.string.recording_track_key), -1); prefs.getLong(activity.getString(R.string.recording_track_key), -1);
} }
if (recordingTrackId != -1) { if (recordingTrackId != -1) {
wakeLock = SystemUtils.acquireWakeLock(activity, wakeLock); wakeLock = SystemUtils.acquireWakeLock(activity, wakeLock);
@@ -1,12 +1,12 @@
/* /*
* Copyright 2011 Google Inc. * Copyright 2011 Google Inc.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may not * 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 * use this file except in compliance with the License. You may obtain a copy of
* the License at * the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
@@ -25,8 +25,6 @@ import android.app.Activity;
import android.app.AlertDialog; import android.app.AlertDialog;
import android.app.ProgressDialog; import android.app.ProgressDialog;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.PowerManager.WakeLock; import android.os.PowerManager.WakeLock;
import android.util.Log; import android.util.Log;
import android.widget.Toast; import android.widget.Toast;
@@ -45,7 +43,7 @@ import org.xml.sax.SAXException;
/** /**
* A class that will import all GPX tracks in /sdcard/MyTracks/gpx/ * A class that will import all GPX tracks in /sdcard/MyTracks/gpx/
* *
* @author David Piggott * @author David Piggott
*/ */
public class ImportAllTracks { public class ImportAllTracks {
@@ -63,12 +61,8 @@ public class ImportAllTracks {
Log.i(Constants.TAG, "ImportAllTracks: Starting"); Log.i(Constants.TAG, "ImportAllTracks: Starting");
fileUtils = new FileUtils(); fileUtils = new FileUtils();
gpxPath = fileUtils.buildExternalDirectoryPath("gpx"); gpxPath = fileUtils.buildExternalDirectoryPath("gpx");
HandlerThread handlerThread;
Handler handler; new Thread(runner).start();
handlerThread = new HandlerThread("ImportAllTracks");
handlerThread.start();
handler = new Handler(handlerThread.getLooper());
handler.post(runner);
} }
private final Runnable runner = new Runnable() { private final Runnable runner = new Runnable() {
@@ -104,9 +98,9 @@ public class ImportAllTracks {
Log.i(Constants.TAG, "ImportAllTracks: Done"); Log.i(Constants.TAG, "ImportAllTracks: Done");
AlertDialog.Builder builder = new AlertDialog.Builder(activity); AlertDialog.Builder builder = new AlertDialog.Builder(activity);
if (gpxFileCount == 0) { if (gpxFileCount == 0) {
builder.setMessage(activity.getString(R.string.import_empty, gpxPath + "/")); builder.setMessage(activity.getString(R.string.import_multi_empty, gpxPath + "/"));
} else { } else {
builder.setMessage(activity.getString(R.string.import_done, importSuccessCount, gpxFileCount, builder.setMessage(activity.getString(R.string.import_multi_done, importSuccessCount, gpxFileCount,
gpxPath + "/")); gpxPath + "/"));
} }
builder.setPositiveButton(R.string.ok, null); builder.setPositiveButton(R.string.ok, null);
@@ -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.TrackDataHub.ListenerDataType;
import com.google.android.apps.mytracks.content.TrackDataListener; import com.google.android.apps.mytracks.content.TrackDataListener;
import com.google.android.apps.mytracks.content.Waypoint; 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.services.tasks.StatusAnnouncerFactory;
import com.google.android.apps.mytracks.stats.TripStatistics; import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.android.apps.mytracks.util.ApiFeatures; import com.google.android.apps.mytracks.util.ApiFeatures;
@@ -122,7 +124,7 @@ public class MapActivity extends com.google.android.maps.MapActivity
// ------------------------ // ------------------------
@Override @Override
public void onCreate(Bundle bundle) { protected void onCreate(Bundle bundle) {
Log.d(TAG, "MapActivity.onCreate"); Log.d(TAG, "MapActivity.onCreate");
super.onCreate(bundle); super.onCreate(bundle);
@@ -131,8 +133,6 @@ public class MapActivity extends com.google.android.maps.MapActivity
new StatusAnnouncerFactory(ApiFeatures.getInstance()).getVolumeStream(); new StatusAnnouncerFactory(ApiFeatures.getInstance()).getVolumeStream();
setVolumeControlStream(volumeStream); setVolumeControlStream(volumeStream);
dataHub = MyTracks.getInstance().getDataHub();
// We don't need a window title bar: // We don't need a window title bar:
requestWindowFeature(Window.FEATURE_NO_TITLE); requestWindowFeature(Window.FEATURE_NO_TITLE);
@@ -179,9 +179,10 @@ public class MapActivity extends com.google.android.maps.MapActivity
@Override @Override
protected void onResume() { protected void onResume() {
Log.d(TAG, "MapActivity.onStart"); Log.d(TAG, "MapActivity.onResume");
super.onResume(); super.onResume();
dataHub = TrackDataHub.getStartedInstance();
dataHub.registerTrackDataListener(this, EnumSet.of( dataHub.registerTrackDataListener(this, EnumSet.of(
ListenerDataType.SELECTED_TRACK_CHANGED, ListenerDataType.SELECTED_TRACK_CHANGED,
ListenerDataType.POINT_UPDATES, ListenerDataType.POINT_UPDATES,
@@ -202,9 +203,10 @@ public class MapActivity extends com.google.android.maps.MapActivity
@Override @Override
protected void onPause() { protected void onPause() {
Log.d(TAG, "MapActivity.onStop"); Log.d(TAG, "MapActivity.onPause");
dataHub.unregisterTrackDataListener(this); dataHub.unregisterTrackDataListener(this);
dataHub = null;
super.onPause(); super.onPause();
} }
@@ -392,13 +394,27 @@ public class MapActivity extends com.google.android.maps.MapActivity
@Override @Override
public boolean onMenuItemSelected(int featureId, MenuItem item) { public boolean onMenuItemSelected(int featureId, MenuItem item) {
if (!super.onMenuItemSelected(featureId, item)) { switch (item.getItemId()) {
MyTracks.getInstance().onActivityResult( case Constants.MENU_SEND_TO_GOOGLE:
Constants.getActionFromMenuId(item.getItemId()), RESULT_OK, SendActivity.sendToGoogle(this, dataHub.getSelectedTrackId(), false);
new Intent()); return true;
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 @Override
@@ -15,6 +15,8 @@
*/ */
package com.google.android.apps.mytracks; package com.google.android.apps.mytracks;
import static com.google.android.apps.mytracks.Constants.TAG;
import com.google.android.apps.mytracks.content.Waypoint; import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.util.LocationUtils; import com.google.android.apps.mytracks.util.LocationUtils;
import com.google.android.maps.GeoPoint; import com.google.android.maps.GeoPoint;
@@ -33,6 +35,7 @@ import android.graphics.Point;
import android.graphics.Rect; import android.graphics.Rect;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.location.Location; import android.location.Location;
import android.util.Log;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -71,7 +74,7 @@ public class MapOverlay extends Overlay {
private GeoPoint lastReferencePoint; private GeoPoint lastReferencePoint;
private Rect lastViewRect; private Rect lastViewRect;
private Path lastPath; private Path lastPath;
/** /**
* Represents a pre-processed {@code Location} to speed up drawing. * Represents a pre-processed {@code Location} to speed up drawing.
* This class is more like a data object and doesn't provide accessors. * This class is more like a data object and doesn't provide accessors.
@@ -93,13 +96,13 @@ public class MapOverlay extends Overlay {
*/ */
public CachedLocation(Location location) { public CachedLocation(Location location) {
this.valid = LocationUtils.isValidLocation(location); this.valid = LocationUtils.isValidLocation(location);
this.geoPoint = valid ? LocationUtils.getGeoPoint(location) : null; this.geoPoint = valid ? LocationUtils.getGeoPoint(location) : null;
} }
}; };
public MapOverlay(Context context) { public MapOverlay(Context context) {
this.context = context; this.context = context;
this.waypoints = new ArrayList<Waypoint>(); this.waypoints = new ArrayList<Waypoint>();
this.points = new ArrayList<CachedLocation>(1024); this.points = new ArrayList<CachedLocation>(1024);
this.pendingPoints = new ArrayBlockingQueue<CachedLocation>( this.pendingPoints = new ArrayBlockingQueue<CachedLocation>(
@@ -164,7 +167,7 @@ public class MapOverlay extends Overlay {
/** /**
* Add a location to the map overlay. * Add a location to the map overlay.
* *
* NOTE: This method doesn't take ownership of the given location, so it is * NOTE: This method doesn't take ownership of the given location, so it is
* safe to reuse the same location while calling this method. * safe to reuse the same location while calling this method.
* *
@@ -172,14 +175,18 @@ public class MapOverlay extends Overlay {
*/ */
public void addLocation(Location l) { public void addLocation(Location l) {
// Queue up in the pending queue until it's merged with {@code #points}. // 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. * Adds a segment split to the map overlay.
*/ */
public void addSegmentSplit() { 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) { public void addWaypoint(Waypoint wpt) {
@@ -203,7 +210,7 @@ public class MapOverlay extends Overlay {
return waypoints.size(); return waypoints.size();
} }
} }
public void clearPoints() { public void clearPoints() {
synchronized (points) { synchronized (points) {
points.clear(); points.clear();
@@ -235,10 +242,15 @@ public class MapOverlay extends Overlay {
// It's safe to keep projection within a single draw operation. // It's safe to keep projection within a single draw operation.
final Projection projection = getMapProjection(mapView); final Projection projection = getMapProjection(mapView);
if (projection == null) {
Log.w(TAG, "No projection, unable to draw");
return;
}
// Get the current viewing window. // Get the current viewing window.
if (trackDrawingEnabled) { if (trackDrawingEnabled) {
Rect viewRect = getMapViewRect(mapView); Rect viewRect = getMapViewRect(mapView);
// Draw the selected track: // Draw the selected track:
drawTrack(canvas, projection, viewRect); drawTrack(canvas, projection, viewRect);
@@ -249,12 +261,12 @@ public class MapOverlay extends Overlay {
// Draw the current location // Draw the current location
drawMyLocation(canvas, projection); drawMyLocation(canvas, projection);
} }
// Visible for testing. // Visible for testing.
Projection getMapProjection(MapView mapView) { Projection getMapProjection(MapView mapView) {
return mapView.getProjection(); return mapView.getProjection();
} }
// Visible for testing. // Visible for testing.
Rect getMapViewRect(MapView mapView) { Rect getMapViewRect(MapView mapView) {
int w = mapView.getLongitudeSpan(); int w = mapView.getLongitudeSpan();
@@ -263,12 +275,12 @@ public class MapOverlay extends Overlay {
int cy = mapView.getMapCenter().getLatitudeE6(); int cy = mapView.getMapCenter().getLatitudeE6();
return new Rect(cx - w / 2, cy - h / 2, cx + w / 2, cy + h / 2); return new Rect(cx - w / 2, cy - h / 2, cx + w / 2, cy + h / 2);
} }
// Visible for testing. // Visible for testing.
Path newPath() { Path newPath() {
return new Path(); return new Path();
} }
// Visible for testing. // Visible for testing.
Path getLastPath() { Path getLastPath() {
return lastPath; return lastPath;
@@ -304,9 +316,9 @@ public class MapOverlay extends Overlay {
synchronized (points) { synchronized (points) {
// Merge the pending points with the list of cached locations. // Merge the pending points with the list of cached locations.
final GeoPoint referencePoint = projection.fromPixels(0, 0); final GeoPoint referencePoint = projection.fromPixels(0, 0);
int newPoints = pendingPoints.drainTo(points); int newPoints = pendingPoints.drainTo(points);
boolean newProjection = !viewRect.equals(lastViewRect) || boolean newProjection = !viewRect.equals(lastViewRect) ||
!referencePoint.equals(lastReferencePoint); !referencePoint.equals(lastReferencePoint);
if (newPoints == 0 && lastPath != null && !newProjection) { if (newPoints == 0 && lastPath != null && !newProjection) {
// No need to recreate path (same points and viewing area). // No need to recreate path (same points and viewing area).
path = lastPath; path = lastPath;
@@ -318,12 +330,12 @@ public class MapOverlay extends Overlay {
} else if (lastPath != null && !newProjection) { } else if (lastPath != null && !newProjection) {
// Incremental update of the path, without repositioning the view. // Incremental update of the path, without repositioning the view.
path = lastPath; path = lastPath;
updatePath(projection, viewRect, path, numPoints - newPoints); updatePath(projection, viewRect, path, numPoints - newPoints);
} else { } else {
// The view has changed so we have to start from scratch. // The view has changed so we have to start from scratch.
path = newPath(); path = newPath();
path.incReserve(numPoints); path.incReserve(numPoints);
updatePath(projection, viewRect, path, 0); updatePath(projection, viewRect, path, 0);
} }
lastPath = path; lastPath = path;
} }
@@ -344,7 +356,7 @@ public class MapOverlay extends Overlay {
} }
} }
} }
// Draw the "Start" marker. // Draw the "Start" marker.
for (int i = 0; i < points.size(); ++i) { for (int i = 0; i < points.size(); ++i) {
if (points.get(i).valid) { if (points.get(i).valid) {
@@ -354,7 +366,7 @@ public class MapOverlay extends Overlay {
} }
} }
} }
private void updatePath(Projection projection, Rect viewRect, Path path, private void updatePath(Projection projection, Rect viewRect, Path path,
int startLocationIdx) { int startLocationIdx) {
// Whether to start a new segment on new valid and visible point. // Whether to start a new segment on new valid and visible point.
@@ -370,7 +382,7 @@ public class MapOverlay extends Overlay {
newSegment = true; newSegment = true;
continue; continue;
} }
final GeoPoint geoPoint = loc.geoPoint; final GeoPoint geoPoint = loc.geoPoint;
// Check if this breaks the existing segment. // Check if this breaks the existing segment.
boolean visible = alwaysVisible || viewRect.contains( boolean visible = alwaysVisible || viewRect.contains(
@@ -380,7 +392,7 @@ public class MapOverlay extends Overlay {
newSegment = true; newSegment = true;
} }
lastVisible = visible; lastVisible = visible;
// Either move to beginning of a new segment or continue the old one. // Either move to beginning of a new segment or continue the old one.
projection.toPixels(geoPoint, pt); projection.toPixels(geoPoint, pt);
if (newSegment) { if (newSegment) {
@@ -391,7 +403,7 @@ public class MapOverlay extends Overlay {
} }
} }
} }
// Visible for testing. // Visible for testing.
Point drawElement(Canvas canvas, Projection projection, GeoPoint geoPoint, Point drawElement(Canvas canvas, Projection projection, GeoPoint geoPoint,
Drawable element, int offsetX, int offsetY) { Drawable element, int offsetX, int offsetY) {
@@ -1,12 +1,12 @@
/* /*
* Copyright 2011 Google Inc. * Copyright 2011 Google Inc.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may not * 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 * use this file except in compliance with the License. You may obtain a copy of
* the License at * the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
@@ -15,6 +15,8 @@
*/ */
package com.google.android.apps.mytracks; package com.google.android.apps.mytracks;
import static com.google.android.apps.mytracks.Constants.TAG;
import com.google.android.accounts.Account; import com.google.android.accounts.Account;
import com.google.android.apps.mytracks.io.AuthManager; import com.google.android.apps.mytracks.io.AuthManager;
import com.google.android.apps.mytracks.io.AuthManagerFactory; import com.google.android.apps.mytracks.io.AuthManagerFactory;
@@ -25,11 +27,8 @@ import com.google.android.maps.mytracks.R;
import android.app.Activity; import android.app.Activity;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.view.ContextMenu; import android.util.Log;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.View.OnCreateContextMenuListener;
import android.widget.AdapterView; import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener; import android.widget.AdapterView.OnItemClickListener;
import android.widget.ListView; import android.widget.ListView;
@@ -42,29 +41,15 @@ import android.widget.TextView;
* @author Rodrigo Damazio * @author Rodrigo Damazio
*/ */
public class MyMapsList extends Activity implements MapsFacade.MapsListCallback { 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 static final int GET_LOGIN = 1;
public static final String EXTRA_ACCOUNT_NAME = "accountName";
public static final String EXTRA_ACCOUNT_TYPE = "accountType";
private MapsFacade mapsClient; private MapsFacade mapsClient;
private AuthManager auth; private AuthManager auth;
private MyMapsListAdapter listAdapter; 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 = private final OnItemClickListener clickListener =
new OnItemClickListener() { new OnItemClickListener() {
@Override @Override
@@ -91,7 +76,6 @@ public class MyMapsList extends Activity implements MapsFacade.MapsListCallback
ListView list = (ListView) findViewById(R.id.maplist); ListView list = (ListView) findViewById(R.id.maplist);
list.setOnItemClickListener(clickListener); list.setOnItemClickListener(clickListener);
list.setOnCreateContextMenuListener(contextMenuListener);
list.setAdapter(listAdapter); list.setAdapter(listAdapter);
startLogin(); startLogin();
@@ -101,19 +85,18 @@ public class MyMapsList extends Activity implements MapsFacade.MapsListCallback
// Starts in the UI thread. // Starts in the UI thread.
// TODO fix this for non-froyo devices. // TODO fix this for non-froyo devices.
if (AuthManagerFactory.useModernAuthManager()) { if (AuthManagerFactory.useModernAuthManager()) {
MyTracks.getInstance().getAccountChooser().chooseAccount( Intent intent = getIntent();
MyMapsList.this, String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
new AccountChooser.AccountHandler() { String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
@Override if (accountName == null || accountType == null) {
public void handleAccountSelected(Account account) { Log.e(TAG, "Didn't receive account name or type");
// Account selection happens in the UI thread. setResult(RESULT_CANCELED);
if (account != null) { finish();
// The user did not quit and there was a valid google return;
// account. }
doLogin(account);
} Account account = new Account(accountName, accountType);
} doLogin(account);
});
} else { } else {
doLogin(null); doLogin(null);
} }
@@ -183,27 +166,4 @@ public class MyMapsList extends Activity implements MapsFacade.MapsListCallback
} }
super.onActivityResult(requestCode, resultCode, data); 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;
}
} }
File diff suppressed because it is too large Load Diff
@@ -16,8 +16,10 @@
package com.google.android.apps.mytracks; package com.google.android.apps.mytracks;
import static com.google.android.apps.mytracks.Constants.TAG; import static com.google.android.apps.mytracks.Constants.TAG;
import com.google.android.apps.mytracks.content.Sensor; import com.google.android.apps.mytracks.content.Sensor;
import com.google.android.apps.mytracks.services.ITrackRecordingService; 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.apps.mytracks.services.sensors.SensorUtils;
import com.google.android.maps.mytracks.R; import com.google.android.maps.mytracks.R;
import com.google.protobuf.InvalidProtocolBufferException; 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. * A task which will update the U/I.
*/ */
@@ -79,35 +86,45 @@ public class SensorStateActivity extends Activity {
setContentView(R.layout.sensor_state); setContentView(R.layout.sensor_state);
// TODO: Allow reading from sensors even if service is not running.
serviceConnection = new TrackRecordingServiceConnection(this, stateUpdater);
serviceConnection.bindIfRunning();
updateState(); updateState();
} }
@Override @Override
protected void onResume() { protected void onResume() {
super.onResume(); super.onResume();
serviceConnection.bindIfRunning();
timer = new Timer(); timer = new Timer();
timer.schedule(new RefreshTask(), REFRESH_PERIOD_MS, REFRESH_PERIOD_MS); timer.schedule(new RefreshTask(), REFRESH_PERIOD_MS, REFRESH_PERIOD_MS);
} }
@Override @Override
protected void onPause() { protected void onPause() {
super.onPause();
timer.cancel(); timer.cancel();
timer.purge(); timer.purge();
timer = null; timer = null;
super.onPause();
}
@Override
protected void onDestroy() {
serviceConnection.unbind();
super.onDestroy();
} }
protected void updateState() { protected void updateState() {
MyTracks mt = MyTracks.getInstance(); ITrackRecordingService service = serviceConnection.getServiceIfBound();
ITrackRecordingService service =
mt == null ? null : mt.getTrackRecordingService();
if (service == null) { if (service == null) {
Log.d(Constants.TAG, "Could not get track recording service."); Log.d(Constants.TAG, "Could not get track recording service.");
updateSensorState(Sensor.SensorState.NONE); updateSensorState(Sensor.SensorState.NONE);
updateSensorData(null); updateSensorData(null);
return; return;
} }
Sensor.SensorDataSet sds = null; Sensor.SensorDataSet sds = null;
try { try {
byte[] buff = service.getSensorData(); byte[] buff = service.getSensorData();
@@ -153,8 +170,8 @@ public class SensorStateActivity extends Activity {
if (sds.hasPower() && sds.getPower().hasValue() if (sds.hasPower() && sds.getPower().hasValue()
&& sds.getPower().getState() == Sensor.SensorState.SENDING) { && sds.getPower().getState() == Sensor.SensorState.SENDING) {
utils.setText(R.id.power_state_register, utils.setText(R.id.power_state_register,
Integer.toString(sds.getPower().getValue())); Integer.toString(sds.getPower().getValue()));
} else { } else {
utils.setText(R.id.power_state_register, utils.setText(R.id.power_state_register,
SensorUtils.getStateAsString( SensorUtils.getStateAsString(
@@ -180,14 +197,14 @@ public class SensorStateActivity extends Activity {
if (sds.hasHeartRate() && sds.getHeartRate().hasValue() if (sds.hasHeartRate() && sds.getHeartRate().hasValue()
&& sds.getHeartRate().getState() == Sensor.SensorState.SENDING) { && sds.getHeartRate().getState() == Sensor.SensorState.SENDING) {
utils.setText(R.id.heart_rate_register, utils.setText(R.id.heart_rate_register,
Integer.toString(sds.getHeartRate().getValue())); Integer.toString(sds.getHeartRate().getValue()));
} else { } else {
utils.setText(R.id.heart_rate_register, utils.setText(R.id.heart_rate_register,
SensorUtils.getStateAsString( SensorUtils.getStateAsString(
sds.hasHeartRate() sds.hasHeartRate()
? sds.getHeartRate().getState() ? sds.getHeartRate().getState()
: Sensor.SensorState.NONE, : Sensor.SensorState.NONE,
this)); this));
} }
} }
} }
@@ -232,7 +232,8 @@ public class SettingsActivity extends PreferenceActivity {
private void updateSensorSettings(String sensorType) { private void updateSensorSettings(String sensorType) {
boolean usesBluetooth = boolean usesBluetooth =
getString(R.string.zephyr_sensor_type).equals(sensorType); getString(R.string.zephyr_sensor_type).equals(sensorType)
|| getString(R.string.polar_sensor_type).equals(sensorType);
findPreference( findPreference(
getString(R.string.bluetooth_sensor_key)).setEnabled(usesBluetooth); getString(R.string.bluetooth_sensor_key)).setEnabled(usesBluetooth);
findPreference( findPreference(
@@ -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.TrackDataHub.ListenerDataType;
import com.google.android.apps.mytracks.content.TrackDataListener; import com.google.android.apps.mytracks.content.TrackDataListener;
import com.google.android.apps.mytracks.content.Waypoint; 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.services.tasks.StatusAnnouncerFactory;
import com.google.android.apps.mytracks.util.ApiFeatures; import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.maps.mytracks.R; import com.google.android.maps.mytracks.R;
import android.app.Activity; import android.app.Activity;
import android.content.SharedPreferences;
import android.location.Location; import android.location.Location;
import android.os.Bundle; import android.os.Bundle;
import android.util.DisplayMetrics; import android.util.DisplayMetrics;
@@ -46,6 +48,18 @@ import java.util.EnumSet;
* @author Rodrigo Damazio * @author Rodrigo Damazio
*/ */
public class StatsActivity extends Activity implements TrackDataListener { 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 != null && dataHub.isRecordingSelected()) {
utils.setTime(R.id.total_time_register,
System.currentTimeMillis() - startTime);
}
}
};
private StatsUtilities utils; private StatsUtilities utils;
private UIUpdateThread thread; private UIUpdateThread thread;
@@ -61,18 +75,7 @@ public class StatsActivity extends Activity implements TrackDataListener {
private boolean showCurrentSegment = false; private boolean showCurrentSegment = false;
private TrackDataHub dataHub; private TrackDataHub dataHub;
private SharedPreferences preferences;
/**
* 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);
}
}
};
/** /**
* A thread that updates the total time field every second. * A thread that updates the total time field every second.
@@ -87,7 +90,7 @@ public class StatsActivity extends Activity implements TrackDataListener {
@Override @Override
public void run() { public void run() {
Log.i(TAG, "Started UI update thread"); Log.i(TAG, "Started UI update thread");
while (MyTracks.getInstance().isRecording()) { while (ServiceUtils.isRecording(StatsActivity.this, null, preferences)) {
runOnUiThread(updateResults); runOnUiThread(updateResults);
try { try {
Thread.sleep(1000L); Thread.sleep(1000L);
@@ -102,10 +105,10 @@ public class StatsActivity extends Activity implements TrackDataListener {
/** Called when the activity is first created. */ /** Called when the activity is first created. */
@Override @Override
public void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
dataHub = MyTracks.getInstance().getDataHub(); preferences = getSharedPreferences(Constants.SETTINGS_NAME, 0);
utils = new StatsUtilities(this); utils = new StatsUtilities(this);
// The volume we want to control is the Text-To-Speech volume // The volume we want to control is the Text-To-Speech volume
@@ -132,18 +135,20 @@ public class StatsActivity extends Activity implements TrackDataListener {
@Override @Override
protected void onResume() { protected void onResume() {
super.onResume();
dataHub = TrackDataHub.getStartedInstance();
dataHub.registerTrackDataListener(this, EnumSet.of( dataHub.registerTrackDataListener(this, EnumSet.of(
ListenerDataType.SELECTED_TRACK_CHANGED, ListenerDataType.SELECTED_TRACK_CHANGED,
ListenerDataType.TRACK_UPDATES, ListenerDataType.TRACK_UPDATES,
ListenerDataType.LOCATION_UPDATES, ListenerDataType.LOCATION_UPDATES,
ListenerDataType.DISPLAY_PREFERENCES)); ListenerDataType.DISPLAY_PREFERENCES));
super.onResume();
} }
@Override @Override
protected void onPause() { protected void onPause() {
dataHub.unregisterTrackDataListener(this); dataHub.unregisterTrackDataListener(this);
dataHub = null;
if (thread != null) { if (thread != null) {
thread.interrupt(); thread.interrupt();
@@ -171,7 +176,7 @@ public class StatsActivity extends Activity implements TrackDataListener {
utils.setReportSpeed(displaySpeed); utils.setReportSpeed(displaySpeed);
updateLabels(); updateLabels();
return true; // Reload data return true; // Reload data
} }
@@ -296,6 +301,7 @@ public class StatsActivity extends Activity implements TrackDataListener {
@Override @Override
public void onTrackUpdated(final Track track) { public void onTrackUpdated(final Track track) {
final boolean recordingSelected = dataHub.isRecordingSelected();
runOnUiThread(new Runnable() { runOnUiThread(new Runnable() {
@Override @Override
public void run() { public void run() {
@@ -305,7 +311,7 @@ public class StatsActivity extends Activity implements TrackDataListener {
} }
startTime = track.getStatistics().getStartTime(); startTime = track.getStatistics().getStartTime();
if (!dataHub.isRecordingSelected()) { if (!recordingSelected) {
utils.setTime(R.id.total_time_register, utils.setTime(R.id.total_time_register,
track.getStatistics().getTotalTime()); track.getStatistics().getTotalTime());
showUnknownLocation(); showUnknownLocation();
@@ -1,12 +1,12 @@
/* /*
* Copyright 2008 Google Inc. * Copyright 2008 Google Inc.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may not * 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 * use this file except in compliance with the License. You may obtain a copy of
* the License at * the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
@@ -16,6 +16,10 @@
package com.google.android.apps.mytracks; package com.google.android.apps.mytracks;
import com.google.android.apps.mytracks.content.TracksColumns; 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.StringUtils;
import com.google.android.apps.mytracks.util.UnitConversions; import com.google.android.apps.mytracks.util.UnitConversions;
import com.google.android.maps.mytracks.R; import com.google.android.maps.mytracks.R;
@@ -76,8 +80,7 @@ public class TrackList extends ListActivity
R.string.tracklist_show_track); R.string.tracklist_show_track);
menu.add(0, Constants.MENU_EDIT, 0, menu.add(0, Constants.MENU_EDIT, 0,
R.string.tracklist_edit_track); R.string.tracklist_edit_track);
if (!MyTracks.getInstance().isRecording() if (!isRecording() || trackId != recordingTrackId) {
|| trackId != recordingTrackId) {
menu.add(0, Constants.MENU_SEND_TO_GOOGLE, 0, menu.add(0, Constants.MENU_SEND_TO_GOOGLE, 0,
R.string.tracklist_send_to_google); R.string.tracklist_send_to_google);
SubMenu share = menu.addSubMenu(0, Constants.MENU_SHARE, 0, 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 @Override
public void onSharedPreferenceChanged( public void onSharedPreferenceChanged(
SharedPreferences sharedPreferences, String key) { SharedPreferences sharedPreferences, String key) {
@@ -138,32 +151,46 @@ public class TrackList extends ListActivity
@Override @Override
public boolean onMenuItemSelected(int featureId, MenuItem item) { public boolean onMenuItemSelected(int featureId, MenuItem item) {
if (!super.onMenuItemSelected(featureId, item)) { switch (item.getItemId()) {
switch (item.getItemId()) { case Constants.MENU_SHOW: {
case Constants.MENU_SHOW: { onListItemClick(null, null, 0, trackId);
onListItemClick(null, null, 0, trackId); return true;
return true; }
} case Constants.MENU_EDIT: {
case Constants.MENU_EDIT: { Intent intent = new Intent(this, TrackDetails.class);
Intent intent = new Intent(this, TrackDetails.class); intent.putExtra("trackid", trackId);
intent.putExtra("trackid", trackId); startActivity(intent);
startActivity(intent); return true;
return true; }
} case Constants.MENU_SHARE:
case Constants.MENU_SHARE: case Constants.MENU_WRITE_TO_SD_CARD:
case Constants.MENU_WRITE_TO_SD_CARD: return false;
return false; case Constants.MENU_SEND_TO_GOOGLE:
default: { SendActivity.sendToGoogle(this, trackId, false);
Intent result = new Intent(); return true;
result.putExtra("trackid", trackId); case Constants.MENU_SHARE_LINK:
setResult( SendActivity.sendToGoogle(this, trackId, true);
Constants.getActionFromMenuId(item.getItemId()), result); return true;
finish(); case Constants.MENU_SAVE_GPX_FILE:
return true; 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 @Override
@@ -197,17 +224,19 @@ public class TrackList extends ListActivity
listView = getListView(); listView = getListView();
listView.setOnCreateContextMenuListener(contextMenuListener); listView.setOnCreateContextMenuListener(contextMenuListener);
preferences = getSharedPreferences(Constants.SETTINGS_NAME, 0);
serviceConnection = new TrackRecordingServiceConnection(this, serviceBindingChanged);
View deleteAll = findViewById(R.id.tracklist_btn_delete_all); 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.setOnClickListener(this);
deleteAll.setEnabled(notRecording);
View exportAll = findViewById(R.id.tracklist_btn_export_all);
exportAll.setOnClickListener(this); exportAll.setOnClickListener(this);
exportAll.setEnabled(notRecording);
updateButtonsEnabled();
findViewById(R.id.tracklist_btn_import_all).setOnClickListener(this); findViewById(R.id.tracklist_btn_import_all).setOnClickListener(this);
SharedPreferences preferences =
getSharedPreferences(Constants.SETTINGS_NAME, 0);
preferences.registerOnSharedPreferenceChangeListener(this); preferences.registerOnSharedPreferenceChangeListener(this);
metricUnits = metricUnits =
preferences.getBoolean(getString(R.string.metric_units_key), true); preferences.getBoolean(getString(R.string.metric_units_key), true);
@@ -220,6 +249,29 @@ public class TrackList extends ListActivity
setListAdapter(); 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() { private void setListAdapter() {
// Get a cursor with all tracks // Get a cursor with all tracks
SimpleCursorAdapter adapter = new SimpleCursorAdapter( SimpleCursorAdapter adapter = new SimpleCursorAdapter(
@@ -283,4 +335,8 @@ public class TrackList extends ListActivity
}); });
setListAdapter(adapter); setListAdapter(adapter);
} }
private boolean isRecording() {
return ServiceUtils.isRecording(TrackList.this, serviceConnection.getServiceIfBound(), preferences);
}
} }
@@ -15,10 +15,14 @@
*/ */
package com.google.android.apps.mytracks; 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.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.Waypoint; import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.content.WaypointCreationRequest; import com.google.android.apps.mytracks.content.WaypointCreationRequest;
import com.google.android.apps.mytracks.content.WaypointsColumns; 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.apps.mytracks.util.StringUtils;
import com.google.android.maps.mytracks.R; import com.google.android.maps.mytracks.R;
@@ -43,6 +47,7 @@ import android.widget.ImageView;
import android.widget.ListView; import android.widget.ListView;
import android.widget.SimpleCursorAdapter; import android.widget.SimpleCursorAdapter;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast;
/** /**
* Activity which shows the list of waypoints in a track. * Activity which shows the list of waypoints in a track.
@@ -60,6 +65,7 @@ public class WaypointsList extends ListActivity
private Button insertStatisticsButton = null; private Button insertStatisticsButton = null;
private long recordingTrackId = -1; private long recordingTrackId = -1;
private MyTracksProviderUtils providerUtils; private MyTracksProviderUtils providerUtils;
private TrackRecordingServiceConnection serviceConnection;
private Cursor waypointsCursor = null; private Cursor waypointsCursor = null;
@@ -125,6 +131,7 @@ public class WaypointsList extends ListActivity
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
providerUtils = MyTracksProviderUtils.Factory.get(this); providerUtils = MyTracksProviderUtils.Factory.get(this);
serviceConnection = new TrackRecordingServiceConnection(this, null);
// We don't need a window title bar: // We don't need a window title bar:
requestWindowFeature(Window.FEATURE_NO_TITLE); requestWindowFeature(Window.FEATURE_NO_TITLE);
@@ -171,6 +178,20 @@ public class WaypointsList extends ListActivity
setListAdapter(); setListAdapter();
} }
@Override
protected void onResume() {
super.onResume();
serviceConnection.bindIfRunning();
}
@Override
protected void onDestroy() {
serviceConnection.unbind();
super.onDestroy();
}
@Override @Override
public void onClick(View v) { public void onClick(View v) {
WaypointCreationRequest request; WaypointCreationRequest request;
@@ -184,17 +205,10 @@ public class WaypointsList extends ListActivity
default: default:
return; return;
} }
long id; long id = insertWaypoint(request);
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;
}
if (id < 0) { 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."); Log.e(Constants.TAG, "Failed to insert marker.");
return; return;
} }
@@ -203,6 +217,28 @@ public class WaypointsList extends ListActivity
startActivity(intent); 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() { private void setListAdapter() {
// Get a cursor with all tracks // Get a cursor with all tracks
SimpleCursorAdapter adapter = new SimpleCursorAdapter( SimpleCursorAdapter adapter = new SimpleCursorAdapter(
@@ -1,12 +1,12 @@
/* /*
* Copyright 2008 Google Inc. * Copyright 2008 Google Inc.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may not * 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 * use this file except in compliance with the License. You may obtain a copy of
* the License at * the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
@@ -154,12 +154,12 @@ public class MyTracksProvider extends ContentProvider {
// Sensor data. // Sensor data.
if (oldVersion <= 17) { if (oldVersion <= 17) {
Log.w(TAG, "Upgrade DB: Adding sensor column."); Log.w(TAG, "Upgrade DB: Adding sensor column.");
db.execSQL("ALTER TABLE " + TRACKPOINTS_TABLE db.execSQL("ALTER TABLE " + TRACKPOINTS_TABLE
+ " ADD " + TrackPointsColumns.SENSOR + " BLOB"); + " ADD " + TrackPointsColumns.SENSOR + " BLOB");
} }
if (oldVersion <= 18) { if (oldVersion <= 18) {
Log.w(TAG, "Upgrade DB: Adding tableid column."); Log.w(TAG, "Upgrade DB: Adding tableid column.");
db.execSQL("ALTER TABLE " + TRACKS_TABLE db.execSQL("ALTER TABLE " + TRACKS_TABLE
+ " ADD " + TracksColumns.TABLEID + " STRING"); + " ADD " + TracksColumns.TABLEID + " STRING");
} }
} }
@@ -186,7 +186,11 @@ public class MyTracksProvider extends ContentProvider {
@Override @Override
public boolean onCreate() { public boolean onCreate() {
DatabaseHelper dbHelper = new DatabaseHelper(getContext()); DatabaseHelper dbHelper = new DatabaseHelper(getContext());
db = dbHelper.getWritableDatabase(); try {
db = dbHelper.getWritableDatabase();
} catch (SQLiteException e) {
Log.e(TAG, "Unable to open database for writing", e);
}
return db != null; return db != null;
} }
@@ -383,7 +387,7 @@ public class MyTracksProvider extends ContentProvider {
if (ApiFeatures.getInstance().canReuseSQLiteQueryBuilder()) { if (ApiFeatures.getInstance().canReuseSQLiteQueryBuilder()) {
Log.i(Constants.TAG, Log.i(Constants.TAG,
"Build query: " + qb.buildQuery(projection, selection, selectionArgs, "Build query: " + qb.buildQuery(projection, selection, selectionArgs,
null, null, sortOrder, null)); null, null, sortOrder, null));
} }
Cursor c = qb.query(db, projection, selection, selectionArgs, null, null, Cursor c = qb.query(db, projection, selection, selectionArgs, null, null,
@@ -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.TAG;
import static com.google.android.apps.mytracks.Constants.TARGET_DISPLAYED_TRACK_POINTS; 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.DataSourceManager.DataSourceListener;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils.DoubleBufferedLocationFactory; import com.google.android.apps.mytracks.content.MyTracksProviderUtils.DoubleBufferedLocationFactory;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils.LocationIterator; 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. // Get content notifications on the main thread, send listener callbacks in another.
// This ensures listener calls are serialized. // This ensures listener calls are serialized.
private final HandlerThread listenerHandlerThread; private HandlerThread listenerHandlerThread;
private final Handler listenerHandler; private Handler listenerHandler;
/** Manager for external listeners (those from activities). */ /** Manager for external listeners (those from activities). */
private final TrackDataListeners listeners; private final TrackDataListeners listeners;
/** Wrapper for interacting with system data managers. */ /** Wrapper for interacting with system data managers. */
private final DataSourcesWrapper dataSources; private DataSourcesWrapper dataSources;
/** Manager for system data listener registrations. */ /** Manager for system data listener registrations. */
private final DataSourceManager dataSourceManager; private DataSourceManager dataSourceManager;
/** Condensed listener for system data listener events. */ /** Condensed listener for system data listener events. */
private final DataSourceListener dataSourceListener = new HubDataSourceListener(); private final DataSourceListener dataSourceListener = new HubDataSourceListener();
/** Whether we've been started. */
private boolean started;
// Cached preference values // Cached preference values
private int minRequiredAccuracy; private int minRequiredAccuracy;
private boolean useMetricUnits; private boolean useMetricUnits;
@@ -191,28 +189,34 @@ public class TrackDataHub {
private int lastSamplingFrequency; private int lastSamplingFrequency;
private DoubleBufferedLocationFactory locationFactory; private DoubleBufferedLocationFactory locationFactory;
private static TrackDataHub startedInstance;
/** /**
* Default constructor. * Builds a new {@link TrackDataHub} instance.
*/ */
public TrackDataHub(Context ctx, SharedPreferences preferences, public synchronized static TrackDataHub newInstance(Context context) {
MyTracksProviderUtils providerUtils) { // Ensure our singleton is never bound to an activity, to avoid memory leaks.
this(ctx, new DataSourcesWrapperImpl(ctx, preferences), new TrackDataListeners(), context = context.getApplicationContext();
preferences, providerUtils, TARGET_DISPLAYED_TRACK_POINTS);
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. * Injection constructor.
*/ */
// @VisibleForTesting // @VisibleForTesting
TrackDataHub(Context ctx, DataSourcesWrapper dataSources, TrackDataListeners listeners, TrackDataHub(Context ctx, TrackDataListeners listeners, SharedPreferences preferences,
SharedPreferences preferences, MyTracksProviderUtils providerUtils, int targetNumPoints) { MyTracksProviderUtils providerUtils, int targetNumPoints) {
this.context = ctx; this.context = ctx;
this.listeners = listeners; this.listeners = listeners;
this.preferences = preferences; this.preferences = preferences;
this.providerUtils = providerUtils; this.providerUtils = providerUtils;
this.dataSources = dataSources;
this.targetNumPoints = targetNumPoints; this.targetNumPoints = targetNumPoints;
this.dataSourceManager = new DataSourceManager(dataSourceListener, dataSources);
this.locationFactory = new DoubleBufferedLocationFactory(); this.locationFactory = new DoubleBufferedLocationFactory();
SELECTED_TRACK_KEY = context.getString(R.string.selected_track_key); 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); METRIC_UNITS_KEY = context.getString(R.string.metric_units_key);
SPEED_REPORTING_KEY = context.getString(R.string.report_speed_key); SPEED_REPORTING_KEY = context.getString(R.string.report_speed_key);
listenerHandlerThread = new HandlerThread("trackDataContentThread");
listenerHandlerThread.start();
listenerHandler = new Handler(listenerHandlerThread.getLooper());
resetState(); resetState();
} }
@@ -234,11 +234,17 @@ public class TrackDataHub {
*/ */
public void start() { public void start() {
Log.i(TAG, "TrackDataHub.start"); Log.i(TAG, "TrackDataHub.start");
if (started) { if (startedInstance != null) {
Log.w(TAG, "Already started, ignoring"); Log.w(TAG, "Already started, ignoring");
return; 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 // This may or may not register internal listeners, depending on whether
// we already had external listeners. // we already had external listeners.
@@ -249,12 +255,23 @@ public class TrackDataHub {
loadDataForAllListeners(); loadDataForAllListeners();
} }
private void loadSharedPreferences() { // @VisibleForTesting
selectedTrackId = preferences.getLong(SELECTED_TRACK_KEY, -1); protected DataSourcesWrapper newDataSources() {
useMetricUnits = preferences.getBoolean(METRIC_UNITS_KEY, true); return new DataSourcesWrapperImpl(context, preferences);
reportSpeed = preferences.getBoolean(SPEED_REPORTING_KEY, true); }
minRequiredAccuracy = preferences.getInt(MIN_REQUIRED_ACCURACY_KEY,
DEFAULT_MIN_REQUIRED_ACCURACY); /**
* 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() { public void stop() {
Log.i(TAG, "TrackDataHub.stop"); Log.i(TAG, "TrackDataHub.stop");
if (!started) { if (!isStarted()) {
Log.w(TAG, "Not started, ignoring"); Log.w(TAG, "Not started, ignoring");
return; return;
} }
// Unregister internal listeners even if there are external listeners registered. // Unregister internal listeners even if there are external listeners registered.
dataSourceManager.unregisterAllListeners(); 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. */ private boolean isStarted() {
public void destroy() { return startedInstance != null;
if (started) { }
throw new IllegalStateException("Can only destroy the data hub after it's been stopped");
@Override
protected void finalize() throws Throwable {
if (isStarted() || listenerHandlerThread.isAlive()) {
Log.e(TAG, "Forgot to stop() TrackDataHub");
} }
ApiFeatures.getInstance().getApiPlatformAdapter() super.finalize();
.stopHandlerThread(listenerHandlerThread); }
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. */ /** Updates known magnetic declination if needed. */
@@ -324,6 +358,10 @@ public class TrackDataHub {
* is not available or doesn't have a fix. * is not available or doesn't have a fix.
*/ */
public void forceUpdateLocation() { public void forceUpdateLocation() {
if (!isStarted()) {
Log.w(TAG, "Not started, not forcing location update");
return;
}
Log.i(TAG, "Forcing location update"); Log.i(TAG, "Forcing location update");
Location loc = dataSources.getLastKnownLocation(); Location loc = dataSources.getLastKnownLocation();
@@ -335,7 +373,7 @@ public class TrackDataHub {
/** Returns the ID of the currently-selected track. */ /** Returns the ID of the currently-selected track. */
public long getSelectedTrackId() { public long getSelectedTrackId() {
if (!started) { if (!isStarted()) {
loadSharedPreferences(); loadSharedPreferences();
} }
return selectedTrackId; return selectedTrackId;
@@ -346,17 +384,9 @@ public class TrackDataHub {
return getSelectedTrackId() > 0; 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. */ /** Returns whether the selected track is still being recorded. */
public boolean isRecordingSelected() { public boolean isRecordingSelected() {
if (!started) { if (!isStarted()) {
loadSharedPreferences(); loadSharedPreferences();
} }
long recordingTrackId = preferences.getLong(RECORDING_TRACK_KEY, -1); long recordingTrackId = preferences.getLong(RECORDING_TRACK_KEY, -1);
@@ -412,9 +442,9 @@ public class TrackDataHub {
// Don't load any data or start internal listeners if start() hasn't been // Don't load any data or start internal listeners if start() hasn't been
// called. When it is called, we'll do both things. // called. When it is called, we'll do both things.
if (!started) return; if (!isStarted()) return;
reloadDataForListener(registration); loadNewDataForListener(registration);
dataSourceManager.updateAllListeners(getNeededListenerTypes()); dataSourceManager.updateAllListeners(getNeededListenerTypes());
} }
@@ -426,7 +456,7 @@ public class TrackDataHub {
// Don't load any data or start internal listeners if start() hasn't been // Don't load any data or start internal listeners if start() hasn't been
// called. When it is called, we'll do both things. // called. When it is called, we'll do both things.
if (!started) return; if (!isStarted()) return;
dataSourceManager.updateAllListeners(getNeededListenerTypes()); dataSourceManager.updateAllListeners(getNeededListenerTypes());
} }
@@ -439,7 +469,8 @@ public class TrackDataHub {
ListenerRegistration registration; ListenerRegistration registration;
synchronized (listeners) { synchronized (listeners) {
registration = listeners.getRegistration(listener); registration = listeners.getRegistration(listener);
reloadDataForListener(registration); registration.resetState();
loadNewDataForListener(registration);
} }
} }
@@ -448,12 +479,13 @@ public class TrackDataHub {
* *
* Assumes it's called from a block that synchronizes on {@link #listeners}. * Assumes it's called from a block that synchronizes on {@link #listeners}.
*/ */
private void reloadDataForListener(final ListenerRegistration registration) { private void loadNewDataForListener(final ListenerRegistration registration) {
if (!started) { if (!isStarted()) {
Log.w(TAG, "Not started, not reloading"); Log.w(TAG, "Not started, not reloading");
return; return;
} }
if (registration == null) { if (registration == null) {
Log.w(TAG, "Not reloading for null registration");
return; return;
} }
@@ -541,7 +573,7 @@ public class TrackDataHub {
* Reloads all track data received so far into the specified listeners. * Reloads all track data received so far into the specified listeners.
*/ */
private void loadDataForAllListeners() { private void loadDataForAllListeners() {
if (!started) { if (!isStarted()) {
Log.w(TAG, "Not started, not reloading"); Log.w(TAG, "Not started, not reloading");
return; return;
} }
@@ -608,7 +640,7 @@ public class TrackDataHub {
/** Called when the speed/pace reporting preference changes. */ /** Called when the speed/pace reporting preference changes. */
private void notifySpeedReportingChanged() { private void notifySpeedReportingChanged() {
if (!started) return; if (!isStarted()) return;
runInListenerThread(new Runnable() { runInListenerThread(new Runnable() {
@Override @Override
@@ -620,7 +652,7 @@ public class TrackDataHub {
// TODO: Do the reloading just once for all interested listeners // TODO: Do the reloading just once for all interested listeners
if (listener.onReportSpeedChanged(reportSpeed)) { if (listener.onReportSpeedChanged(reportSpeed)) {
synchronized (listeners) { synchronized (listeners) {
reloadDataForListener(listeners.getRegistration(listener)); reloadDataForListener(listener);
} }
} }
} }
@@ -630,7 +662,7 @@ public class TrackDataHub {
/** Called when the metric units setting changes. */ /** Called when the metric units setting changes. */
private void notifyUnitsChanged() { private void notifyUnitsChanged() {
if (!started) return; if (!isStarted()) return;
runInListenerThread(new Runnable() { runInListenerThread(new Runnable() {
@Override @Override
@@ -640,7 +672,7 @@ public class TrackDataHub {
for (TrackDataListener listener : displayListeners) { for (TrackDataListener listener : displayListeners) {
if (listener.onUnitsChanged(useMetricUnits)) { if (listener.onUnitsChanged(useMetricUnits)) {
synchronized (listeners) { synchronized (listeners) {
reloadDataForListener(listeners.getRegistration(listener)); reloadDataForListener(listener);
} }
} }
} }
@@ -989,6 +1021,7 @@ public class TrackDataHub {
// no matter how many points we get in the newest batch, we'll never exceed // no matter how many points we get in the newest batch, we'll never exceed
// MAX_DISPLAYED_TRACK_POINTS = 2 * TARGET_DISPLAYED_TRACK_POINTS before resampling. // MAX_DISPLAYED_TRACK_POINTS = 2 * TARGET_DISPLAYED_TRACK_POINTS before resampling.
long numTotalPoints = lastStoredLocationId - localFirstSeenLocationId; long numTotalPoints = lastStoredLocationId - localFirstSeenLocationId;
numTotalPoints = Math.max(0L, numTotalPoints);
pointSamplingFrequency = (int) (1 + numTotalPoints / targetNumPoints); pointSamplingFrequency = (int) (1 + numTotalPoints / targetNumPoints);
} }
@@ -1063,6 +1096,12 @@ public class TrackDataHub {
// @VisibleForTesting // @VisibleForTesting
protected void runInListenerThread(Runnable runnable) { 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); listenerHandler.post(runnable);
} }
@@ -1,12 +1,12 @@
/* /*
* Copyright 2011 Google Inc. * Copyright 2011 Google Inc.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may not * 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 * use this file except in compliance with the License. You may obtain a copy of
* the License at * the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
@@ -57,6 +57,13 @@ class TrackDataListeners {
return types.contains(type); return types.contains(type);
} }
public void resetState() {
lastTrackId = 0L;
lastPointId = 0L;
lastSamplingFrequency = 0;
numLoadedPoints = 0;
}
@Override @Override
public String toString() { public String toString() {
return "ListenerRegistration [listener=" + listener + ", types=" + types return "ListenerRegistration [listener=" + listener + ", types=" + types
@@ -1,12 +1,12 @@
/* /*
* Copyright 2010 Google Inc. * Copyright 2010 Google Inc.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may not * 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 * use this file except in compliance with the License. You may obtain a copy of
* the License at * the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
@@ -20,11 +20,10 @@ import android.content.Intent;
/** /**
* This interface describes a class that will fetch and maintain a Google * This interface describes a class that will fetch and maintain a Google
* authentication token. * authentication token.
* *
* @author Sandor Dornbush * @author Sandor Dornbush
*/ */
public interface AuthManager { public interface AuthManager {
/** /**
* Initializes the login process. The user should be asked to login if they * Initializes the login process. The user should be asked to login if they
* haven't already. The {@link Runnable} provided will be executed when the * haven't already. The {@link Runnable} provided will be executed when the
@@ -34,7 +33,7 @@ public interface AuthManager {
* has been successfully fetched and is available via * has been successfully fetched and is available via
* {@link #getAuthToken()} * {@link #getAuthToken()}
*/ */
public abstract void doLogin(Runnable whenFinished, Object o); void doLogin(Runnable whenFinished, Object o);
/** /**
* The {@link android.app.Activity} owner of this class should call this * The {@link android.app.Activity} owner of this class should call this
@@ -53,7 +52,7 @@ public interface AuthManager {
* the auth token, or False if there was an error or the request was * the auth token, or False if there was an error or the request was
* canceled * canceled
*/ */
public abstract boolean authResult(int resultCode, Intent results); boolean authResult(int resultCode, Intent results);
/** /**
* Returns the current auth token. Response may be null if no valid auth * Returns the current auth token. Response may be null if no valid auth
@@ -62,7 +61,7 @@ public interface AuthManager {
* @return The current auth token or null if no auth token has been * @return The current auth token or null if no auth token has been
* fetched * fetched
*/ */
public abstract String getAuthToken(); String getAuthToken();
/** /**
* Invalidates the existing auth token and request a new one. The * Invalidates the existing auth token and request a new one. The
@@ -72,6 +71,5 @@ public interface AuthManager {
* @param whenFinished A {@link Runnable} to execute when a new auth token * @param whenFinished A {@link Runnable} to execute when a new auth token
* is successfully fetched * is successfully fetched
*/ */
public abstract void invalidateAndRefresh(Runnable whenFinished); void invalidateAndRefresh(Runnable whenFinished);
} }
@@ -22,7 +22,6 @@ import com.google.android.accounts.AccountManagerFuture;
import com.google.android.accounts.AuthenticatorException; import com.google.android.accounts.AuthenticatorException;
import com.google.android.accounts.OperationCanceledException; import com.google.android.accounts.OperationCanceledException;
import com.google.android.apps.mytracks.AccountChooser; import com.google.android.apps.mytracks.AccountChooser;
import com.google.android.apps.mytracks.MyTracks;
import com.google.android.apps.mytracks.Constants; import com.google.android.apps.mytracks.Constants;
import android.app.Activity; import android.app.Activity;
@@ -183,10 +182,12 @@ public class ModernAuthManager implements AuthManager {
public void run() { public void run() {
accountManager.invalidateAuthToken(Constants.ACCOUNT_TYPE, accountManager.invalidateAuthToken(Constants.ACCOUNT_TYPE,
authToken); authToken);
MyTracks.getInstance().getAccountChooser().chooseAccount(activity,
AccountChooser accountChooser = new AccountChooser();
accountChooser.chooseAccount(activity,
new AccountChooser.AccountHandler() { new AccountChooser.AccountHandler() {
@Override @Override
public void handleAccountSelected(Account account) { public void onAccountSelected(Account account) {
if (account != null) { if (account != null) {
doLogin(whenFinished, account); doLogin(whenFinished, account);
} else { } else {
@@ -15,8 +15,8 @@
*/ */
package com.google.android.apps.mytracks.io; 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.Constants;
import com.google.android.apps.mytracks.ProgressIndicator;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils; import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.Track; import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.io.docs.DocsHelper; import com.google.android.apps.mytracks.io.docs.DocsHelper;
@@ -25,15 +25,13 @@ import com.google.android.apps.mytracks.io.gdata.GDataWrapper;
import com.google.android.common.gdata.AndroidXmlParserFactory; import com.google.android.common.gdata.AndroidXmlParserFactory;
import com.google.android.maps.mytracks.R; import com.google.android.maps.mytracks.R;
import com.google.wireless.gdata.client.GDataClient; 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.DocumentsClient;
import com.google.wireless.gdata.docs.SpreadsheetsClient; import com.google.wireless.gdata.docs.SpreadsheetsClient;
import com.google.wireless.gdata.docs.XmlDocsGDataParserFactory; import com.google.wireless.gdata.docs.XmlDocsGDataParserFactory;
import com.google.wireless.gdata.client.GDataServiceClient;
import android.app.Activity; import android.app.Activity;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.os.Handler;
import android.os.HandlerThread;
import android.util.Log; import android.util.Log;
import java.io.IOException; import java.io.IOException;
@@ -46,17 +44,15 @@ import java.io.IOException;
public class SendToDocs { public class SendToDocs {
/** The GData service name for Google Spreadsheets (aka Trix) */ /** The GData service name for Google Spreadsheets (aka Trix) */
public static final String GDATA_SERVICE_NAME_TRIX = "wise"; public static final String GDATA_SERVICE_NAME_TRIX = "wise";
/** The GData service name for the Google Docs Document List */ /** The GData service name for the Google Docs Document List */
public static final String GDATA_SERVICE_NAME_DOCLIST = "writely"; public static final String GDATA_SERVICE_NAME_DOCLIST = "writely";
private final Activity activity; private final Activity activity;
private final AuthManager trixAuth; private final AuthManager trixAuth;
private final AuthManager docListAuth; private final AuthManager docListAuth;
private final long trackId; private final ProgressIndicator progressIndicator;
private final boolean metricUnits; private final boolean metricUnits;
private final HandlerThread handlerThread;
private final Handler handler;
private boolean createdNewSpreadSheet = false; private boolean createdNewSpreadSheet = false;
@@ -64,12 +60,13 @@ public class SendToDocs {
private String statusMessage = ""; private String statusMessage = "";
private Runnable onCompletion = null; private Runnable onCompletion = null;
public SendToDocs(Activity activity, AuthManager trixAuth, public SendToDocs(Activity activity, AuthManager trixAuth,
AuthManager docListAuth, long trackId) { AuthManager docListAuth, ProgressIndicator progressIndicator) {
this.activity = activity; this.activity = activity;
this.trixAuth = trixAuth; this.trixAuth = trixAuth;
this.docListAuth = docListAuth; this.docListAuth = docListAuth;
this.trackId = trackId; this.progressIndicator = progressIndicator;
SharedPreferences preferences = activity.getSharedPreferences( SharedPreferences preferences = activity.getSharedPreferences(
Constants.SETTINGS_NAME, 0); Constants.SETTINGS_NAME, 0);
@@ -80,24 +77,20 @@ public class SendToDocs {
} else { } else {
metricUnits = true; metricUnits = true;
} }
}
public void sendToDocs(final long trackId) {
Log.d(Constants.TAG, Log.d(Constants.TAG,
"Sending to Google Docs: trackId = " + trackId); "Sending to Google Docs: trackId = " + trackId);
handlerThread = new HandlerThread("SendToGoogleDocs"); new Thread("SendToGoogleDocs") {
handlerThread.start();
handler = new Handler(handlerThread.getLooper());
}
public void run() {
handler.post(new Runnable() {
@Override @Override
public void run() { public void run() {
doUpload(); doUpload(trackId);
} }
}); }.start();
} }
private void doUpload() { private void doUpload(long trackId) {
// TODO // TODO
statusMessage = activity.getString(R.string.error_sending_to_fusiontables); statusMessage = activity.getString(R.string.error_sending_to_fusiontables);
success = false; success = false;
@@ -162,7 +155,7 @@ public class SendToDocs {
GDataWrapper<GDataServiceClient> docListWrapper = new GDataWrapper<GDataServiceClient>(); GDataWrapper<GDataServiceClient> docListWrapper = new GDataWrapper<GDataServiceClient>();
docListWrapper.setAuthManager(docListAuth); docListWrapper.setAuthManager(docListAuth);
docListWrapper.setRetryOnAuthFailure(true); docListWrapper.setRetryOnAuthFailure(true);
GDataWrapper<GDataServiceClient> trixWrapper = new GDataWrapper<GDataServiceClient>(); GDataWrapper<GDataServiceClient> trixWrapper = new GDataWrapper<GDataServiceClient>();
trixWrapper.setAuthManager(trixAuth); trixWrapper.setAuthManager(trixAuth);
trixWrapper.setRetryOnAuthFailure(true); trixWrapper.setRetryOnAuthFailure(true);
@@ -190,7 +183,7 @@ public class SendToDocs {
// First try to find the spreadsheet: // First try to find the spreadsheet:
String spreadsheetId = null; String spreadsheetId = null;
try { try {
spreadsheetId = docsHelper.requestSpreadsheetId(docListWrapper, spreadsheetId = docsHelper.requestSpreadsheetId(docListWrapper,
sheetTitle); sheetTitle);
} catch (IOException e) { } catch (IOException e) {
Log.i(Constants.TAG, "Spreadsheet lookup failed.", e); Log.i(Constants.TAG, "Spreadsheet lookup failed.", e);
@@ -198,7 +191,7 @@ public class SendToDocs {
} }
if (spreadsheetId == null) { if (spreadsheetId == null) {
MyTracks.getInstance().setProgressValue(65); progressIndicator.setProgressValue(65);
// Waiting a few seconds and trying again. Maybe the server just had a // Waiting a few seconds and trying again. Maybe the server just had a
// hickup (unfortunately that happens quite a lot...). // hickup (unfortunately that happens quite a lot...).
try { try {
@@ -206,18 +199,18 @@ public class SendToDocs {
} catch (InterruptedException e) { } catch (InterruptedException e) {
Log.e(Constants.TAG, "Sleep interrupted", e); Log.e(Constants.TAG, "Sleep interrupted", e);
} }
try { try {
spreadsheetId = docsHelper.requestSpreadsheetId(docListWrapper, spreadsheetId = docsHelper.requestSpreadsheetId(docListWrapper,
sheetTitle); sheetTitle);
} catch (IOException e) { } catch (IOException e) {
Log.i(Constants.TAG, "2nd spreadsheet lookup failed.", e); Log.i(Constants.TAG, "2nd spreadsheet lookup failed.", e);
return false; return false;
} }
} }
// We were unable to find an existing spreadsheet, so create a new one. // We were unable to find an existing spreadsheet, so create a new one.
MyTracks.getInstance().setProgressValue(70); progressIndicator.setProgressValue(70);
if (spreadsheetId == null) { if (spreadsheetId == null) {
Log.i(Constants.TAG, "Creating new spreadsheet: " + sheetTitle); Log.i(Constants.TAG, "Creating new spreadsheet: " + sheetTitle);
@@ -229,11 +222,11 @@ public class SendToDocs {
+ sheetTitle, e); + sheetTitle, e);
return false; return false;
} }
MyTracks.getInstance().setProgressValue(80); progressIndicator.setProgressValue(80);
createdNewSpreadSheet = true; createdNewSpreadSheet = true;
if (spreadsheetId == null) { if (spreadsheetId == null) {
MyTracks.getInstance().setProgressValue(85); progressIndicator.setProgressValue(85);
// The previous creation might have succeeded even though GData // The previous creation might have succeeded even though GData
// reported an error. Seems to be a know bug, // reported an error. Seems to be a know bug,
// see http://code.google.com/p/gdata-issues/issues/detail?id=929 // see http://code.google.com/p/gdata-issues/issues/detail?id=929
@@ -247,7 +240,7 @@ public class SendToDocs {
} }
try { try {
spreadsheetId = docsHelper.requestSpreadsheetId(docListWrapper, spreadsheetId = docsHelper.requestSpreadsheetId(docListWrapper,
sheetTitle); sheetTitle);
} catch (IOException e) { } catch (IOException e) {
Log.i(Constants.TAG, "Failed create-failed lookup", e); Log.i(Constants.TAG, "Failed create-failed lookup", e);
@@ -255,16 +248,16 @@ public class SendToDocs {
} }
if (spreadsheetId == null) { if (spreadsheetId == null) {
MyTracks.getInstance().setProgressValue(87); progressIndicator.setProgressValue(87);
// Re-try // Re-try
try { try {
Thread.sleep(5000); Thread.sleep(5000);
} catch (InterruptedException e) { } catch (InterruptedException e) {
Log.e(Constants.TAG, "Sleep interrupted", e); Log.e(Constants.TAG, "Sleep interrupted", e);
} }
try { try {
spreadsheetId = docsHelper.requestSpreadsheetId(docListWrapper, spreadsheetId = docsHelper.requestSpreadsheetId(docListWrapper,
sheetTitle); sheetTitle);
} catch (IOException e) { } catch (IOException e) {
Log.i(Constants.TAG, "Failed create-failed relookup", e); Log.i(Constants.TAG, "Failed create-failed relookup", e);
@@ -290,9 +283,9 @@ public class SendToDocs {
return false; return false;
} }
MyTracks.getInstance().setProgressValue(90); progressIndicator.setProgressValue(90);
docsHelper.addTrackRow(activity, trixAuth, spreadsheetId, worksheetId, docsHelper.addTrackRow(activity, trixAuth, spreadsheetId, worksheetId,
track, metricUnits); track, metricUnits);
Log.i(Constants.TAG, "Done uploading to docs."); Log.i(Constants.TAG, "Done uploading to docs.");
} catch (IOException e) { } catch (IOException e) {
@@ -15,6 +15,7 @@
*/ */
package com.google.android.apps.mytracks.io; package com.google.android.apps.mytracks.io;
import static com.google.android.apps.mytracks.Constants.TAG;
import com.google.android.apps.mytracks.Constants; import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.ProgressIndicator; import com.google.android.apps.mytracks.ProgressIndicator;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils; import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
@@ -49,6 +50,7 @@ import android.util.Log;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
@@ -156,6 +158,11 @@ public class SendToFusionTables implements Runnable {
} }
public static String getMapVisualizationUrl(Track track) { public static String getMapVisualizationUrl(Track track) {
if (track == null || track.getStatistics() == null || track.getTableId() == null) {
Log.w(TAG, "Unable to get track URL");
return null;
}
// TODO(leifhendrik): Determine correct bounding box and zoom level that will show the entire track. // TODO(leifhendrik): Determine correct bounding box and zoom level that will show the entire track.
TripStatistics stats = track.getStatistics(); TripStatistics stats = track.getStatistics();
double latE6 = stats.getBottom() + (stats.getTop() - stats.getBottom()) / 2; double latE6 = stats.getBottom() + (stats.getTop() - stats.getBottom()) / 2;
@@ -426,10 +433,10 @@ public class SendToFusionTables implements Runnable {
* @return the kml. * @return the kml.
*/ */
private String getKmlPoint(Location location) { private String getKmlPoint(Location location) {
StringBuilder builder = new StringBuilder("<Point><coordinates>"); StringBuilder builder = new StringBuilder("<Point><coordinates>");
appendCoordinate(location, builder); appendCoordinate(location, builder);
builder.append("</coordinates></Point>"); builder.append("</coordinates></Point>");
return builder.toString(); return builder.toString();
} }
/** /**
@@ -605,7 +612,7 @@ public class SendToFusionTables implements Runnable {
// I am leaving the number of waypoints very high which should not be a // 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 // problem because we don't try to load them into objects all at the
// same time. // same time.
boolean success = true; boolean success = true;
Cursor c = null; Cursor c = null;
try { try {
c = providerUtils.getWaypointsCursor( c = providerUtils.getWaypointsCursor(
@@ -619,9 +626,9 @@ public class SendToFusionTables implements Runnable {
Waypoint wpt = providerUtils.createWaypoint(c); Waypoint wpt = providerUtils.createWaypoint(c);
Log.d(Constants.TAG, "SendToFusionTables: Creating waypoint."); Log.d(Constants.TAG, "SendToFusionTables: Creating waypoint.");
success = createNewPoint(wpt.getName(), wpt.getDescription(), wpt.getLocation(), success = createNewPoint(wpt.getName(), wpt.getDescription(), wpt.getLocation(),
MARKER_TYPE_WAYPOINT); MARKER_TYPE_WAYPOINT);
if (!success) { if (!success) {
break; break;
} }
} }
} }
@@ -643,7 +650,7 @@ public class SendToFusionTables implements Runnable {
// to fit the completion percentage range alloted to track data upload. // to fit the completion percentage range alloted to track data upload.
double totalPercentage = double totalPercentage =
(totalLocationsRead + totalLocationsPrepared + totalLocationsUploaded) (totalLocationsRead + totalLocationsPrepared + totalLocationsUploaded)
/ (totalLocations * 3); / (totalLocations * 3.0);
double scaledPercentage = totalPercentage double scaledPercentage = totalPercentage
* (PROGRESS_UPLOAD_DATA_MAX - PROGRESS_UPLOAD_DATA_MIN) + PROGRESS_UPLOAD_DATA_MIN; * (PROGRESS_UPLOAD_DATA_MAX - PROGRESS_UPLOAD_DATA_MIN) + PROGRESS_UPLOAD_DATA_MIN;
@@ -687,8 +694,8 @@ public class SendToFusionTables implements Runnable {
boolean success = response.isSuccessStatusCode; boolean success = response.isSuccessStatusCode;
if (success) { if (success) {
byte[] result = new byte[1024]; byte[] result = new byte[1024];
response.getContent().read(result); int read = response.getContent().read(result);
String s = Strings.fromBytesUtf8(result); String s = new String(result, 0, read, Charset.forName("UTF8"));
String[] lines = s.split(Strings.LINE_SEPARATOR); String[] lines = s.split(Strings.LINE_SEPARATOR);
if (lines[0].equals("tableid")) { if (lines[0].equals("tableid")) {
tableId = lines[1]; tableId = lines[1];
@@ -100,7 +100,6 @@ public class SendToMyMaps implements Runnable {
int statusMessageId = R.string.error_sending_to_mymap; int statusMessageId = R.string.error_sending_to_mymap;
boolean success = true; boolean success = true;
try { try {
progressIndicator.setProgressValue(1);
progressIndicator.setProgressMessage( progressIndicator.setProgressMessage(
R.string.progress_message_reading_track); R.string.progress_message_reading_track);
@@ -454,7 +453,7 @@ public class SendToMyMaps implements Runnable {
// The percent of the total that represents the completed part of this // The percent of the total that represents the completed part of this
// segment. // segment.
int totalPercentage = int totalPercentage =
(totalLocationsRead + totalLocationsPrepared + totalLocationsUploaded) (totalLocationsRead + totalLocationsPrepared + totalLocationsUploaded) * 100
/ (totalLocations * 3); / (totalLocations * 3);
totalPercentage = Math.min(99, totalPercentage); totalPercentage = Math.min(99, totalPercentage);
progressIndicator.setProgressValue(totalPercentage); progressIndicator.setProgressValue(totalPercentage);
@@ -202,8 +202,6 @@ public class BackupActivityHelper {
Log.e(Constants.TAG, "Failed to restore backup", e); Log.e(Constants.TAG, "Failed to restore backup", e);
showToast(R.string.io_read_failed); showToast(R.string.io_read_failed);
} finally { } finally {
dismissDialog(progressDialog);
// Data may have been restored, "reboot" the app to catch it // Data may have been restored, "reboot" the app to catch it
restartApplication(); restartApplication();
} }
@@ -15,6 +15,8 @@
*/ */
package com.google.android.apps.mytracks.io.backup; 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.Constants;
import com.google.android.apps.mytracks.content.TrackPointsColumns; import com.google.android.apps.mytracks.content.TrackPointsColumns;
import com.google.android.apps.mytracks.content.TracksColumns; import com.google.android.apps.mytracks.content.TracksColumns;
@@ -205,7 +207,9 @@ class ExternalFileBackup {
} catch (IOException e) { } catch (IOException e) {
// We tried to delete the partially created file, but do nothing // We tried to delete the partially created file, but do nothing
// if that also fails. // if that also fails.
outputFile.delete(); if (!outputFile.delete()) {
Log.w(TAG, "Failed to delete file " + outputFile.getAbsolutePath());
}
throw e; throw e;
} finally { } finally {
@@ -0,0 +1,216 @@
/*
* 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.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.Dialog;
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 static final int PROGRESS_DIALOG = 1;
private static final int SUCCESS_DIALOG = 2;
private static final int FAILURE_DIALOG = 3;
private MyTracksProviderUtils providerUtils;
private ProgressDialog progressDialog;
private int resultMessage;
private long importedTrackIds[];
@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());
}
@Override
protected Dialog onCreateDialog(int id) {
switch (id) {
case PROGRESS_DIALOG:
progressDialog = new ProgressDialog(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));
return progressDialog;
case SUCCESS_DIALOG:
final Builder dialogBuilder = new AlertDialog.Builder(this);
dialogBuilder.setCancelable(true);
dialogBuilder.setPositiveButton(android.R.string.ok, new OnClickListener() {
@Override
public void onClick(DialogInterface arg0, int arg1) {
finish();
}
});
dialogBuilder.setNeutralButton(R.string.import_show_track, new OnClickListener() {
@Override
public void onClick(DialogInterface arg0, int arg1) {
showImportedTrack();
finish();
}
});
return dialogBuilder.create();
case FAILURE_DIALOG:
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setNeutralButton(R.string.ok, new OnClickListener() {
@Override
public void onClick(DialogInterface arg0, int arg1) {
finish();
}
});
builder.setIcon(android.R.drawable.ic_dialog_alert);
builder.setTitle(R.string.error);
return builder.create();
}
return null;
}
@Override
protected void onPrepareDialog(int id, Dialog dialog) {
super.onPrepareDialog(id, dialog);
switch (id) {
case SUCCESS_DIALOG:
AlertDialog successDialog = (AlertDialog) dialog;
successDialog.setMessage(getString(R.string.import_success, importedTrackIds.length));
break;
case FAILURE_DIALOG:
AlertDialog failureDialog = (AlertDialog) dialog;
failureDialog.setMessage(getString(resultMessage));
break;
}
}
protected void showImportedTrack() {
long lastTrackId = importedTrackIds[importedTrackIds.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();
}
private void startTrackImport(final String fileName) {
showDialog(PROGRESS_DIALOG);
Thread t = new Thread() {
@Override
public void run() {
resultMessage = R.string.success;
importedTrackIds = null;
try {
try {
InputStream is = new FileInputStream(fileName);
importedTrackIds = GpxImporter.importGPXFile(is, providerUtils);
} catch (SAXException e) {
Log.e(TAG, "Caught an unexpected exception.", e);
resultMessage = R.string.error_generic;
} catch (ParserConfigurationException e) {
Log.e(TAG, "Caught an unexpected exception.", e);
resultMessage = R.string.error_generic;
} catch (IOException e) {
Log.e(TAG, "Caught an unexpected exception.", e);
resultMessage = R.string.error_unable_to_read_file;
} catch (NullPointerException e) {
Log.e(TAG, "Caught an unexpected exception.", e);
resultMessage = R.string.error_invalid_gpx_format;
} catch (OutOfMemoryError e) {
Log.e(TAG, "Caught an unexpected exception.", e);
resultMessage = R.string.error_out_of_memory;
}
boolean success = (importedTrackIds != null && importedTrackIds.length > 0);
showImportResult(success);
} finally {
runOnUiThread(new Runnable() {
public void run() {
dismissDialog(PROGRESS_DIALOG);
}
});
}
}
};
t.start();
}
private void showImportResult(boolean success) {
final int dialogToShow = success ? SUCCESS_DIALOG : FAILURE_DIALOG;
// Show a dialog telling the user about the import, and asking if he wishes
// to open the track right away.
runOnUiThread(new Runnable() {
@Override
public void run() {
showDialog(dialogToShow);
}
});
}
}
@@ -0,0 +1,235 @@
/*
* 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.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.app.AlertDialog;
import android.app.Dialog;
import android.content.ContentUris;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnCancelListener;
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 static final int RESULT_DIALOG = 1;
/* VisibleForTesting */
static final int PROGRESS_DIALOG = 2;
private MyTracksProviderUtils providerUtils;
private long trackId;
private TrackWriter writer;
private boolean shareFile;
private TrackFileFormat format;
private WriteProgressController controller;
@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);
}
controller = new WriteProgressController(this, writer, PROGRESS_DIALOG);
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() {
removeDialog(RESULT_DIALOG);
showDialog(RESULT_DIALOG);
}
@Override
protected Dialog onCreateDialog(int id) {
switch (id) {
case RESULT_DIALOG:
return createResultDialog();
case PROGRESS_DIALOG:
if (controller != null) {
return controller.createProgressDialog();
}
default:
return super.onCreateDialog(id);
}
}
private Dialog createResultDialog() {
boolean success = writer.wasSuccess();
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setMessage(writer.getErrorMessage());
builder.setNeutralButton(R.string.ok, new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int arg1) {
dialog.dismiss();
finish();
}
});
builder.setOnCancelListener(new OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
dialog.dismiss();
finish();
}
});
builder.setIcon(success ? android.R.drawable.ic_dialog_info :
android.R.drawable.ic_dialog_alert);
builder.setTitle(success ? R.string.success : R.string.error);
return builder.create();
}
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);
}
}
@@ -1,12 +1,12 @@
/* /*
* Copyright 2010 Google Inc. * Copyright 2010 Google Inc.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may not * 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 * use this file except in compliance with the License. You may obtain a copy of
* the License at * the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
@@ -15,7 +15,10 @@
*/ */
package com.google.android.apps.mytracks.io.file; package com.google.android.apps.mytracks.io.file;
import static com.google.android.apps.mytracks.Constants.TAG;
import android.os.Environment; import android.os.Environment;
import android.util.Log;
import java.io.File; import java.io.File;
@@ -24,18 +27,18 @@ import java.io.File;
* @author Sandor Dornbush * @author Sandor Dornbush
*/ */
public class TempFileCleaner { public class TempFileCleaner {
private long currentTimeMillis; private long currentTimeMillis;
public static void clean() { public static void clean() {
(new TempFileCleaner(System.currentTimeMillis())).cleanImpl(); (new TempFileCleaner(System.currentTimeMillis())).cleanImpl();
} }
// @VisibleForTesting // @VisibleForTesting
TempFileCleaner(long time) { TempFileCleaner(long time) {
currentTimeMillis = time; currentTimeMillis = time;
} }
private void cleanImpl() { private void cleanImpl() {
if (!Environment.getExternalStorageState().equals( if (!Environment.getExternalStorageState().equals(
Environment.MEDIA_MOUNTED)) { Environment.MEDIA_MOUNTED)) {
@@ -64,7 +67,9 @@ public class TempFileCleaner {
long oldest = currentTimeMillis - 1000 * 3600; long oldest = currentTimeMillis - 1000 * 3600;
for (File f : dir.listFiles()) { for (File f : dir.listFiles()) {
if (f.lastModified() < oldest) { if (f.lastModified() < oldest) {
f.delete(); if (!f.delete()) {
Log.w(TAG, "Failed to delete file " + f.getAbsolutePath());
}
count++; count++;
} }
} }
@@ -15,6 +15,8 @@
*/ */
package com.google.android.apps.mytracks.io.file; 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.Constants;
import com.google.android.apps.mytracks.content.MyTracksLocation; import com.google.android.apps.mytracks.content.MyTracksLocation;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils; import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
@@ -120,7 +122,9 @@ class TrackWriterImpl implements TrackWriter {
} catch (InterruptedException e) { } catch (InterruptedException e) {
Log.i(Constants.TAG, "The track write was interrupted"); Log.i(Constants.TAG, "The track write was interrupted");
if (file != null) { if (file != null) {
file.delete(); if (!file.delete()) {
Log.w(TAG, "Failed to delete file " + file.getAbsolutePath());
}
} }
success = false; success = false;
errorMessage = R.string.error_operation_cancelled; errorMessage = R.string.error_operation_cancelled;
@@ -195,7 +199,7 @@ class TrackWriterImpl implements TrackWriter {
directory, track.getName(), writer.getExtension()); directory, track.getName(), writer.getExtension());
if (fileName == null) { if (fileName == null) {
Log.e(Constants.TAG, Log.e(Constants.TAG,
"Unable to get a unique filename for " + fileName); "Unable to get a unique filename for " + track.getName());
return false; return false;
} }
@@ -13,9 +13,8 @@
* License for the specific language governing permissions and limitations under * License for the specific language governing permissions and limitations under
* the License. * 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.maps.mytracks.R; import com.google.android.maps.mytracks.R;
import android.app.Activity; import android.app.Activity;
@@ -41,14 +40,15 @@ class WriteProgressController {
* dialog has been dismissed. Whether the write succeeded can be * dialog has been dismissed. Whether the write succeeded can be
* determined by examining the {@link TrackWriter}. * determined by examining the {@link TrackWriter}.
*/ */
public void onComplete(TrackWriter writer); public void onComplete();
} }
private final Activity activity; private final Activity activity;
private final TrackWriter writer; private final TrackWriter writer;
private final ProgressDialog dialog; private ProgressDialog dialog;
private OnCompletionListener onCompletionListener; private OnCompletionListener onCompletionListener;
private final int progressDialogId;
/** /**
* @param activity the activity associated with this write * @param activity the activity associated with this write
@@ -57,17 +57,10 @@ class WriteProgressController {
* interested in notification upon completion of the write, they should * interested in notification upon completion of the write, they should
* use {@link #setOnCompletionListener}. * use {@link #setOnCompletionListener}.
*/ */
public WriteProgressController(Activity activity, TrackWriter writer) { public WriteProgressController(Activity activity, TrackWriter writer, int progressDialogId) {
this.activity = activity; this.activity = activity;
this.writer = writer; this.writer = writer;
this.progressDialogId = progressDialogId;
dialog = new ProgressDialog(activity);
dialog.setIcon(android.R.drawable.ic_dialog_info);
dialog.setTitle(activity.getString(R.string.progress_title));
dialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
dialog.setMessage(activity.getString(R.string.write_progress_message));
dialog.setIndeterminate(true);
dialog.setOnCancelListener(dialogCancelListener);
writer.setOnCompletionListener(writerCompleteListener); writer.setOnCompletionListener(writerCompleteListener);
writer.setOnWriteListener(writerWriteListener); writer.setOnWriteListener(writerWriteListener);
@@ -78,9 +71,20 @@ class WriteProgressController {
this.onCompletionListener = onCompletionListener; this.onCompletionListener = onCompletionListener;
} }
public ProgressDialog createProgressDialog() {
dialog = new ProgressDialog(activity);
dialog.setIcon(android.R.drawable.ic_dialog_info);
dialog.setTitle(activity.getString(R.string.progress_title));
dialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
dialog.setMessage(activity.getString(R.string.write_progress_message));
dialog.setIndeterminate(true);
dialog.setOnCancelListener(dialogCancelListener);
return dialog;
}
/** Initiate an asynchronous write. */ /** Initiate an asynchronous write. */
public void startWrite() { public void startWrite() {
DialogManager.showDialogSafely(activity, dialog); activity.showDialog(progressDialogId);
writer.writeTrackAsync(); writer.writeTrackAsync();
} }
@@ -101,10 +105,10 @@ class WriteProgressController {
new TrackWriter.OnCompletionListener() { new TrackWriter.OnCompletionListener() {
@Override @Override
public void onComplete() { public void onComplete() {
DialogManager.dismissDialogSafely(activity, dialog); activity.dismissDialog(progressDialogId);
if (onCompletionListener != null) { if (onCompletionListener != null) {
onCompletionListener.onComplete(writer); onCompletionListener.onComplete();
} }
} }
}; };
@@ -1,12 +1,12 @@
/* /*
* Copyright 2010 Google Inc. * Copyright 2010 Google Inc.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may not * 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 * use this file except in compliance with the License. You may obtain a copy of
* the License at * the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
@@ -137,7 +137,7 @@ public class AndroidGDataClient implements GDataClient {
/* /*
* (non-Javadoc) * (non-Javadoc)
* *
* @see GDataClient#encodeUri(java.lang.String) * @see GDataClient#encodeUri(java.lang.String)
*/ */
public String encodeUri(String uri) { public String encodeUri(String uri) {
@@ -155,7 +155,7 @@ public class AndroidGDataClient implements GDataClient {
/* /*
* (non-Javadoc) * (non-Javadoc)
* *
* @see com.google.wireless.gdata.client.GDataClient#createQueryParams() * @see com.google.wireless.gdata.client.GDataClient#createQueryParams()
*/ */
public QueryParams createQueryParams() { public QueryParams createQueryParams() {
@@ -273,7 +273,7 @@ public class AndroidGDataClient implements GDataClient {
String errorMessage = null; String errorMessage = null;
HttpEntity entity = response.getEntity(); HttpEntity entity = response.getEntity();
try { try {
if (response != null && entity != null) { if (entity != null) {
InputStream in = entity.getContent(); InputStream in = entity.getContent();
ByteArrayOutputStream baos = new ByteArrayOutputStream(); ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buf = new byte[8192]; byte[] buf = new byte[8192];
@@ -329,7 +329,7 @@ public class AndroidGDataClient implements GDataClient {
/* /*
* (non-Javadoc) * (non-Javadoc)
* *
* @see GDataClient#getFeedAsStream(java.lang.String, java.lang.String) * @see GDataClient#getFeedAsStream(java.lang.String, java.lang.String)
*/ */
public InputStream getFeedAsStream(String feedUrl, String authToken) public InputStream getFeedAsStream(String feedUrl, String authToken)
@@ -358,7 +358,7 @@ public class AndroidGDataClient implements GDataClient {
/* /*
* (non-Javadoc) * (non-Javadoc)
* *
* @see GDataClient#createEntry * @see GDataClient#createEntry
*/ */
public InputStream createEntry(String feedUrl, String authToken, public InputStream createEntry(String feedUrl, String authToken,
@@ -377,7 +377,7 @@ public class AndroidGDataClient implements GDataClient {
/* /*
* (non-Javadoc) * (non-Javadoc)
* *
* @see GDataClient#updateEntry * @see GDataClient#updateEntry
*/ */
public InputStream updateEntry(String editUri, String authToken, public InputStream updateEntry(String editUri, String authToken,
@@ -395,7 +395,7 @@ public class AndroidGDataClient implements GDataClient {
/* /*
* (non-Javadoc) * (non-Javadoc)
* *
* @see GDataClient#deleteEntry * @see GDataClient#deleteEntry
*/ */
public void deleteEntry(String editUri, String authToken) public void deleteEntry(String editUri, String authToken)
@@ -3,7 +3,7 @@ package com.google.android.apps.mytracks.io.mymaps;
import com.google.android.maps.GeoPoint; import com.google.android.maps.GeoPoint;
import java.io.Serializable; import java.util.Random;
import java.util.Vector; import java.util.Vector;
/** /**
@@ -12,7 +12,7 @@ import java.util.Vector;
* {@link MyMapsFeatureMetadata} object so that it can be more efficiently * {@link MyMapsFeatureMetadata} object so that it can be more efficiently
* transmitted to other activities. * transmitted to other activities.
*/ */
class MyMapsFeature implements Serializable { class MyMapsFeature {
private static final long serialVersionUID = 8439035544430497236L; 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. */ /** The metadata of this feature in a format efficient for transmission. */
private MyMapsFeatureMetadata featureInfo = new MyMapsFeatureMetadata(); private MyMapsFeatureMetadata featureInfo = new MyMapsFeatureMetadata();
private final Random random = new Random();
/** /**
* Initializes a valid but empty feature. It will default to a * 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 * {@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 * @param point The new point to add
*/ */
public synchronized void addPoint(GeoPoint point) { public void addPoint(GeoPoint point) {
latitudeE6.add(point.getLatitudeE6()); latitudeE6.add(point.getLatitudeE6());
longitudeE6.add(point.getLongitudeE6()); 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 * Generates a new local id for this feature based on the current time and
* a random number. * a random number.
*/ */
public synchronized void generateAndroidId() { void generateAndroidId() {
Long time = System.currentTimeMillis(); long time = System.currentTimeMillis();
Integer random = (int) (Math.random() * 10000); int rand = random.nextInt(10000);
androidId = time.toString() + "." + random.toString(); androidId = time + "." + rand;
} }
/** /**
@@ -82,7 +84,7 @@ class MyMapsFeature implements Serializable {
* *
* @return The local id for this feature * @return The local id for this feature
*/ */
public String getAndroidId() { String getAndroidId() {
return androidId; return androidId;
} }
@@ -102,7 +104,7 @@ class MyMapsFeature implements Serializable {
* *
* @param description The new description of this feature * @param description The new description of this feature
*/ */
public synchronized void setDescription(String description) { public void setDescription(String description) {
featureInfo.setDescription(description); featureInfo.setDescription(description);
} }
@@ -196,7 +198,7 @@ class MyMapsFeature implements Serializable {
* *
* @param title The new title of this feature * @param title The new title of this feature
*/ */
public synchronized void setTitle(String title) { public void setTitle(String title) {
featureInfo.setTitle(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 * @param type The new type of the feature. That type must be one of
* {@link #MARKER}, {@link #LINE}, or {@link #SHAPE} * {@link #MARKER}, {@link #LINE}, or {@link #SHAPE}
*/ */
public synchronized void setType(int type) { public void setType(int type) {
featureInfo.setType(type); featureInfo.setType(type);
} }
@@ -217,7 +219,7 @@ class MyMapsFeature implements Serializable {
* *
* @param color The new ARGB color of this feature * @param color The new ARGB color of this feature
*/ */
public synchronized void setColor(int color) { public void setColor(int color) {
featureInfo.setColor(color); featureInfo.setColor(color);
} }
@@ -227,7 +229,7 @@ class MyMapsFeature implements Serializable {
* *
* @param url The new icon url of the feature * @param url The new icon url of the feature
*/ */
public synchronized void setIconUrl(String url) { public void setIconUrl(String url) {
featureInfo.setIconUrl(url); featureInfo.setIconUrl(url);
} }
} }
@@ -82,6 +82,6 @@ class MyMapsFeatureMetadata {
} }
public void setIconUrl(String url) { public void setIconUrl(String url) {
iconUrl = new String(url); iconUrl = url;
} }
} }
@@ -23,7 +23,7 @@ class MyMapsMapMetadata {
} }
public void setTitle(String title) { public void setTitle(String title) {
this.title = new String(title); this.title = title;
} }
public String getDescription() { public String getDescription() {
@@ -31,7 +31,7 @@ class MyMapsMapMetadata {
} }
public void setDescription(String description) { public void setDescription(String description) {
this.description = new String(description); this.description = description;
} }
public boolean getSearchable() { public boolean getSearchable() {
@@ -47,6 +47,6 @@ class MyMapsMapMetadata {
} }
public void setGDataEditUri(String editUri) { public void setGDataEditUri(String editUri) {
this.gdataEditUri = new String(editUri); this.gdataEditUri = editUri;
} }
} }
@@ -85,8 +85,8 @@ class XmlMapsGDataParserFactory implements GDataParserFactory {
@Override @Override
public GDataSerializer createSerializer(Entry en) { public GDataSerializer createSerializer(Entry en) {
if (en.getClass() == MapFeatureEntry.class) { if (en instanceof MapFeatureEntry) {
return new XmlMapsGDataSerializer(xmlFactory, en); return new XmlMapsGDataSerializer(xmlFactory, (MapFeatureEntry) en);
} else { } else {
return new XmlEntryGDataSerializer(xmlFactory, en); return new XmlEntryGDataSerializer(xmlFactory, en);
} }
@@ -1,7 +1,6 @@
// Copyright 2010 Google Inc. All Rights Reserved. // Copyright 2010 Google Inc. All Rights Reserved.
package com.google.android.apps.mytracks.io.mymaps; 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.data.StringUtils;
import com.google.wireless.gdata.parser.ParseException; import com.google.wireless.gdata.parser.ParseException;
import com.google.wireless.gdata.parser.xml.XmlGDataParser; 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 android.util.Log;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlSerializer;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
import java.util.Map; import java.util.Map;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlSerializer;
/** /**
* Serializer of maps data for GData. * Serializer of maps data for GData.
*/ */
@@ -29,11 +28,11 @@ class XmlMapsGDataSerializer extends XmlEntryGDataSerializer {
private XmlParserFactory factory; private XmlParserFactory factory;
private OutputStream stream; private OutputStream stream;
public XmlMapsGDataSerializer(XmlParserFactory factory, Entry entry) { public XmlMapsGDataSerializer(XmlParserFactory factory, MapFeatureEntry entry) {
super(factory, entry); super(factory, entry);
this.factory = factory; this.factory = factory;
this.entry = (MapFeatureEntry) entry; this.entry = entry;
} }
@Override @Override
@@ -58,7 +58,8 @@ public class ResultDialogFactory {
*/ */
public static AlertDialog makeDialog(Activity activity, List<SendResult> results, public static AlertDialog makeDialog(Activity activity, List<SendResult> results,
DialogInterface.OnClickListener onOkClickListener, DialogInterface.OnClickListener onOkClickListener,
DialogInterface.OnClickListener onShareClickListener) { DialogInterface.OnClickListener onShareClickListener,
DialogInterface.OnCancelListener onCancelListener) {
boolean success = true; boolean success = true;
for (SendResult result : results) { for (SendResult result : results) {
if (!result.isSuccess()) { if (!result.isSuccess()) {
@@ -84,6 +85,8 @@ public class ResultDialogFactory {
onShareClickListener); onShareClickListener);
} }
builder.setOnCancelListener(onCancelListener);
return builder.create(); return builder.create();
} }
@@ -0,0 +1,896 @@
/*
* 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.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.Dialog;
import android.app.ProgressDialog;
import android.content.ContentUris;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnCancelListener;
import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Bundle;
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 {
// Keys for saved state variables.
private static final String STATE_ACCOUNT_TYPE = "accountType";
private static final String STATE_ACCOUNT_NAME = "accountName";
private static final String STATE_TABLE_ID = "tableId";
private static final String STATE_MAP_ID = "mapId";
private static final String STATE_DOCS_MESSAGE = "docsMsg";
private static final String STATE_FUSION_TABLES_MESSAGE = "fusionMsg";
private static final String STATE_MAPS_MESSAGE = "mapsMsg";
private static final String STATE_DOCS_SUCCESS = "docsSuccess";
private static final String STATE_FUSION_SUCCESS = "fusionSuccess";
private static final String STATE_MAPS_SUCCESS = "mapsSuccess";
private static final String STATE_STATE = "state";
private static final String EXTRA_SHARE_LINK = "share_link";
/** States for the state machine that defines the upload process. */
private enum SendState {
SEND_OPTIONS,
START,
AUTHENTICATE_MAPS,
PICK_MAP,
SEND_TO_MAPS,
SEND_TO_MAPS_DONE,
AUTHENTICATE_FUSION_TABLES,
SEND_TO_FUSION_TABLES,
SEND_TO_FUSION_TABLES_DONE,
AUTHENTICATE_DOCS,
AUTHENTICATE_TRIX,
SEND_TO_DOCS,
SEND_TO_DOCS_DONE,
SHOW_RESULTS,
SHARE_LINK,
FINISH,
DONE,
NOT_READY
}
private static final int SEND_DIALOG = 1;
private static final int PROGRESS_DIALOG = 2;
/* @VisibleForTesting */
static final int DONE_DIALOG = 3;
// UI
private SendDialog sendDialog;
private ProgressDialog progressDialog;
// Services
private MyTracksProviderUtils providerUtils;
private SharedPreferences sharedPreferences;
private GoogleAnalyticsTracker tracker;
// Authentication
private AuthManager lastAuth;
private final HashMap<String, AuthManager> authMap =
new HashMap<String, AuthManager>();
private final AccountChooser accountChooser = new AccountChooser();
private String lastAccountName;
private String lastAccountType;
// Send request information.
private boolean shareRequested = false;
private long sendTrackId;
// Send result information, used by results dialog.
private boolean sendToMyMapsSuccess = false;
private boolean sendToFusionTablesSuccess = false;
private boolean sendToDocsSuccess = false;
// TODO: Make these be used for showing results
private String sendToMyMapsMessage;
private String sendToFusionTablesMessage;
private String sendToDocsMessage;
// Send result information, used to share a link.
private String sendToMyMapsMapId;
private String sendToFusionTablesTableId;
// Current sending state.
private SendState currentState;
private final OnCancelListener finishOnCancelListener = new OnCancelListener() {
@Override
public void onCancel(DialogInterface arg0) {
onAllDone();
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
Log.d(TAG, "SendActivity.onCreate");
super.onCreate(savedInstanceState);
providerUtils = MyTracksProviderUtils.Factory.get(this);
sharedPreferences = getSharedPreferences(Constants.SETTINGS_NAME, 0);
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));
if (!handleIntent()) {
finish();
return;
}
if (savedInstanceState != null) {
restoreInstanceState(savedInstanceState);
}
executeStateMachine(null);
}
private boolean handleIntent() {
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);
return false;
}
sendTrackId = ContentUris.parseId(data);
shareRequested = intent.getBooleanExtra(EXTRA_SHARE_LINK, false);
return true;
}
@Override
protected Dialog onCreateDialog(int id) {
switch (id) {
case SEND_DIALOG:
return createSendDialog();
case PROGRESS_DIALOG:
return createProgressDialog();
case DONE_DIALOG:
return createDoneDialog();
}
return null;
}
private Dialog createSendDialog() {
sendDialog = new SendDialog(this);
sendDialog.setOnClickListener(new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if (which != DialogInterface.BUTTON_POSITIVE) {
finish();
return;
}
dialog.dismiss();
executeStateMachine(SendState.START);
}
});
sendDialog.setOnCancelListener(finishOnCancelListener);
return sendDialog;
}
private void restoreInstanceState(Bundle savedInstanceState) {
currentState = SendState.values()[savedInstanceState.getInt(STATE_STATE)];
sendToMyMapsSuccess = savedInstanceState.getBoolean(STATE_MAPS_SUCCESS);
sendToFusionTablesSuccess = savedInstanceState.getBoolean(STATE_FUSION_SUCCESS);
sendToDocsSuccess = savedInstanceState.getBoolean(STATE_DOCS_SUCCESS);
sendToMyMapsMessage = savedInstanceState.getString(STATE_MAPS_MESSAGE);
sendToFusionTablesMessage = savedInstanceState.getString(STATE_FUSION_TABLES_MESSAGE);
sendToDocsMessage = savedInstanceState.getString(STATE_DOCS_MESSAGE);
sendToMyMapsMapId = savedInstanceState.getString(STATE_MAP_ID);
sendToFusionTablesTableId = savedInstanceState.getString(STATE_TABLE_ID);
lastAccountName = savedInstanceState.getString(STATE_ACCOUNT_NAME);
lastAccountType = savedInstanceState.getString(STATE_ACCOUNT_TYPE);
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putInt(STATE_STATE, currentState.ordinal());
outState.putBoolean(STATE_MAPS_SUCCESS, sendToMyMapsSuccess);
outState.putBoolean(STATE_FUSION_SUCCESS, sendToFusionTablesSuccess);
outState.putBoolean(STATE_DOCS_SUCCESS, sendToDocsSuccess);
outState.putString(STATE_MAPS_MESSAGE, sendToMyMapsMessage);
outState.putString(STATE_FUSION_TABLES_MESSAGE, sendToFusionTablesMessage);
outState.putString(STATE_DOCS_MESSAGE, sendToDocsMessage);
outState.putString(STATE_MAP_ID, sendToMyMapsMapId);
outState.putString(STATE_TABLE_ID, sendToFusionTablesTableId);
outState.putString(STATE_ACCOUNT_NAME, lastAccountName);
outState.putString(STATE_ACCOUNT_TYPE, lastAccountType);
// TODO: Ideally we should serialize/restore the authenticator map and lastAuth somehow,
// but it's highly unlikely we'll get killed while an auth dialog is displayed.
}
@Override
protected void onStop() {
Log.d(TAG, "SendActivity.onStop" + currentState);
tracker.dispatch();
tracker.stop();
super.onStop();
}
@Override
protected void onDestroy() {
Log.d(TAG, "SendActivity.onDestroy, state=" + currentState);
super.onDestroy();
}
private void executeStateMachine(SendState startState) {
if (startState != null) {
currentState = startState;
}
// If a state handler returns NOT_READY, it means it's waiting for some
// event, and will call this method again when it happens.
while (currentState != SendState.DONE &&
currentState != SendState.NOT_READY) {
Log.d(TAG, "Executing state " + currentState);
currentState = executeState(currentState);
Log.d(TAG, "New state is " + currentState);
}
}
private SendState executeState(SendState state) {
switch (state) {
case SEND_OPTIONS:
return showSendOptions();
case START:
return startSend();
case AUTHENTICATE_MAPS:
return authenticateToGoogleMaps();
case PICK_MAP:
return pickMap();
case SEND_TO_MAPS:
return sendToGoogleMaps();
case SEND_TO_MAPS_DONE:
return onSendToGoogleMapsDone();
case AUTHENTICATE_FUSION_TABLES:
return authenticateToFusionTables();
case SEND_TO_FUSION_TABLES:
return sendToFusionTables();
case SEND_TO_FUSION_TABLES_DONE:
return onSendToFusionTablesDone();
case AUTHENTICATE_DOCS:
return authenticateToGoogleDocs();
case AUTHENTICATE_TRIX:
return authenticateToGoogleTrix();
case SEND_TO_DOCS:
return sendToGoogleDocs();
case SEND_TO_DOCS_DONE:
return onSendToGoogleDocsDone();
case SHOW_RESULTS:
return onSendToGoogleDone();
case SHARE_LINK:
return shareLink();
case FINISH:
return onAllDone();
default:
Log.e(TAG, "Reached a non-executable state");
return null;
}
}
private SendState showSendOptions() {
showDialog(SEND_DIALOG);
return SendState.NOT_READY;
}
/**
* Initiates the process to send tracks to google.
* This is called once the user has selected sending options via the
* SendToGoogleDialog.
*/
private SendState startSend() {
showDialog(PROGRESS_DIALOG);
if (sendDialog.getSendToMyMaps()) {
return SendState.AUTHENTICATE_MAPS;
} else if (sendDialog.getSendToFusionTables()) {
return SendState.AUTHENTICATE_FUSION_TABLES;
} else if (sendDialog.getSendToDocs()) {
return SendState.AUTHENTICATE_DOCS;
} else {
Log.w(TAG, "Nowhere to upload to");
return SendState.FINISH;
}
}
private Dialog createProgressDialog() {
progressDialog = new ProgressDialog(this);
progressDialog.setCancelable(false);
progressDialog.setIcon(android.R.drawable.ic_dialog_info);
progressDialog.setTitle(R.string.progress_title);
progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
progressDialog.setMax(100);
progressDialog.setProgress(0);
return progressDialog;
}
private SendState authenticateToGoogleMaps() {
Log.d(TAG, "SendActivity.authenticateToGoogleMaps");
progressDialog.setProgress(0);
progressDialog.setMessage(getString(
R.string.progress_message_authenticating_mymaps));
authenticate(Constants.AUTHENTICATE_TO_MY_MAPS, MyMapsConstants.SERVICE_NAME);
// AUTHENTICATE_TO_MY_MAPS callback calls sendToGoogleMaps
return SendState.NOT_READY;
}
private SendState pickMap() {
if (!sendDialog.getCreateNewMap()) {
// Ask the user to choose a map to upload into
Intent listIntent = new Intent(this, MyMapsList.class);
listIntent.putExtra(MyMapsList.EXTRA_ACCOUNT_NAME, lastAccountName);
listIntent.putExtra(MyMapsList.EXTRA_ACCOUNT_TYPE, lastAccountType);
startActivityForResult(listIntent, Constants.GET_MAP);
// The callback for GET_MAP calls authenticateToGoogleMaps
return SendState.NOT_READY;
} else {
return SendState.SEND_TO_MAPS;
}
}
private SendState sendToGoogleMaps() {
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);
}
}
executeStateMachine(SendState.SEND_TO_MAPS_DONE);
}
};
if (sendToMyMapsMapId == null) {
sendToMyMapsMapId = SendToMyMaps.NEW_MAP_ID;
}
final SendToMyMaps sender = new SendToMyMaps(this, sendToMyMapsMapId, lastAuth,
sendTrackId, this /*progressIndicator*/, onCompletion);
new Thread(sender, "SendToMyMaps").start();
return SendState.NOT_READY;
}
private SendState onSendToGoogleMapsDone() {
if (sendDialog.getSendToFusionTables()) {
return SendState.AUTHENTICATE_FUSION_TABLES;
} else if (sendDialog.getSendToDocs()) {
return SendState.AUTHENTICATE_DOCS;
} else {
return SendState.SHOW_RESULTS;
}
}
private SendState authenticateToFusionTables() {
progressDialog.setProgress(0);
progressDialog.setMessage(getString(
R.string.progress_message_authenticating_fusiontables));
authenticate(Constants.AUTHENTICATE_TO_FUSION_TABLES, SendToFusionTables.SERVICE_ID);
// AUTHENTICATE_TO_FUSION_TABLES callback calls sendToFusionTables
return SendState.NOT_READY;
}
private SendState 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);
}
}
executeStateMachine(SendState.SEND_TO_FUSION_TABLES_DONE);
}
};
final SendToFusionTables sender = new SendToFusionTables(this, lastAuth,
sendTrackId, this /*progressIndicator*/, onCompletion);
new Thread(sender, "SendToFusionTables").start();
return SendState.NOT_READY;
}
private SendState onSendToFusionTablesDone() {
if (sendDialog.getSendToDocs()) {
return SendState.AUTHENTICATE_DOCS;
} else {
return SendState.SHOW_RESULTS;
}
}
private SendState authenticateToGoogleDocs() {
setProgressValue(0);
setProgressMessage(
R.string.progress_message_authenticating_docs);
authenticate(Constants.AUTHENTICATE_TO_DOCLIST, SendToDocs.GDATA_SERVICE_NAME_DOCLIST);
// AUTHENTICATE_TO_DOCLIST callback calls authenticateToGoogleTrix
return SendState.NOT_READY;
}
private SendState authenticateToGoogleTrix() {
setProgressValue(30);
setProgressMessage(
R.string.progress_message_authenticating_docs);
authenticate(Constants.AUTHENTICATE_TO_TRIX, SendToDocs.GDATA_SERVICE_NAME_TRIX);
// AUTHENTICATE_TO_TRIX callback calls sendToGoogleDocs
return SendState.NOT_READY;
}
private SendState 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();
executeStateMachine(SendState.SEND_TO_DOCS_DONE);
}
};
sender.setOnCompletion(onCompletion);
sender.sendToDocs(sendTrackId);
return SendState.NOT_READY;
}
private SendState onSendToGoogleDocsDone() {
return SendState.SHOW_RESULTS;
}
private SendState onSendToGoogleDone() {
tracker.dispatch();
runOnUiThread(new Runnable() {
@Override
public void run() {
Log.d(TAG, "Sending to Google done.");
dismissDialog(PROGRESS_DIALOG);
dismissDialog(SEND_DIALOG);
// Ensure a new done dialog is created each time.
// This is required because the send results must be available at the
// time the dialog is created.
removeDialog(DONE_DIALOG);
showDialog(DONE_DIALOG);
}
});
return SendState.NOT_READY;
}
private Dialog createDoneDialog() {
Log.d(TAG, "Creating done dialog");
// 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.
List<SendResult> results = makeSendToGoogleResults();
final boolean canShare = sendToFusionTablesTableId != null || sendToMyMapsMapId != null;
final OnClickListener finishListener = new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
executeStateMachine(SendState.FINISH);
}
};
DialogInterface.OnClickListener doShareListener = null;
if (canShare) {
doShareListener = new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
executeStateMachine(SendState.SHARE_LINK);
}
};
}
DialogInterface.OnClickListener onOkListener = (canShare && shareRequested)
? doShareListener : finishListener;
DialogInterface.OnClickListener onShareListener = (canShare && !shareRequested)
? doShareListener : null;
return ResultDialogFactory.makeDialog(this, results, onOkListener, onShareListener, finishOnCancelListener);
}
private SendState shareLink() {
boolean sentToMyMaps = sendDialog.getSendToMyMaps();
boolean sentToFusionTables = sendDialog.getSendToFusionTables();
if (!shareLinkToMap(sentToMyMaps, sentToFusionTables)) {
Log.w(TAG, "Failed to share link");
}
return SendState.FINISH;
}
private SendState onAllDone() {
Log.d(TAG, "All sending done.");
removeDialog(PROGRESS_DIALOG);
removeDialog(SEND_DIALOG);
removeDialog(DONE_DIALOG);
progressDialog = null;
sendDialog = null;
finish();
return SendState.DONE;
}
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<SendResult> makeSendToGoogleResults() {
List<SendResult> results = new ArrayList<SendResult>();
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 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(requestCode, service);
}
});
} else {
doLogin(requestCode, service, null);
}
}
private void chooseAccount(final int requestCode, final String service) {
accountChooser.chooseAccount(SendActivity.this,
new AccountChooser.AccountHandler() {
@Override
public void onAccountSelected(Account account) {
if (account == null) {
dismissDialog(PROGRESS_DIALOG);
finish();
return;
}
lastAccountName = account.name;
lastAccountType = account.type;
doLogin(requestCode, service, account);
}
});
}
private void doLogin(final int requestCode, final String service, final Object account) {
lastAuth.doLogin(new Runnable() {
public void run() {
Log.i(TAG, "Login success for " + service + "!");
onActivityResult(requestCode, RESULT_OK, new Intent());
}
}, account);
}
@Override
public void onActivityResult(int requestCode, int resultCode,
final Intent results) {
Log.w(TAG, "Result: req=" + requestCode + "; res=" + resultCode + "; result=" + results);
SendState nextState = null;
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)) {
nextState = SendState.FINISH;
}
}
case Constants.GET_MAP: {
// User picked a map to upload to
Log.d(TAG, "Get map result: " + resultCode);
if (resultCode == RESULT_OK) {
if (results.hasExtra("mapid")) {
sendToMyMapsMapId = results.getStringExtra("mapid");
}
nextState = SendState.SEND_TO_MAPS;
} else {
nextState = SendState.FINISH;
}
break;
}
case Constants.AUTHENTICATE_TO_MY_MAPS: {
// Authenticated with Google My Maps
if (results != null && resultCode == RESULT_OK) {
nextState = SendState.PICK_MAP;
} else {
nextState = SendState.SHOW_RESULTS;
}
break;
}
case Constants.AUTHENTICATE_TO_FUSION_TABLES: {
// Authenticated with Google Fusion Tables
if (results != null && resultCode == RESULT_OK) {
nextState = SendState.SEND_TO_FUSION_TABLES;
} else {
nextState = SendState.SHOW_RESULTS;
}
break;
}
case Constants.AUTHENTICATE_TO_DOCLIST: {
// Authenticated with Google Docs
if (resultCode == RESULT_OK) {
nextState = SendState.AUTHENTICATE_TRIX;
} else {
nextState = SendState.SHOW_RESULTS;
}
break;
}
case Constants.AUTHENTICATE_TO_TRIX: {
// Authenticated with Trix
if (resultCode == RESULT_OK) {
nextState = SendState.SEND_TO_DOCS;
} else {
nextState = SendState.SHOW_RESULTS;
}
break;
}
default: {
Log.w(TAG, "Warning unhandled request code: " + requestCode);
return;
}
}
if (nextState != null) {
executeStateMachine(nextState);
}
}
/**
* Resets status information for sending to MyMaps/Docs.
*/
private void resetState() {
currentState = SendState.SEND_OPTIONS;
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);
}
}
@@ -16,8 +16,6 @@
package com.google.android.apps.mytracks.io.sendtogoogle; package com.google.android.apps.mytracks.io.sendtogoogle;
import com.google.android.apps.mytracks.Constants; 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 com.google.android.maps.mytracks.R;
import android.app.Dialog; import android.app.Dialog;
@@ -42,15 +40,13 @@ import android.widget.RadioGroup;
*/ */
public class SendDialog extends Dialog { public class SendDialog extends Dialog {
private RadioGroup groupMyMaps;
private RadioButton createNewMapRadioButton; private RadioButton createNewMapRadioButton;
private RadioButton pickMapRadioButton; private RadioButton pickMapRadioButton;
private CheckBox sendToMyMapsCheckBox; private CheckBox sendToMyMapsCheckBox;
private CheckBox sendToFusionTablesCheckBox; private CheckBox sendToFusionTablesCheckBox;
private CheckBox sendToDocsCheckBox; private CheckBox sendToDocsCheckBox;
private RadioButton sendStatsRadioButton;
private RadioButton sendStatsAndPointsRadioButton; private RadioButton sendStatsAndPointsRadioButton;
private Button sendButton; private OnClickListener clickListener;
public SendDialog(Context context) { public SendDialog(Context context) {
super(context); super(context);
@@ -62,18 +58,38 @@ public class SendDialog extends Dialog {
requestWindowFeature(Window.FEATURE_NO_TITLE); requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.mytracks_send_to_google); 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); Button cancel = (Button) findViewById(R.id.sendtogoogle_cancel);
cancel.setOnClickListener(new View.OnClickListener() { cancel.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) { 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); Button send = (Button) findViewById(R.id.sendtogoogle_send_now);
send.setOnClickListener(new View.OnClickListener() { send.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) { public void onClick(View v) {
if (clickListener != null) {
clickListener.onClick(SendDialog.this, BUTTON_POSITIVE);
}
dismiss(); dismiss();
MyTracks.getInstance().sendToGoogle();
} }
}); });
@@ -86,25 +102,9 @@ public class SendDialog extends Dialog {
groupMyMaps.setVisibility(sendToMyMapsCheckBox.isChecked() ? View.VISIBLE : View.INVISIBLE); 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); sendToMyMapsCheckBox.setOnCheckedChangeListener(checkBoxListener);
sendToFusionTablesCheckBox =
(CheckBox) findViewById(R.id.sendtogoogle_google_fusiontables);
sendToFusionTablesCheckBox.setOnCheckedChangeListener(checkBoxListener); sendToFusionTablesCheckBox.setOnCheckedChangeListener(checkBoxListener);
sendToDocsCheckBox = (CheckBox) findViewById(R.id.sendtogoogle_google_docs);
sendToDocsCheckBox.setOnCheckedChangeListener(checkBoxListener); 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 = SharedPreferences prefs =
getContext().getSharedPreferences(Constants.SETTINGS_NAME, 0); getContext().getSharedPreferences(Constants.SETTINGS_NAME, 0);
@@ -163,6 +163,10 @@ public class SendDialog extends Dialog {
super.onStop(); super.onStop();
} }
public void setOnClickListener(OnClickListener clickListener) {
this.clickListener = clickListener;
}
public boolean getSendToMyMaps() { public boolean getSendToMyMaps() {
return sendToMyMapsCheckBox.isChecked(); return sendToMyMapsCheckBox.isChecked();
} }
@@ -182,4 +186,4 @@ public class SendDialog extends Dialog {
public boolean getSendStatsAndPoints() { public boolean getSendStatsAndPoints() {
return sendStatsAndPointsRadioButton.isChecked(); return sendStatsAndPointsRadioButton.isChecked();
} }
} }
@@ -93,13 +93,13 @@ public class PreferenceManager implements OnSharedPreferenceChangeListener {
return; return;
} }
if (key == null || key.equals(minRecordingDistanceKey)) { if (key == null || key.equals(minRecordingDistanceKey)) {
service.setMinRecordingDistance( int minRecordingDistance = sharedPreferences.getInt(
sharedPreferences.getInt( minRecordingDistanceKey,
minRecordingDistanceKey, Constants.DEFAULT_MIN_RECORDING_DISTANCE);
Constants.DEFAULT_MIN_RECORDING_DISTANCE)); service.setMinRecordingDistance(minRecordingDistance);
Log.d(Constants.TAG, Log.d(Constants.TAG,
"TrackRecordingService: minRecordingDistance = " "TrackRecordingService: minRecordingDistance = "
+ service.getMinRecordingDistance()); + minRecordingDistance);
} }
if (key == null || key.equals(maxRecordingDistanceKey)) { if (key == null || key.equals(maxRecordingDistanceKey)) {
service.setMaxRecordingDistance(sharedPreferences.getInt( service.setMaxRecordingDistance(sharedPreferences.getInt(
@@ -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<RunningServiceInfo> 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() {}
}
File diff suppressed because it is too large Load Diff
@@ -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();
}
}
}
@@ -394,8 +394,9 @@ public class BluetoothConnectionManager {
offset = 0; offset = 0;
// Send the obtained bytes to the UI Activity // Send copy of the obtained bytes to the UI Activity.
handler.obtainMessage(MESSAGE_READ, bytes, -1, buffer) // Avoids memory inconsistency issues.
handler.obtainMessage(MESSAGE_READ, bytes, -1, buffer.clone())
.sendToTarget(); .sendToTarget();
} catch (IOException e) { } catch (IOException e) {
Log.e(Constants.TAG, "disconnected", e); Log.e(Constants.TAG, "disconnected", e);
@@ -0,0 +1,140 @@
/*
* 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.sensors;
import com.google.android.apps.mytracks.content.Sensor;
/**
* An implementation of a Sensor MessageParser for Polar Wearlink Bluetooth HRM.
*
* Polar Bluetooth Wearlink packet example;
* Hdr Len Chk Seq Status HeartRate RRInterval_16-bits
* FE 08 F7 06 F1 48 03 64
* where;
* Hdr always = 254 (0xFE),
* Chk = 255 - Len
* Seq range 0 to 15
* Status = Upper nibble may be battery voltage
* bit 0 is Beat Detection flag.
*
* Additional packet examples;
* FE 08 F7 06 F1 48 03 64
* FE 0A F5 06 F1 48 03 64 03 70
*
* @author John R. Gerthoffer
*/
public class PolarMessageParser implements MessageParser {
private int lastHeartRate = 0;
/**
* Applies Polar packet validation rules to buffer.
* Polar packets are checked for following;
* offset 0 = header byte, 254 (0xFE).
* offset 1 = packet length byte, 8, 10, 12, 14.
* offset 2 = check byte, 255 - packet length.
* offset 3 = sequence byte, range from 0 to 15.
*
* @param an array of bytes to parse
* @param buffer offset to beginning of packet.
* @return whether buffer has a valid packet at offset i
*/
private boolean packetValid (byte[] buffer, int i) {
boolean headerValid = (buffer[i] & 0xFF) == 0xFE;
boolean checkbyteValid = (buffer[i + 2] & 0xFF) == (0xFF - (buffer[i + 1] & 0xFF));
boolean sequenceValid = (buffer[i + 3] & 0xFF) < 16;
return headerValid && checkbyteValid && sequenceValid;
}
@Override
public Sensor.SensorDataSet parseBuffer(byte[] buffer) {
int heartRate = 0;
boolean heartrateValid = false;
// Minimum length Polar packets is 8, so stop search 8 bytes before buffer ends.
for (int i = 0; i < buffer.length - 8; i++) {
heartrateValid = packetValid(buffer,i);
if (heartrateValid) {
heartRate = buffer[i + 5] & 0xFF;
break;
}
}
// If our buffer is corrupted, use decaying last good value.
if(!heartrateValid) {
heartRate = (int) (lastHeartRate * 0.8);
if(heartRate < 50)
heartRate = 0;
}
lastHeartRate = heartRate; // Remember good value for next time.
// Heart Rate
Sensor.SensorData.Builder b = Sensor.SensorData.newBuilder()
.setValue(heartRate)
.setState(Sensor.SensorState.SENDING);
Sensor.SensorDataSet sds = Sensor.SensorDataSet.newBuilder()
.setCreationTime(System.currentTimeMillis())
.setHeartRate(b)
.build();
return sds;
}
/**
* Applies packet validation rules to buffer
*
* @param an array of bytes to parse
* @return whether buffer has a valid packet starting at index zero
*/
@Override
public boolean isValid(byte[] buffer) {
return packetValid(buffer,0);
}
/**
* Polar uses variable packet sizes; 8, 10, 12, 14 and rarely 16.
* The most frequent are 8 and 10.
* We will wait for 16 bytes.
* This way, we are assured of getting one good one.
*
* @return the size of buffer needed to parse a good packet
*/
@Override
public int getFrameSize() {
return 16;
}
/**
* Searches buffer for the beginning of a valid packet.
*
* @param an array of bytes to parse
* @return index to beginning of good packet, or -1 if none found.
*/
@Override
public int findNextAlignment(byte[] buffer) {
// Minimum length Polar packets is 8, so stop search 8 bytes before buffer ends.
for (int i = 0; i < buffer.length - 8; i++) {
if (packetValid(buffer,i)) {
return i;
}
}
return -1;
}
}
@@ -0,0 +1,28 @@
/*
* 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.sensors;
import android.content.Context;
/**
* PolarSensorManager - A sensor manager for Polar heart rate monitors.
*/
public class PolarSensorManager extends BluetoothSensorManager {
public PolarSensorManager(Context context) {
super(context, new PolarMessageParser());
}
}
@@ -1,12 +1,12 @@
/* /*
* Copyright 2009 Google Inc. * Copyright 2009 Google Inc.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may not * 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 * use this file except in compliance with the License. You may obtain a copy of
* the License at * the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
@@ -45,7 +45,9 @@ public class SensorManagerFactory {
if (prefs == null) { if (prefs == null) {
return null; return null;
} }
context = context.getApplicationContext();
String sensor = prefs.getString(context.getString(R.string.sensor_type_key), null); String sensor = prefs.getString(context.getString(R.string.sensor_type_key), null);
Log.i(Constants.TAG, "Creating sensor of type: " + sensor); Log.i(Constants.TAG, "Creating sensor of type: " + sensor);
@@ -57,6 +59,8 @@ public class SensorManagerFactory {
return new AntSRMSensorManager(context); return new AntSRMSensorManager(context);
} else if (sensor.equals(context.getString(R.string.zephyr_sensor_type))) { } else if (sensor.equals(context.getString(R.string.zephyr_sensor_type))) {
return new ZephyrSensorManager(context); return new ZephyrSensorManager(context);
} else if (sensor.equals(context.getString(R.string.polar_sensor_type))) {
return new PolarSensorManager(context);
} else { } else {
Log.w(Constants.TAG, "Unable to find sensor type: " + sensor); Log.w(Constants.TAG, "Unable to find sensor type: " + sensor);
return null; return null;
@@ -130,8 +130,17 @@ public abstract class AntSensorManager extends SensorManager {
@Override @Override
public void onDestroy() { public void onDestroy() {
Log.i(TAG, "destroying AntSensorManager"); Log.i(TAG, "destroying AntSensorManager");
context.unregisterReceiver(statusReceiver); try {
context.unregisterReceiver(dataReceiver); context.unregisterReceiver(statusReceiver);
} catch (IllegalArgumentException e) {
Log.w(TAG, "Failed to unregister ANT status receiver", e);
}
try {
context.unregisterReceiver(dataReceiver);
} catch (IllegalArgumentException e) {
Log.w(TAG, "Failed to unregister ANT data receiver", e);
}
try { try {
antReceiver.releaseInterface(); antReceiver.releaseInterface();
@@ -1,12 +1,12 @@
/* /*
* Copyright 2011 Google Inc. * Copyright 2011 Google Inc.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may not * 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 * use this file except in compliance with the License. You may obtain a copy of
* the License at * the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
@@ -44,34 +44,35 @@ public class FroyoStatusAnnouncerTask extends StatusAnnouncerTask {
new OnUtteranceCompletedListener() { new OnUtteranceCompletedListener() {
@Override @Override
public void onUtteranceCompleted(String utteranceId) { public void onUtteranceCompleted(String utteranceId) {
if (audioManager != null) { int result = audioManager.abandonAudioFocus(null);
Log.d(TAG, "FroyoStatusAnnouncerTask: Abandoning audio focus."); if (result == AudioManager.AUDIOFOCUS_REQUEST_FAILED) {
audioManager.abandonAudioFocus(null); Log.w(TAG, "FroyoStatusAnnouncerTask: Failed to relinquish audio focus");
} }
} }
}; };
public FroyoStatusAnnouncerTask(Context context) { public FroyoStatusAnnouncerTask(Context context) {
super(context); super(context);
audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
} }
@Override @Override
protected void onTtsInit(int status) { protected void onTtsReady() {
super.onTtsInit(status); super.onTtsReady();
if (status == TextToSpeech.SUCCESS) {
tts.setOnUtteranceCompletedListener(utteranceListener); tts.setOnUtteranceCompletedListener(utteranceListener);
}
} }
@Override @Override
protected void speakAnnouncment(String announcement) { protected synchronized void speakAnnouncement(String announcement) {
int result = audioManager.requestAudioFocus(null, int result = audioManager.requestAudioFocus(null,
TextToSpeech.Engine.DEFAULT_STREAM, TextToSpeech.Engine.DEFAULT_STREAM,
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK); AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK);
if (result == AudioManager.AUDIOFOCUS_REQUEST_FAILED) { if (result == AudioManager.AUDIOFOCUS_REQUEST_FAILED) {
Log.w(TAG, "FroyoStatusAnnouncerTask: Request audio focus failed."); Log.w(TAG, "FroyoStatusAnnouncerTask: Request for audio focus failed.");
} }
// We don't care about the utterance id. // We don't care about the utterance id.
// It is supplied here to force onUtteranceCompleted to be called. // It is supplied here to force onUtteranceCompleted to be called.
tts.speak(announcement, TextToSpeech.QUEUE_FLUSH, SPEECH_PARAMS); tts.speak(announcement, TextToSpeech.QUEUE_FLUSH, SPEECH_PARAMS);
@@ -1,12 +1,12 @@
/* /*
* Copyright 2010 Google Inc. * Copyright 2010 Google Inc.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may not * 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 * use this file except in compliance with the License. You may obtain a copy of
* the License at * the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
@@ -67,7 +67,7 @@ public class PeriodicTaskExecutor {
if (!service.isRecording()) { if (!service.isRecording()) {
return; return;
} }
if (!isTimeFrequency()) { if (!isTimeFrequency()) {
if (timerExecutor != null) { if (timerExecutor != null) {
timerExecutor.shutdown(); timerExecutor.shutdown();
@@ -77,7 +77,7 @@ public class PeriodicTaskExecutor {
if (taskFrequency == 0) { if (taskFrequency == 0) {
return; return;
} }
// Try to make the task. // Try to make the task.
task = factory.create(service); task = factory.create(service);
// Returning null is ok. // Returning null is ok.
@@ -90,7 +90,7 @@ public class PeriodicTaskExecutor {
if (timerExecutor == null) { if (timerExecutor == null) {
timerExecutor = new TimerTaskExecutor(task, service); timerExecutor = new TimerTaskExecutor(task, service);
} }
timerExecutor.scheduleTask(taskFrequency * 60000); timerExecutor.scheduleTask(taskFrequency * 60000L);
} else { } else {
// For distance based splits. // For distance based splits.
calculateNextTaskDistance(); calculateNextTaskDistance();
@@ -119,7 +119,7 @@ public class PeriodicTaskExecutor {
if (!service.isRecording() || task == null) { if (!service.isRecording() || task == null) {
return; return;
} }
if (!isDistanceFrequency()) { if (!isDistanceFrequency()) {
nextTaskDistance = Double.MAX_VALUE; nextTaskDistance = Double.MAX_VALUE;
Log.d(TAG, "SplitManager: Distance splits disabled."); Log.d(TAG, "SplitManager: Distance splits disabled.");
@@ -1,12 +1,12 @@
/* /*
* Copyright 2009 Google Inc. * Copyright 2009 Google Inc.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may not * 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 * use this file except in compliance with the License. You may obtain a copy of
* the License at * the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
@@ -66,6 +66,11 @@ public class StatusAnnouncerTask implements PeriodicTask {
/** /**
* The response received from the TTS engine after initialization. * The response received from the TTS engine after initialization.
*/ */
private int initStatus = TextToSpeech.ERROR;
/**
* Whether the TTS engine is ready.
*/
private boolean ready = false; private boolean ready = false;
/** /**
@@ -81,13 +86,14 @@ public class StatusAnnouncerTask implements PeriodicTask {
public void onCallStateChanged(int state, String incomingNumber) { public void onCallStateChanged(int state, String incomingNumber) {
speechAllowed = state == TelephonyManager.CALL_STATE_IDLE; speechAllowed = state == TelephonyManager.CALL_STATE_IDLE;
if (!speechAllowed && tts.isSpeaking()) { if (!speechAllowed && tts != null && tts.isSpeaking()) {
// If we're already speaking, stop it. // If we're already speaking, stop it.
tts.stop(); tts.stop();
} }
} }
}; };
public StatusAnnouncerTask(Context context) { public StatusAnnouncerTask(Context context) {
this(context, new StringUtils(context)); this(context, new StringUtils(context));
} }
@@ -97,33 +103,6 @@ public class StatusAnnouncerTask implements PeriodicTask {
this.stringUtils = stringUtils; this.stringUtils = stringUtils;
} }
/**
* Called when the TTS engine is initialized.
*/
protected void onTtsInit(int status) {
Log.i(TAG, "TrackRecordingService.TTS init: " + status);
// TTS should be valid here but NPE exceptions were reported to the market.
this.ready = status == TextToSpeech.SUCCESS && tts != null;
if (ready) {
// Force the language to be the same as the string we will be speaking,
// if that's available.
Locale speechLanguage = Locale.getDefault();
int languageAvailability = tts.isLanguageAvailable(speechLanguage);
if (languageAvailability == TextToSpeech.LANG_MISSING_DATA ||
languageAvailability == TextToSpeech.LANG_NOT_SUPPORTED) {
// English is probably supported.
// TODO: Somehow use announcement strings from English too.
Log.w(TAG, "Default language not available, using English.");
speechLanguage = Locale.ENGLISH;
}
tts.setLanguage(speechLanguage);
// Slow down the speed just a bit as it is hard to hear when exercising.
tts.setSpeechRate(TTS_SPEECH_RATE);
}
}
/** /**
* {@inheritDoc} * {@inheritDoc}
* *
@@ -144,12 +123,15 @@ public class StatusAnnouncerTask implements PeriodicTask {
* to avoid needing to instantiate an entire {@link TrackRecordingService} * to avoid needing to instantiate an entire {@link TrackRecordingService}
* just to test the announcer. * just to test the announcer.
*/ */
protected void runWithStatistics(TripStatistics statistics) { // @VisibleForTesting
void runWithStatistics(TripStatistics statistics) {
if (statistics == null) { if (statistics == null) {
Log.e(TAG, "StatusAnnouncer stats not initialized."); Log.e(TAG, "StatusAnnouncer stats not initialized.");
return; return;
} }
if (!ready || tts == null) {
checkReady();
if (!ready) {
Log.e(TAG, "StatusAnnouncer Tts not ready."); Log.e(TAG, "StatusAnnouncer Tts not ready.");
return; return;
} }
@@ -162,10 +144,10 @@ public class StatusAnnouncerTask implements PeriodicTask {
String announcement = getAnnouncement(statistics); String announcement = getAnnouncement(statistics);
Log.d(Constants.TAG, "Announcement: " + announcement); Log.d(Constants.TAG, "Announcement: " + announcement);
speakAnnouncment(announcement); speakAnnouncement(announcement);
} }
protected void speakAnnouncment(String announcement) { protected void speakAnnouncement(String announcement) {
tts.speak(announcement, TextToSpeech.QUEUE_FLUSH, null); tts.speak(announcement, TextToSpeech.QUEUE_FLUSH, null);
} }
@@ -181,7 +163,7 @@ public class StatusAnnouncerTask implements PeriodicTask {
boolean metricUnits = true; boolean metricUnits = true;
boolean reportSpeed = true; boolean reportSpeed = true;
if (preferences != null) { if (preferences != null) {
metricUnits = metricUnits =
preferences.getBoolean(context.getString(R.string.metric_units_key), preferences.getBoolean(context.getString(R.string.metric_units_key),
true); true);
reportSpeed = reportSpeed =
@@ -256,6 +238,59 @@ public class StatusAnnouncerTask implements PeriodicTask {
listenToPhoneState(phoneListener, PhoneStateListener.LISTEN_CALL_STATE); listenToPhoneState(phoneListener, PhoneStateListener.LISTEN_CALL_STATE);
} }
/**
* Called when the TTS engine is initialized.
*/
private void onTtsInit(int status) {
Log.i(TAG, "TrackRecordingService.TTS init: " + status);
synchronized (this) {
// TTS should be valid here but NPE exceptions were reported to the market.
initStatus = status;
checkReady();
}
}
/**
* Ensures that the TTS is ready (finishing its initialization if needed).
*/
private void checkReady() {
synchronized (this) {
if (ready) {
// Already done;
return;
}
ready = initStatus == TextToSpeech.SUCCESS && tts != null;
Log.d(TAG, "Status announcer ready: " + ready);
if (ready) {
onTtsReady();
}
}
}
/**
* Finishes the TTS engine initialization.
* Called once (and only once) when the TTS engine is ready.
*/
protected void onTtsReady() {
// Force the language to be the same as the string we will be speaking,
// if that's available.
Locale speechLanguage = Locale.getDefault();
int languageAvailability = tts.isLanguageAvailable(speechLanguage);
if (languageAvailability == TextToSpeech.LANG_MISSING_DATA ||
languageAvailability == TextToSpeech.LANG_NOT_SUPPORTED) {
// English is probably supported.
// TODO: Somehow use announcement strings from English too.
Log.w(TAG, "Default language not available, using English.");
speechLanguage = Locale.ENGLISH;
}
tts.setLanguage(speechLanguage);
// Slow down the speed just a bit as it is hard to hear when exercising.
tts.setSpeechRate(TTS_SPEECH_RATE);
}
@Override @Override
public void shutdown() { public void shutdown() {
// Stop listening to phone state. // Stop listening to phone state.
@@ -19,7 +19,6 @@ import android.app.Notification;
import android.app.NotificationManager; import android.app.NotificationManager;
import android.app.Service; import android.app.Service;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.os.HandlerThread;
/** /**
* A set of methods that may be implemented in a platform specific way. * 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, void stopForeground(Service service, NotificationManager notificationManager,
int id); 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. * Applies all changes done to the given preferences editor.
* Changes may or may not be applied immediately. * Changes may or may not be applied immediately.
@@ -16,11 +16,11 @@
package com.google.android.apps.mytracks.util; package com.google.android.apps.mytracks.util;
import static com.google.android.apps.mytracks.Constants.TAG; import static com.google.android.apps.mytracks.Constants.TAG;
import android.app.Notification; import android.app.Notification;
import android.app.NotificationManager; import android.app.NotificationManager;
import android.app.Service; import android.app.Service;
import android.content.SharedPreferences.Editor; import android.content.SharedPreferences.Editor;
import android.os.HandlerThread;
import android.util.Log; import android.util.Log;
import java.lang.reflect.InvocationTargetException; 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 @Override
public void applyPreferenceChanges(Editor editor) { public void applyPreferenceChanges(Editor editor) {
editor.commit(); editor.commit();
@@ -19,7 +19,6 @@ import android.app.Notification;
import android.app.NotificationManager; import android.app.NotificationManager;
import android.app.Service; import android.app.Service;
import android.content.SharedPreferences.Editor; import android.content.SharedPreferences.Editor;
import android.os.HandlerThread;
/** /**
* The Eclair (API level 5) specific implementation of the * The Eclair (API level 5) specific implementation of the
@@ -42,11 +41,6 @@ public class EclairPlatformAdapter implements ApiPlatformAdapter {
service.stopForeground(id != -1); service.stopForeground(id != -1);
} }
@Override
public boolean stopHandlerThread(HandlerThread handlerThread) {
return handlerThread.quit();
}
@Override @Override
public void applyPreferenceChanges(Editor editor) { public void applyPreferenceChanges(Editor editor) {
editor.commit(); editor.commit();
@@ -1,12 +1,12 @@
/* /*
* Copyright 2008 Google Inc. * Copyright 2008 Google Inc.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may not * 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 * use this file except in compliance with the License. You may obtain a copy of
* the License at * the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
@@ -57,10 +57,11 @@ public class ResourceUtils {
public static void readBinaryFileToOutputStream( public static void readBinaryFileToOutputStream(
Context activity, int id, OutputStream os) { Context activity, int id, OutputStream os) {
BufferedInputStream in = null; BufferedInputStream in = null;
BufferedOutputStream out = null;
try { try {
in = new BufferedInputStream( in = new BufferedInputStream(
activity.getResources().openRawResource(id)); activity.getResources().openRawResource(id));
BufferedOutputStream out = new BufferedOutputStream(os); out = new BufferedOutputStream(os);
int b; int b;
while ((b = in.read()) != -1) { while ((b = in.read()) != -1) {
out.write(b); out.write(b);
@@ -76,6 +77,14 @@ public class ResourceUtils {
// Ignore // Ignore
} }
} }
if (out != null) {
try {
out.close();
} catch (IOException e) {
// Ignore
}
}
} }
} }
@@ -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<String> uriPathSegments = uri.getPathSegments();
List<String> 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() {}
}
@@ -1,12 +1,12 @@
/* /*
* Copyright 2008 Google Inc. * Copyright 2008 Google Inc.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may not * 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 * use this file except in compliance with the License. You may obtain a copy of
* the License at * the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
@@ -66,17 +66,6 @@ interface ITrackRecordingService {
*/ */
void endCurrentTrack(); 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 current sensor data.
* The data is returned as a byte array which is a binary version of a * The data is returned as a byte array which is a binary version of a
@@ -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<MyTracks> {
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);
}
}
@@ -1,12 +1,12 @@
/* /*
* Copyright 2010 Google Inc. * Copyright 2010 Google Inc.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may not * 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 * use this file except in compliance with the License. You may obtain a copy of
* the License at * the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
@@ -15,6 +15,8 @@
*/ */
package com.google.android.apps.mytracks; 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 com.google.android.maps.mytracks.R;
import android.app.Activity; import android.app.Activity;
@@ -32,60 +34,53 @@ import java.util.concurrent.atomic.AtomicBoolean;
/** /**
* A unit test for {@link MyTracks} activity. * A unit test for {@link MyTracks} activity.
* *
* @author Bartlomiej Niechwiej * @author Bartlomiej Niechwiej
*/ */
public class MyTracksTest extends ActivityInstrumentationTestCase2<MyTracks>{ public class MyTracksTest extends ActivityInstrumentationTestCase2<MyTracks>{
private SharedPreferences sharedPreferences;
private TrackRecordingServiceConnection serviceConnection;
public MyTracksTest() { public MyTracksTest() {
super(MyTracks.class); super(MyTracks.class);
} }
@Override
protected void setUp() throws Exception {
super.setUp();
MyTracks.clearInstance();
assertNull(MyTracks.getInstance());
}
@Override @Override
protected void tearDown() throws Exception { protected void tearDown() throws Exception {
clearSelectedAndRecordingTracks(); clearSelectedAndRecordingTracks();
waitForIdle(); waitForIdle();
super.tearDown(); super.tearDown();
} }
public void testInitialization_mainAction() { public void testInitialization_mainAction() {
// Make sure we can start MyTracks and the activity doesn't start recording. // Make sure we can start MyTracks and the activity doesn't start recording.
assertNotNull(getActivity()); assertInitialized();
assertNotNull(MyTracks.getInstance());
assertNotNull(getActivity().getSharedPreferences());
// Check if not recording. // Check if not recording.
assertFalse(getActivity().isRecording()); assertFalse(isRecording());
assertEquals(-1, getActivity().getRecordingTrackId()); assertEquals(-1, getRecordingTrackId());
long selectedTrackId = getActivity().getSharedPreferences().getLong( long selectedTrackId = getSharedPreferences().getLong(
getActivity().getString(R.string.selected_track_key), -1); getActivity().getString(R.string.selected_track_key), -1);
assertEquals(selectedTrackId, getActivity().getSelectedTrackId()); assertEquals(selectedTrackId, getActivity().getSelectedTrackId());
} }
public void testInitialization_viewActionWithNoData() { public void testInitialization_viewActionWithNoData() {
// Simulate start with ACTION_VIEW intent. // Simulate start with ACTION_VIEW intent.
Intent startIntent = new Intent(); Intent startIntent = new Intent();
startIntent.setAction(Intent.ACTION_VIEW); startIntent.setAction(Intent.ACTION_VIEW);
setActivityIntent(startIntent); setActivityIntent(startIntent);
assertNotNull(getActivity()); assertInitialized();
assertNotNull(MyTracks.getInstance());
assertNotNull(getActivity().getSharedPreferences());
// Check if not recording. // Check if not recording.
assertFalse(getActivity().isRecording()); assertFalse(isRecording());
assertEquals(-1, getActivity().getRecordingTrackId()); assertEquals(-1, getRecordingTrackId());
long selectedTrackId = getActivity().getSharedPreferences().getLong( long selectedTrackId = getSharedPreferences().getLong(
getActivity().getString(R.string.selected_track_key), -1); getActivity().getString(R.string.selected_track_key), -1);
assertEquals(selectedTrackId, getActivity().getSelectedTrackId()); assertEquals(selectedTrackId, getActivity().getSelectedTrackId());
} }
public void testInitialization_viewActionWithValidData() throws Exception { public void testInitialization_viewActionWithValidData() throws Exception {
// Simulate start with ACTION_VIEW intent. // Simulate start with ACTION_VIEW intent.
Intent startIntent = new Intent(); Intent startIntent = new Intent();
@@ -93,24 +88,22 @@ public class MyTracksTest extends ActivityInstrumentationTestCase2<MyTracks>{
Uri uri = Uri.fromFile(File.createTempFile("valid", ".gpx")); Uri uri = Uri.fromFile(File.createTempFile("valid", ".gpx"));
// TODO: Add a valid GPX. // TODO: Add a valid GPX.
startIntent.setData(uri); startIntent.setData(uri);
setActivityIntent(startIntent); setActivityIntent(startIntent);
assertNotNull(getActivity()); assertInitialized();
assertNotNull(MyTracks.getInstance());
assertNotNull(getActivity().getSharedPreferences());
// Check if not recording. // Check if not recording.
assertFalse(getActivity().isRecording()); assertFalse(isRecording());
assertEquals(-1, getActivity().getRecordingTrackId()); assertEquals(-1, getRecordingTrackId());
long selectedTrackId = getActivity().getSharedPreferences().getLong( long selectedTrackId = getSharedPreferences().getLong(
getActivity().getString(R.string.selected_track_key), -1); getActivity().getString(R.string.selected_track_key), -1);
assertEquals(selectedTrackId, getActivity().getSelectedTrackId()); assertEquals(selectedTrackId, getActivity().getSelectedTrackId());
// TODO: Finish this test. // TODO: Finish this test.
} }
public void testInitialization_viewActionWithInvalidData() throws Exception { public void testInitialization_viewActionWithInvalidData() throws Exception {
// Simulate start with ACTION_VIEW intent. // Simulate start with ACTION_VIEW intent.
Intent startIntent = new Intent(); Intent startIntent = new Intent();
@@ -118,66 +111,62 @@ public class MyTracksTest extends ActivityInstrumentationTestCase2<MyTracks>{
Uri uri = Uri.fromFile(File.createTempFile("invalid", ".gpx")); Uri uri = Uri.fromFile(File.createTempFile("invalid", ".gpx"));
startIntent.setData(uri); startIntent.setData(uri);
setActivityIntent(startIntent); setActivityIntent(startIntent);
assertNotNull(getActivity()); assertInitialized();
assertNotNull(MyTracks.getInstance());
assertNotNull(getActivity().getSharedPreferences());
// Check if not recording. // Check if not recording.
assertFalse(getActivity().isRecording()); assertFalse(isRecording());
assertEquals(-1, getActivity().getRecordingTrackId()); assertEquals(-1, getRecordingTrackId());
long selectedTrackId = getActivity().getSharedPreferences().getLong( long selectedTrackId = getSharedPreferences().getLong(
getActivity().getString(R.string.selected_track_key), -1); getActivity().getString(R.string.selected_track_key), -1);
assertEquals(selectedTrackId, getActivity().getSelectedTrackId()); assertEquals(selectedTrackId, getActivity().getSelectedTrackId());
// TODO: Finish this test. // TODO: Finish this test.
} }
public void testRecording_startAndStop() throws Exception { public void testRecording_startAndStop() throws Exception {
// Make sure we can start MyTracks and the activity doesn't start recording. assertInitialized();
assertNotNull(getActivity());
assertNotNull(MyTracks.getInstance());
assertNotNull(getActivity().getSharedPreferences());
// Check if not recording. // Check if not recording.
clearSelectedAndRecordingTracks(); clearSelectedAndRecordingTracks();
waitForIdle(); waitForIdle();
assertFalse(getActivity().isRecording()); assertFalse(isRecording());
assertEquals(-1, getActivity().getRecordingTrackId()); assertEquals(-1, getRecordingTrackId());
long selectedTrackId = getActivity().getSharedPreferences().getLong( long selectedTrackId = getSharedPreferences().getLong(
getActivity().getString(R.string.selected_track_key), -1); getActivity().getString(R.string.selected_track_key), -1);
assertEquals(selectedTrackId, getActivity().getSelectedTrackId()); assertEquals(selectedTrackId, getActivity().getSelectedTrackId());
// Start a new track. // Start a new track.
getActivity().startRecording(); getActivity().startRecording();
serviceConnection.bindIfRunning();
long recordingTrackId = awaitRecordingStatus(5000, true); long recordingTrackId = awaitRecordingStatus(5000, true);
assertTrue(recordingTrackId >= 0); assertTrue(recordingTrackId >= 0);
// Wait until we are done and make sure that selectedTrack = recordingTrack. // Wait until we are done and make sure that selectedTrack = recordingTrack.
waitForIdle(); waitForIdle();
assertEquals(recordingTrackId, getActivity().getSharedPreferences().getLong( assertEquals(recordingTrackId, getSharedPreferences().getLong(
getActivity().getString(R.string.recording_track_key), -1)); getActivity().getString(R.string.recording_track_key), -1));
selectedTrackId = getActivity().getSharedPreferences().getLong( selectedTrackId = getSharedPreferences().getLong(
getActivity().getString(R.string.selected_track_key), -1); getActivity().getString(R.string.selected_track_key), -1);
assertEquals(recordingTrackId, selectedTrackId); assertEquals(recordingTrackId, selectedTrackId);
assertEquals(selectedTrackId, getActivity().getSelectedTrackId()); assertEquals(selectedTrackId, getActivity().getSelectedTrackId());
// Watch for MyTracksDetails activity. // Watch for MyTracksDetails activity.
ActivityMonitor monitor = getInstrumentation().addMonitor( ActivityMonitor monitor = getInstrumentation().addMonitor(
TrackDetails.class.getName(), null, false); TrackDetails.class.getName(), null, false);
// Now, stop the track and make sure that it is still selected, but // Now, stop the track and make sure that it is still selected, but
// no longer recording. // no longer recording.
getActivity().stopRecording(); getActivity().stopRecording();
// Check if we got back MyTracksDetails activity. // Check if we got back MyTracksDetails activity.
Activity activity = getInstrumentation().waitForMonitor(monitor); Activity activity = getInstrumentation().waitForMonitor(monitor);
assertTrue(activity instanceof TrackDetails); assertTrue(activity instanceof TrackDetails);
// TODO: Update track name and other properties and test if they were // TODO: Update track name and other properties and test if they were
// properly saved. // properly saved.
// Simulate a click on Save button. // Simulate a click on Save button.
Button save = (Button) activity.findViewById(R.id.trackdetails_save); Button save = (Button) activity.findViewById(R.id.trackdetails_save);
save.performClick(); save.performClick();
@@ -185,18 +174,24 @@ public class MyTracksTest extends ActivityInstrumentationTestCase2<MyTracks>{
// Check the remaining properties. // Check the remaining properties.
recordingTrackId = awaitRecordingStatus(5000, false); recordingTrackId = awaitRecordingStatus(5000, false);
assertEquals(-1, recordingTrackId); assertEquals(-1, recordingTrackId);
assertEquals(recordingTrackId, getActivity().getRecordingTrackId()); assertEquals(recordingTrackId, getRecordingTrackId());
assertEquals(recordingTrackId, getActivity().getSharedPreferences().getLong( assertEquals(recordingTrackId, getSharedPreferences().getLong(
getActivity().getString(R.string.recording_track_key), -1)); getActivity().getString(R.string.recording_track_key), -1));
// Make sure this is the same track as the last recording track ID. // Make sure this is the same track as the last recording track ID.
assertEquals(selectedTrackId, getActivity().getSelectedTrackId()); assertEquals(selectedTrackId, getActivity().getSelectedTrackId());
} }
private void assertInitialized() {
assertNotNull(getActivity());
serviceConnection = new TrackRecordingServiceConnection(getActivity(), null);
}
/** /**
* Waits until the UI thread becomes idle. * Waits until the UI thread becomes idle.
*/ */
private void waitForIdle() throws InterruptedException { 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(); final Object semaphore = new Object();
synchronized (semaphore) { synchronized (semaphore) {
final AtomicBoolean isIdle = new AtomicBoolean(); final AtomicBoolean isIdle = new AtomicBoolean();
@@ -216,20 +211,20 @@ public class MyTracksTest extends ActivityInstrumentationTestCase2<MyTracks>{
} }
/** /**
* Clears {selected,recording}TrackId in the {@link SharedPreferences}. * Clears {selected,recording}TrackId in the {@link getSharedPreferences()}.
*/ */
private void clearSelectedAndRecordingTracks() { 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.selected_track_key), -1);
editor.putLong(getActivity().getString(R.string.recording_track_key), -1); editor.putLong(getActivity().getString(R.string.recording_track_key), -1);
editor.clear(); editor.clear();
editor.commit(); editor.commit();
} }
/** /**
* Waits until the recording state changes to the given status. * Waits until the recording state changes to the given status.
* *
* @param timeout the maximum time to wait, in milliseconds. * @param timeout the maximum time to wait, in milliseconds.
* @param isRecording the final status to await. * @param isRecording the final status to await.
* @return the recording track ID. * @return the recording track ID.
@@ -237,14 +232,30 @@ public class MyTracksTest extends ActivityInstrumentationTestCase2<MyTracks>{
private long awaitRecordingStatus(long timeout, boolean isRecording) private long awaitRecordingStatus(long timeout, boolean isRecording)
throws TimeoutException, InterruptedException { throws TimeoutException, InterruptedException {
long startTime = System.nanoTime(); long startTime = System.nanoTime();
while (getActivity().isRecording() != isRecording) { while (isRecording() != isRecording) {
if (System.nanoTime() - startTime > timeout * 1000000) { if (System.nanoTime() - startTime > timeout * 1000000) {
throw new TimeoutException("Timeout while waiting for recording!"); throw new TimeoutException("Timeout while waiting for recording!");
} }
Thread.sleep(20); Thread.sleep(20);
} }
waitForIdle(); waitForIdle();
assertEquals(isRecording, getActivity().isRecording()); assertEquals(isRecording, isRecording());
return getActivity().getRecordingTrackId(); 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());
} }
} }
@@ -91,7 +91,12 @@ public class TrackDataHubTest extends AndroidTestCase {
dataSources = AndroidMock.createNiceMock("dataSources", DataSourcesWrapper.class); dataSources = AndroidMock.createNiceMock("dataSources", DataSourcesWrapper.class);
listeners = new TrackDataListeners(); 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 @Override
protected void runInListenerThread(Runnable runnable) { protected void runInListenerThread(Runnable runnable) {
// Run everything in the same thread. // Run everything in the same thread.
@@ -124,7 +129,7 @@ public class TrackDataHubTest extends AndroidTestCase {
AndroidMock.replay(dataSources); AndroidMock.replay(dataSources);
hub.stop(); hub.stop();
hub.destroy(); hub = null;
super.tearDown(); super.tearDown();
} }
@@ -13,10 +13,7 @@
* License for the specific language governing permissions and limitations under * License for the specific language governing permissions and limitations under
* the License. * 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.io.file.TrackWriter;
import android.app.ProgressDialog; import android.app.ProgressDialog;
import android.test.ActivityInstrumentationTestCase2; import android.test.ActivityInstrumentationTestCase2;
@@ -29,9 +26,9 @@ import java.util.concurrent.atomic.AtomicReference;
* *
* @author Matthew Simmons * @author Matthew Simmons
*/ */
public class WriteProgressControllerTest extends ActivityInstrumentationTestCase2<MyTracks> { public class WriteProgressControllerTest extends ActivityInstrumentationTestCase2<SaveActivity> {
public WriteProgressControllerTest() { public WriteProgressControllerTest() {
super(MyTracks.class); super(SaveActivity.class);
} }
private static void assertProgress(ProgressDialog dialog, int expectedProgress, private static void assertProgress(ProgressDialog dialog, int expectedProgress,
@@ -57,15 +54,15 @@ public class WriteProgressControllerTest extends ActivityInstrumentationTestCase
}; };
WriteProgressController controller = new WriteProgressController( WriteProgressController controller = new WriteProgressController(
getActivity(), mockWriter); getActivity(), mockWriter, SaveActivity.PROGRESS_DIALOG);
controller.setOnCompletionListener(new WriteProgressController.OnCompletionListener() { controller.setOnCompletionListener(new WriteProgressController.OnCompletionListener() {
@Override @Override
public void onComplete(TrackWriter writer) { public void onComplete() {
controllerDoneRef.set(true); controllerDoneRef.set(true);
} }
}); });
dialogRef.set(controller.getDialog()); dialogRef.set(controller.createProgressDialog());
controller.startWrite(); controller.startWrite();
@@ -15,7 +15,6 @@
*/ */
package com.google.android.apps.mytracks.io.sendtogoogle; package com.google.android.apps.mytracks.io.sendtogoogle;
import com.google.android.apps.mytracks.MyTracks;
import com.google.android.maps.mytracks.R; import com.google.android.maps.mytracks.R;
import android.app.AlertDialog; import android.app.AlertDialog;
@@ -33,9 +32,9 @@ import java.util.List;
* *
* @author Matthew Simmons * @author Matthew Simmons
*/ */
public class ResultDialogFactoryTest extends ActivityInstrumentationTestCase2<MyTracks> { public class ResultDialogFactoryTest extends ActivityInstrumentationTestCase2<SendActivity> {
public ResultDialogFactoryTest() { public ResultDialogFactoryTest() {
super(MyTracks.class); super(SendActivity.class);
} }
private List<SendResult> makeResults(SendResult... results) { private List<SendResult> makeResults(SendResult... results) {
@@ -55,7 +54,7 @@ public class ResultDialogFactoryTest extends ActivityInstrumentationTestCase2<My
List<SendResult> results = makeResults(new SendResult(SendType.MYMAPS, true), List<SendResult> results = makeResults(new SendResult(SendType.MYMAPS, true),
new SendResult(SendType.DOCS, true)); new SendResult(SendType.DOCS, true));
AlertDialog dialog = ResultDialogFactory.makeDialog(getActivity(), results, clickListener, null); AlertDialog dialog = ResultDialogFactory.makeDialog(getActivity(), results, clickListener, null, null);
dialog.show(); dialog.show();
ListView listView = (ListView) dialog.findViewById(R.id.send_to_google_result_list); ListView listView = (ListView) dialog.findViewById(R.id.send_to_google_result_list);
@@ -87,7 +86,7 @@ public class ResultDialogFactoryTest extends ActivityInstrumentationTestCase2<My
new SendResult(SendType.DOCS, true)); new SendResult(SendType.DOCS, true));
AlertDialog dialog = ResultDialogFactory.makeDialog(getActivity(), results, AlertDialog dialog = ResultDialogFactory.makeDialog(getActivity(), results,
clickListener, clickListener); clickListener, clickListener, null);
dialog.show(); dialog.show();
assertEquals(View.VISIBLE, dialog.getButton(AlertDialog.BUTTON_POSITIVE).getVisibility()); assertEquals(View.VISIBLE, dialog.getButton(AlertDialog.BUTTON_POSITIVE).getVisibility());
@@ -98,7 +97,7 @@ public class ResultDialogFactoryTest extends ActivityInstrumentationTestCase2<My
List<SendResult> results = makeResults(new SendResult(SendType.MYMAPS, true), List<SendResult> results = makeResults(new SendResult(SendType.MYMAPS, true),
new SendResult(SendType.DOCS, false)); new SendResult(SendType.DOCS, false));
AlertDialog dialog = ResultDialogFactory.makeDialog(getActivity(), results, clickListener, null); AlertDialog dialog = ResultDialogFactory.makeDialog(getActivity(), results, clickListener, null, null);
dialog.show(); dialog.show();
assertEquals(View.GONE, assertEquals(View.GONE,
@@ -112,7 +111,7 @@ public class ResultDialogFactoryTest extends ActivityInstrumentationTestCase2<My
new SendResult(SendType.DOCS, false)); new SendResult(SendType.DOCS, false));
AlertDialog dialog = ResultDialogFactory.makeDialog(getActivity(), results, AlertDialog dialog = ResultDialogFactory.makeDialog(getActivity(), results,
clickListener, clickListener); clickListener, clickListener, null);
dialog.show(); dialog.show();
assertEquals(View.VISIBLE, dialog.getButton(AlertDialog.BUTTON_POSITIVE).getVisibility()); assertEquals(View.VISIBLE, dialog.getButton(AlertDialog.BUTTON_POSITIVE).getVisibility());
@@ -49,9 +49,9 @@ import java.util.List;
/** /**
* Tests for the MyTracks track recording service. * Tests for the MyTracks track recording service.
* *
* @author Bartlomiej Niechwiej * @author Bartlomiej Niechwiej
* *
* TODO: The original class, ServiceTestCase, has a few limitations, e.g. * TODO: The original class, ServiceTestCase, has a few limitations, e.g.
* it's not possible to properly shutdown the service, unless tearDown() * it's not possible to properly shutdown the service, unless tearDown()
* is called, which prevents from testing multiple scenarios in a single * is called, which prevents from testing multiple scenarios in a single
@@ -63,7 +63,7 @@ public class TrackRecordingServiceTest
private Context context; private Context context;
private MyTracksProviderUtils providerUtils; private MyTracksProviderUtils providerUtils;
private SharedPreferences sharedPreferences; private SharedPreferences sharedPreferences;
/* /*
* In order to support starting and binding to the service in the same * 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 * unit test, we provide a workaround, as the original class doesn't allow
@@ -72,19 +72,19 @@ public class TrackRecordingServiceTest
private boolean bound; private boolean bound;
private Intent serviceIntent; private Intent serviceIntent;
public TrackRecordingServiceTest() { public TrackRecordingServiceTest() {
super(TrackRecordingService.class); super(TrackRecordingService.class);
} }
/** /**
* A context wrapper with the user provided {@link ContentResolver}. * A context wrapper with the user provided {@link ContentResolver}.
* *
* TODO: Move to test utils package. * TODO: Move to test utils package.
*/ */
public static class MockContext extends ContextWrapper { public static class MockContext extends ContextWrapper {
private final ContentResolver contentResolver; private final ContentResolver contentResolver;
public MockContext(ContentResolver contentResolver, Context base) { public MockContext(ContentResolver contentResolver, Context base) {
super(base); super(base);
this.contentResolver = contentResolver; this.contentResolver = contentResolver;
@@ -108,7 +108,7 @@ public class TrackRecordingServiceTest
return 4; return 4;
} }
} }
@Override @Override
protected IBinder bindService(Intent intent) { protected IBinder bindService(Intent intent) {
if (getService() != null) { if (getService() != null) {
@@ -190,7 +190,7 @@ public class TrackRecordingServiceTest
startService(startIntent); startService(startIntent);
assertNotNull(getService()); assertNotNull(getService());
// We expect to resume the previous track. // We expect to resume the previous track.
assertTrue(getService().isRecording()); assertTrue(getService().isRecording());
ITrackRecordingService service = bindAndGetService(createStartIntent()); ITrackRecordingService service = bindAndGetService(createStartIntent());
assertEquals(123, service.getRecordingTrackId()); assertEquals(123, service.getRecordingTrackId());
@@ -293,7 +293,7 @@ public class TrackRecordingServiceTest
assertTrue(tracks.isEmpty()); assertTrue(tracks.isEmpty());
ITrackRecordingService service = bindAndGetService(createStartIntent()); 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()); assertFalse(service.isRecording());
assertEquals(-1, service.getRecordingTrackId()); assertEquals(-1, service.getRecordingTrackId());
} }
@@ -463,92 +463,19 @@ public class TrackRecordingServiceTest
context.getString(R.string.recording_track_key), 0)); context.getString(R.string.recording_track_key), 0));
assertEquals(-1, service.getRecordingTrackId()); assertEquals(-1, service.getRecordingTrackId());
} }
@MediumTest @MediumTest
public void testIntegration_completeRecordingSession() throws Exception { public void testIntegration_completeRecordingSession() throws Exception {
List<Track> tracks = providerUtils.getAllTracks(); List<Track> tracks = providerUtils.getAllTracks();
assertTrue(tracks.isEmpty()); assertTrue(tracks.isEmpty());
fullRecordingSession(); 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 @MediumTest
public void testInsertStatisticsMarker_noRecordingTrack() throws Exception { public void testInsertStatisticsMarker_noRecordingTrack() throws Exception {
ITrackRecordingService service = bindAndGetService(createStartIntent()); ITrackRecordingService service = bindAndGetService(createStartIntent());
assertFalse(service.isRecording()); assertFalse(service.isRecording());
try { try {
service.insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS); service.insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS);
fail("Expecting IllegalStateException"); fail("Expecting IllegalStateException");
@@ -556,17 +483,17 @@ public class TrackRecordingServiceTest
// Expected. // Expected.
} }
} }
@MediumTest @MediumTest
public void testInsertStatisticsMarker_validLocation() throws Exception { public void testInsertStatisticsMarker_validLocation() throws Exception {
createDummyTrack(123, -1, true); createDummyTrack(123, -1, true);
ITrackRecordingService service = bindAndGetService(createStartIntent()); ITrackRecordingService service = bindAndGetService(createStartIntent());
assertTrue(service.isRecording()); assertTrue(service.isRecording());
assertEquals(1, service.insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS)); assertEquals(1, service.insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS));
assertEquals(2, service.insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS)); assertEquals(2, service.insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS));
Waypoint wpt = providerUtils.getWaypoint(1); Waypoint wpt = providerUtils.getWaypoint(1);
assertEquals(getContext().getString(R.string.stats_icon_url), assertEquals(getContext().getString(R.string.stats_icon_url),
wpt.getIcon()); wpt.getIcon());
@@ -586,7 +513,7 @@ public class TrackRecordingServiceTest
public void testInsertWaypointMarker_noRecordingTrack() throws Exception { public void testInsertWaypointMarker_noRecordingTrack() throws Exception {
ITrackRecordingService service = bindAndGetService(createStartIntent()); ITrackRecordingService service = bindAndGetService(createStartIntent());
assertFalse(service.isRecording()); assertFalse(service.isRecording());
try { try {
service.insertWaypoint(WaypointCreationRequest.DEFAULT_MARKER); service.insertWaypoint(WaypointCreationRequest.DEFAULT_MARKER);
fail("Expecting IllegalStateException"); fail("Expecting IllegalStateException");
@@ -594,14 +521,14 @@ public class TrackRecordingServiceTest
// Expected. // Expected.
} }
} }
@MediumTest @MediumTest
public void testInsertWaypointMarker_validWaypoint() throws Exception { public void testInsertWaypointMarker_validWaypoint() throws Exception {
createDummyTrack(123, -1, true); createDummyTrack(123, -1, true);
ITrackRecordingService service = bindAndGetService(createStartIntent()); ITrackRecordingService service = bindAndGetService(createStartIntent());
assertTrue(service.isRecording()); assertTrue(service.isRecording());
assertEquals(1, service.insertWaypoint(WaypointCreationRequest.DEFAULT_MARKER)); assertEquals(1, service.insertWaypoint(WaypointCreationRequest.DEFAULT_MARKER));
Waypoint wpt = providerUtils.getWaypoint(1); Waypoint wpt = providerUtils.getWaypoint(1);
assertEquals(getContext().getString(R.string.waypoint_icon_url), assertEquals(getContext().getString(R.string.waypoint_icon_url),
@@ -614,7 +541,7 @@ public class TrackRecordingServiceTest
assertNotNull(wpt.getLocation()); assertNotNull(wpt.getLocation());
assertNull(wpt.getStatistics()); assertNull(wpt.getStatistics());
} }
@MediumTest @MediumTest
public void testWithProperties_noAnnouncementFreq() throws Exception { public void testWithProperties_noAnnouncementFreq() throws Exception {
functionalTest(R.string.announcement_frequency_key, (Object) null); functionalTest(R.string.announcement_frequency_key, (Object) null);
@@ -624,7 +551,7 @@ public class TrackRecordingServiceTest
public void testWithProperties_defaultAnnouncementFreq() throws Exception { public void testWithProperties_defaultAnnouncementFreq() throws Exception {
functionalTest(R.string.announcement_frequency_key, 1); functionalTest(R.string.announcement_frequency_key, 1);
} }
@MediumTest @MediumTest
public void testWithProperties_noMaxRecordingDist() throws Exception { public void testWithProperties_noMaxRecordingDist() throws Exception {
functionalTest(R.string.max_recording_distance_key, (Object) null); functionalTest(R.string.max_recording_distance_key, (Object) null);
@@ -654,7 +581,7 @@ public class TrackRecordingServiceTest
public void testWithProperties_defaultSplitFreqByDist() throws Exception { public void testWithProperties_defaultSplitFreqByDist() throws Exception {
functionalTest(R.string.split_frequency_key, 5); functionalTest(R.string.split_frequency_key, 5);
} }
@MediumTest @MediumTest
public void testWithProperties_defaultSplitFreqByTime() throws Exception { public void testWithProperties_defaultSplitFreqByTime() throws Exception {
functionalTest(R.string.split_frequency_key, -2); functionalTest(R.string.split_frequency_key, -2);
@@ -674,7 +601,7 @@ public class TrackRecordingServiceTest
public void testWithProperties_metricUnitsDisabled() throws Exception { public void testWithProperties_metricUnitsDisabled() throws Exception {
functionalTest(R.string.metric_units_key, false); functionalTest(R.string.metric_units_key, false);
} }
@MediumTest @MediumTest
public void testWithProperties_noMinRecordingInterval() throws Exception { public void testWithProperties_noMinRecordingInterval() throws Exception {
functionalTest(R.string.min_recording_interval_key, (Object) null); functionalTest(R.string.min_recording_interval_key, (Object) null);
@@ -690,23 +617,23 @@ public class TrackRecordingServiceTest
public void testWithProperties_noMinRequiredAccuracy() throws Exception { public void testWithProperties_noMinRequiredAccuracy() throws Exception {
functionalTest(R.string.min_required_accuracy_key, (Object) null); functionalTest(R.string.min_required_accuracy_key, (Object) null);
} }
@MediumTest @MediumTest
public void testWithProperties_defaultMinRequiredAccuracy() throws Exception { public void testWithProperties_defaultMinRequiredAccuracy() throws Exception {
functionalTest(R.string.min_required_accuracy_key, 500); functionalTest(R.string.min_required_accuracy_key, 500);
} }
@MediumTest @MediumTest
public void testWithProperties_noSensorType() throws Exception { public void testWithProperties_noSensorType() throws Exception {
functionalTest(R.string.sensor_type_key, (Object) null); functionalTest(R.string.sensor_type_key, (Object) null);
} }
@MediumTest @MediumTest
public void testWithProperties_zephyrSensorType() throws Exception { public void testWithProperties_zephyrSensorType() throws Exception {
functionalTest(R.string.sensor_type_key, 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) { private ITrackRecordingService bindAndGetService(Intent intent) {
ITrackRecordingService service = ITrackRecordingService.Stub.asInterface( ITrackRecordingService service = ITrackRecordingService.Stub.asInterface(
bindService(intent)); bindService(intent));
@@ -724,7 +651,7 @@ public class TrackRecordingServiceTest
addTrack(dummyTrack, isRecording); addTrack(dummyTrack, isRecording);
return dummyTrack; return dummyTrack;
} }
private void updateAutoResumePrefs(int attempts, int timeoutMins) { private void updateAutoResumePrefs(int attempts, int timeoutMins) {
Editor editor = sharedPreferences.edit(); Editor editor = sharedPreferences.edit();
editor.putInt(context.getString( editor.putInt(context.getString(
@@ -733,20 +660,20 @@ public class TrackRecordingServiceTest
R.string.auto_resume_track_timeout_key), timeoutMins); R.string.auto_resume_track_timeout_key), timeoutMins);
editor.commit(); editor.commit();
} }
private Intent createStartIntent() { private Intent createStartIntent() {
Intent startIntent = new Intent(); Intent startIntent = new Intent();
startIntent.setClass(context, TrackRecordingService.class); startIntent.setClass(context, TrackRecordingService.class);
return startIntent; return startIntent;
} }
private void addTrack(Track track, boolean isRecording) { private void addTrack(Track track, boolean isRecording) {
assertTrue(track.getId() >= 0); assertTrue(track.getId() >= 0);
providerUtils.insertTrack(track); providerUtils.insertTrack(track);
assertEquals(track.getId(), providerUtils.getTrack(track.getId()).getId()); assertEquals(track.getId(), providerUtils.getTrack(track.getId()).getId());
setRecordingTrack(isRecording ? track.getId() : -1); setRecordingTrack(isRecording ? track.getId() : -1);
} }
private void setRecordingTrack(long id) { private void setRecordingTrack(long id) {
Editor editor = sharedPreferences.edit(); Editor editor = sharedPreferences.edit();
editor.putLong(context.getString(R.string.recording_track_key), id); editor.putLong(context.getString(R.string.recording_track_key), id);
@@ -760,11 +687,11 @@ public class TrackRecordingServiceTest
throws Exception { throws Exception {
final String key = context.getString(resourceId); final String key = context.getString(resourceId);
for (Object value : values) { 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 editor = sharedPreferences.edit();
editor.clear(); editor.clear();
if (value instanceof String) { if (value instanceof String) {
editor.putString(key, (String) value); editor.putString(key, (String) value);
} else if (value instanceof Long) { } else if (value instanceof Long) {
editor.putLong(key, (Long) value); editor.putLong(key, (Long) value);
} else if (value instanceof Integer) { } else if (value instanceof Integer) {
@@ -775,15 +702,15 @@ public class TrackRecordingServiceTest
// Do nothing, as clear above has already removed this property. // Do nothing, as clear above has already removed this property.
} }
editor.commit(); editor.commit();
fullRecordingSession(); fullRecordingSession();
} }
} }
private void fullRecordingSession() throws Exception { private void fullRecordingSession() throws Exception {
ITrackRecordingService service = bindAndGetService(createStartIntent()); ITrackRecordingService service = bindAndGetService(createStartIntent());
assertFalse(service.isRecording()); assertFalse(service.isRecording());
// Start a track. // Start a track.
long id = service.startNewTrack(); long id = service.startNewTrack();
assertTrue(id >= 0); assertTrue(id >= 0);
@@ -806,14 +733,14 @@ public class TrackRecordingServiceTest
loc.setTime(startTime + i * 10000); loc.setTime(startTime + i * 10000);
loc.setBearing(3.0f); loc.setBearing(3.0f);
service.recordLocation(loc); service.recordLocation(loc);
if (i % 10 == 0) { if (i % 10 == 0) {
service.insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS); service.insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS);
} else if (i % 7 == 0) { } else if (i % 7 == 0) {
service.insertWaypoint(WaypointCreationRequest.DEFAULT_MARKER); service.insertWaypoint(WaypointCreationRequest.DEFAULT_MARKER);
} }
} }
// Stop the track. Validate if it has correct data. // Stop the track. Validate if it has correct data.
service.endCurrentTrack(); service.endCurrentTrack();
assertFalse(service.isRecording()); assertFalse(service.isRecording());
@@ -0,0 +1,70 @@
package com.google.android.apps.mytracks.services.sensors;
import java.util.Arrays;
import com.google.android.apps.mytracks.content.Sensor;
import junit.framework.TestCase;
public class PolarMessageParserTest extends TestCase {
PolarMessageParser parser = new PolarMessageParser();
// A complete and valid Polar HxM packet
// FE08F701D1001104FE08F702D1001104
private final byte[] originalBuf =
{(byte) 0xFE, 0x08, (byte) 0xF7, 0x01, (byte) 0xD1, 0x00, 0x11, 0x04, (byte) 0xFE, 0x08,
(byte) 0xF7, 0x02, (byte) 0xD1, 0x00, 0x11, 0x04};
private byte[] buf;
public void setUp() {
buf = Arrays.copyOf(originalBuf, originalBuf.length);
}
public void testIsValid() {
assertTrue(parser.isValid(buf));
}
public void testIsValid_invalidHeader() {
// Invalidate header.
buf[0] = 0x03;
assertFalse(parser.isValid(buf));
}
public void testIsValid_invalidCheckbyte() {
// Invalidate checkbyte.
buf[2] = 0x03;
assertFalse(parser.isValid(buf));
}
public void testIsValid_invalidSequence() {
// Invalidate sequence.
buf[3] = 0x11;
assertFalse(parser.isValid(buf));
}
public void testParseBuffer() {
buf[5] = 70;
Sensor.SensorDataSet sds = parser.parseBuffer(buf);
assertTrue(sds.hasHeartRate());
assertTrue(sds.getHeartRate().getState() == Sensor.SensorState.SENDING);
assertEquals(70, sds.getHeartRate().getValue());
}
public void testFindNextAlignment_offset() {
// The first 4 bytes are garbage
buf = new byte[originalBuf.length + 4];
buf[0] = 4;
buf[1] = 2;
buf[2] = 4;
buf[3] = 2;
// Then the valid message.
System.arraycopy(originalBuf, 0, buf, 4, originalBuf.length);
assertEquals(4, parser.findNextAlignment(buf));
}
public void testFindNextAlignment_invalid() {
buf[0] = 0;
assertEquals(-1, parser.findNextAlignment(buf));
}
}