diff --git a/MyTracks/AndroidManifest.xml b/MyTracks/AndroidManifest.xml index 0f6250419..56ad0f2e1 100755 --- a/MyTracks/AndroidManifest.xml +++ b/MyTracks/AndroidManifest.xml @@ -112,12 +112,19 @@ limitations under the License. + + + + + @@ -129,7 +136,12 @@ limitations under the License. + + + + + + + - - - + + + + + - + + + + @@ -173,7 +193,6 @@ limitations under the License. - @@ -196,6 +215,10 @@ limitations under the License. + + + - - - - - - + + - + + + + + + + + + + + - + + + - + diff --git a/MyTracks/res/layout/about.xml b/MyTracks/res/layout/about.xml index d7f822937..37cd65890 100644 --- a/MyTracks/res/layout/about.xml +++ b/MyTracks/res/layout/about.xml @@ -87,7 +87,7 @@ limitations under the License. android:text="@string/about_support" style="@style/AboutLabel"/> diff --git a/MyTracks/res/layout/choose_map.xml b/MyTracks/res/layout/choose_map.xml new file mode 100644 index 000000000..13f453177 --- /dev/null +++ b/MyTracks/res/layout/choose_map.xml @@ -0,0 +1,45 @@ + + + + + + + diff --git a/MyTracks/res/layout/choose_map_item.xml b/MyTracks/res/layout/choose_map_item.xml new file mode 100644 index 000000000..eb27429a1 --- /dev/null +++ b/MyTracks/res/layout/choose_map_item.xml @@ -0,0 +1,47 @@ + + + + + + + diff --git a/MyTracks/res/layout/list.xml b/MyTracks/res/layout/list.xml deleted file mode 100644 index 9d2dd9812..000000000 --- a/MyTracks/res/layout/list.xml +++ /dev/null @@ -1,103 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/MyTracks/res/layout/listitem.xml b/MyTracks/res/layout/listitem.xml deleted file mode 100644 index 29fb1d098..000000000 --- a/MyTracks/res/layout/listitem.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - diff --git a/MyTracks/res/layout/send_to_google_result.xml b/MyTracks/res/layout/send_to_google_result.xml deleted file mode 100644 index bba36ca80..000000000 --- a/MyTracks/res/layout/send_to_google_result.xml +++ /dev/null @@ -1,65 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/MyTracks/res/layout/send_to_google_result_list_item.xml b/MyTracks/res/layout/send_to_google_result_list_item.xml deleted file mode 100644 index 045a60584..000000000 --- a/MyTracks/res/layout/send_to_google_result_list_item.xml +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - - - - - diff --git a/MyTracks/res/layout/upload_result.xml b/MyTracks/res/layout/upload_result.xml new file mode 100644 index 000000000..6148886d9 --- /dev/null +++ b/MyTracks/res/layout/upload_result.xml @@ -0,0 +1,143 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MyTracks/res/menu/main.xml b/MyTracks/res/menu/main.xml index 7979d052b..21c80178c 100644 --- a/MyTracks/res/menu/main.xml +++ b/MyTracks/res/menu/main.xml @@ -1,4 +1,19 @@ + + + - + - + - + + + + + \ No newline at end of file diff --git a/MyTracks/res/values/do_not_translate.xml b/MyTracks/res/values/do_not_translate.xml index bf6d60df8..745e71d63 100644 --- a/MyTracks/res/values/do_not_translate.xml +++ b/MyTracks/res/values/do_not_translate.xml @@ -37,6 +37,9 @@ limitations under the License. UA-7222692-2 m.google.com/privacy My Tracks Recording Service + + groups.google.com/a/googleproductforums.com/forum/#!categories/maps/mytracks + mytracks.appspot.com com.google.android.apps.mytracks.MYTRACKS_NOTIFICATIONS diff --git a/MyTracks/res/values/strings.xml b/MyTracks/res/values/strings.xml index 88ff0558e..1da6821c9 100644 --- a/MyTracks/res/values/strings.xml +++ b/MyTracks/res/values/strings.xml @@ -693,6 +693,43 @@ limitations under the License. My Tracks: Imported %1$d of %2$s from %3$s. + + + Your maps could not be loaded at this time. + + + Loading your Google Maps. + + + You don\'t have any maps. + + + Pick a map: + + + Public + + + Your Google Maps + + + Unlisted + + - (End) + translation_description="When inserting a marker to indicate the end of a track, the marker's + name. E.g., 'track_name (End)'. + + %1$s track name. E.g., 'track_name'."> + %1$s (End) - (Start) + translation_description="When inserting a marker to indicate the start of a track, the marker's + name. E.g., 'track_name (Start)'. + + %1$s track name. E.g., 'track_name'."> + %1$s (Start) - Your maps could not be loaded at this time. - - - Loading maps... - - - You don\'t have any maps. - - - Pick a map: - - - Public - - - Your Google Maps - - - Unlisted - - Tracks + + Search + + + + Search My Tracks tracks and waypoints + + + My Tracks tracks and waypoints + + + Speak the name of a track or waypoint. + + No account found - - Authenticating to %1$s... - - - Creating a new %1$s item... - - - Preparing track... - - - Reading track... - - - Sending a track to %1$s... + Sending to %1$s. Please wait... - (Part %1$d) + %1$s: track name. E.g., 'track_name' + %2$d: part number. E.g., '1'."> + %1$s (Part %2$d) @@ -1735,13 +1738,6 @@ limitations under the License. Screenshot: settings_recording_top.png"> Voice - - Voice announcement is not available on your Android phone. - + \ No newline at end of file diff --git a/MyTracks/src/com/google/android/apps/mytracks/AccountChooser.java b/MyTracks/src/com/google/android/apps/mytracks/AccountChooser.java deleted file mode 100644 index f7e886106..000000000 --- a/MyTracks/src/com/google/android/apps/mytracks/AccountChooser.java +++ /dev/null @@ -1,142 +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.maps.mytracks.R; - -import android.accounts.Account; -import android.accounts.AccountManager; -import android.app.Activity; -import android.app.AlertDialog; -import android.content.DialogInterface; -import android.util.Log; - -/** - * Choose which account to upload track information to. - * - * @author Sandor Dornbush - * - * @deprecated To be removed. - */ -public class AccountChooser { - - /** - * The last selected account. - */ - private int selectedAccountIndex = -1; - private Account selectedAccount = null; - - /** - * An interface for receiving updates once the user has selected the account. - */ - public interface AccountHandler { - /** - * Handle the account being selected. - * @param account The selected account or null if none could be found - */ - public void onAccountSelected(Account account); - } - - /** - * Chooses the best account to upload to. - * If no account is found the user will be alerted. - * If only one account is found that will be used. - * If multiple accounts are found the user will be allowed to choose. - * - * @param activity The parent activity - * @param handler The handler to be notified when an account has been selected - */ - public void chooseAccount(final Activity activity, - final AccountHandler handler) { - final Account[] accounts = AccountManager.get(activity) - .getAccountsByType(Constants.ACCOUNT_TYPE); - if (accounts.length < 1) { - alertNoAccounts(activity, handler); - return; - } - if (accounts.length == 1) { - handler.onAccountSelected(accounts[0]); - return; - } - - // TODO This should be read out of a preference. - if (selectedAccount != null) { - handler.onAccountSelected(selectedAccount); - return; - } - - // Let the user choose. - Log.e(Constants.TAG, "Multiple matching accounts found."); - final AlertDialog.Builder builder = new AlertDialog.Builder(activity); - builder.setTitle(R.string.send_google_choose_account_title); - builder.setCancelable(false); - builder.setPositiveButton(R.string.generic_ok, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - dialog.dismiss(); - - if (selectedAccountIndex >= 0) { - selectedAccount = accounts[selectedAccountIndex]; - } - - handler.onAccountSelected(selectedAccount); - } - }); - builder.setNegativeButton(R.string.generic_cancel, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - dialog.dismiss(); - handler.onAccountSelected(null); - } - }); - String[] choices = new String[accounts.length]; - for (int i = 0; i < accounts.length; i++) { - choices[i] = accounts[i].name; - } - builder.setSingleChoiceItems(choices, selectedAccountIndex, - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - selectedAccountIndex = which; - } - }); - builder.show(); - } - - public void setChosenAccount(String accountName, String accountType) { - selectedAccount = new Account(accountName, accountType); - } - - /** - * Puts up a dialog alerting the user that no suitable account was found. - */ - private void alertNoAccounts(final Activity activity, - final AccountHandler handler) { - Log.e(Constants.TAG, "No matching accounts found."); - final AlertDialog.Builder builder = new AlertDialog.Builder(activity); - builder.setTitle(R.string.send_google_no_account_title); - builder.setMessage(R.string.send_google_no_account_message); - builder.setCancelable(true); - builder.setPositiveButton(R.string.generic_ok, - new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - dialog.dismiss(); - handler.onAccountSelected(null); - } - }); - builder.show(); - } -} diff --git a/MyTracks/src/com/google/android/apps/mytracks/ChartActivity.java b/MyTracks/src/com/google/android/apps/mytracks/ChartActivity.java index fbc24fcdf..2e8c0e86d 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/ChartActivity.java +++ b/MyTracks/src/com/google/android/apps/mytracks/ChartActivity.java @@ -26,10 +26,8 @@ import com.google.android.apps.mytracks.content.TrackDataHub; import com.google.android.apps.mytracks.content.TrackDataHub.ListenerDataType; import com.google.android.apps.mytracks.content.TrackDataListener; import com.google.android.apps.mytracks.content.Waypoint; -import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerFactory; import com.google.android.apps.mytracks.stats.DoubleBuffer; import com.google.android.apps.mytracks.stats.TripStatisticsBuilder; -import com.google.android.apps.mytracks.util.ApiFeatures; import com.google.android.apps.mytracks.util.LocationUtils; import com.google.android.apps.mytracks.util.UnitConversions; import com.google.android.maps.mytracks.R; @@ -40,6 +38,7 @@ import android.content.DialogInterface; import android.content.DialogInterface.OnClickListener; import android.location.Location; import android.os.Bundle; +import android.speech.tts.TextToSpeech; import android.util.Log; import android.view.Menu; import android.view.MenuItem; @@ -116,9 +115,7 @@ public class ChartActivity extends Activity implements TrackDataListener { super.onCreate(savedInstanceState); // The volume we want to control is the Text-To-Speech volume - int volumeStream = - new StatusAnnouncerFactory(ApiFeatures.getInstance()).getVolumeStream(); - setVolumeControlStream(volumeStream); + setVolumeControlStream(TextToSpeech.Engine.DEFAULT_STREAM); requestWindowFeature(Window.FEATURE_NO_TITLE); setContentView(R.layout.mytracks_charts); diff --git a/MyTracks/src/com/google/android/apps/mytracks/CheckUnits.java b/MyTracks/src/com/google/android/apps/mytracks/CheckUnits.java index 38b52f3e2..e56ca4327 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/CheckUnits.java +++ b/MyTracks/src/com/google/android/apps/mytracks/CheckUnits.java @@ -16,7 +16,7 @@ package com.google.android.apps.mytracks; -import com.google.android.apps.mytracks.util.ApiFeatures; +import com.google.android.apps.mytracks.util.ApiAdapterFactory; import com.google.android.maps.mytracks.R; import android.app.AlertDialog; @@ -63,8 +63,7 @@ class CheckUnits { Constants.SETTINGS_NAME, Context.MODE_PRIVATE); SharedPreferences.Editor editor = useMetricPreferences.edit(); String key = context.getString(R.string.metric_units_key); - ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges( - editor.putBoolean(key, metric)); + ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor.putBoolean(key, metric)); } }); builder.setOnCancelListener(new DialogInterface.OnCancelListener() { @@ -76,7 +75,7 @@ class CheckUnits { } private static void recordCheckPerformed(SharedPreferences preferences) { - ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges( + ApiAdapterFactory.getApiAdapter().applyPreferenceChanges( preferences.edit().putBoolean(CHECK_UNITS_PREFERENCE_KEY, true)); } diff --git a/MyTracks/src/com/google/android/apps/mytracks/Constants.java b/MyTracks/src/com/google/android/apps/mytracks/Constants.java index a6d2a3df8..3cf7fae51 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/Constants.java +++ b/MyTracks/src/com/google/android/apps/mytracks/Constants.java @@ -35,29 +35,18 @@ public abstract class Constants { /* * onActivityResult request codes: - * - * TODO: These should be moved to the - * activities that call startActivityForResult. Thus do not need to keep a - * global order across all activities. */ - public static final int GET_LOGIN = 0; - public static final int GET_MAP = 1; - public static final int SHOW_TRACK = 2; - public static final int AUTHENTICATE_TO_MAPS = 3; - public static final int AUTHENTICATE_TO_FUSION_TABLES = 4; - public static final int AUTHENTICATE_TO_DOCLIST = 5; - public static final int AUTHENTICATE_TO_TRIX = 6; - public static final int SHARE_GPX_FILE = 7; - public static final int SHARE_KML_FILE = 8; - public static final int SHARE_CSV_FILE = 9; - public static final int SHARE_TCX_FILE = 10; - public static final int SAVE_GPX_FILE = 11; - public static final int SAVE_KML_FILE = 12; - public static final int SAVE_CSV_FILE = 13; - public static final int SAVE_TCX_FILE = 14; - public static final int SHOW_WAYPOINT = 15; - public static final int WELCOME = 16; - public static final int CHOOSE_ACCOUNT = 17; + public static final int SHOW_TRACK = 0; + public static final int SHARE_GPX_FILE = 1; + public static final int SHARE_KML_FILE = 2; + public static final int SHARE_CSV_FILE = 3; + public static final int SHARE_TCX_FILE = 4; + public static final int SAVE_GPX_FILE = 5; + public static final int SAVE_KML_FILE = 6; + public static final int SAVE_CSV_FILE = 7; + public static final int SAVE_TCX_FILE = 8; + public static final int SHOW_WAYPOINT = 9; + public static final int WELCOME = 10; /* * Menu ids: diff --git a/MyTracks/src/com/google/android/apps/mytracks/DeleteAllTracks.java b/MyTracks/src/com/google/android/apps/mytracks/DeleteAllTracks.java index a4a96cfa5..776f4bbd2 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/DeleteAllTracks.java +++ b/MyTracks/src/com/google/android/apps/mytracks/DeleteAllTracks.java @@ -16,7 +16,7 @@ package com.google.android.apps.mytracks; import com.google.android.apps.mytracks.content.MyTracksProviderUtils; -import com.google.android.apps.mytracks.util.ApiFeatures; +import com.google.android.apps.mytracks.util.ApiAdapterFactory; import com.google.android.maps.mytracks.R; import android.app.AlertDialog; @@ -66,7 +66,7 @@ public class DeleteAllTracks extends Handler { Editor editor = prefs.edit(); // TODO: Go through data manager editor.putLong(context.getString(R.string.selected_track_key), -1); - ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges(editor); + ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor); if (done != null) { Handler h = new Handler(); h.post(done); diff --git a/MyTracks/src/com/google/android/apps/mytracks/DeleteTrack.java b/MyTracks/src/com/google/android/apps/mytracks/DeleteTrack.java index a1d26a6c3..e34d78b49 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/DeleteTrack.java +++ b/MyTracks/src/com/google/android/apps/mytracks/DeleteTrack.java @@ -19,7 +19,7 @@ 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.ApiFeatures; +import com.google.android.apps.mytracks.util.ApiAdapterFactory; import com.google.android.apps.mytracks.util.UriUtils; import com.google.android.maps.mytracks.R; @@ -110,7 +110,7 @@ public class DeleteTrack extends Activity Constants.SETTINGS_NAME, Context.MODE_PRIVATE); if (preferences.getLong(selectedKey, -1) == deleteTrackId) { Editor editor = preferences.edit().putLong(selectedKey, -1); - ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges(editor); + ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor); } } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/DialogManager.java b/MyTracks/src/com/google/android/apps/mytracks/DialogManager.java deleted file mode 100644 index 98c824052..000000000 --- a/MyTracks/src/com/google/android/apps/mytracks/DialogManager.java +++ /dev/null @@ -1,77 +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 static com.google.android.apps.mytracks.Constants.TAG; - -import android.app.Activity; -import android.app.Dialog; -import android.util.Log; -import android.view.WindowManager.BadTokenException; - -/** - * A class to handle all dialog related events for My Tracks. - * - * @author Sandor Dornbush - */ -public class DialogManager { - - /** - * The equivalent of {@link Dialog#show()}, but for a specific dialog - * instance. - */ - 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() { - public void run() { - try { - dialog.show(); - } catch (BadTokenException e) { - Log.w(TAG, "Could not display dialog", e); - } catch (IllegalStateException e) { - Log.w(TAG, "Could not display dialog", e); - } - } - }); - } - - /** - * The equivalent of {@link Dialog#dismiss()}, but for a specific dialog - * instance. - */ - 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() { - public void run() { - try { - dialog.dismiss(); - } catch (IllegalArgumentException e) { - // This will be thrown if this dialog was not shown before. - } - } - }); - } - - private DialogManager() {} -} diff --git a/MyTracks/src/com/google/android/apps/mytracks/ExportAllTracks.java b/MyTracks/src/com/google/android/apps/mytracks/ExportAllTracks.java index b90d66d41..ef6169658 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/ExportAllTracks.java +++ b/MyTracks/src/com/google/android/apps/mytracks/ExportAllTracks.java @@ -81,12 +81,12 @@ public class ExportAllTracks { String exportFileFormat = activity.getString(R.string.track_list_export_file); String fileTypes[] = activity.getResources().getStringArray(R.array.file_types); - + String[] choices = new String[fileTypes.length]; for (int i = 0; i < fileTypes.length; i++) { choices[i] = String.format(exportFileFormat, fileTypes[i]); } - + AlertDialog.Builder builder = new AlertDialog.Builder(activity); builder.setTitle(R.string.track_list_export_all); builder.setSingleChoiceItems(choices, 0, itemClick); @@ -160,7 +160,7 @@ public class ExportAllTracks { try { MyTracksProviderUtils providerUtils = MyTracksProviderUtils.Factory.get(activity); - cursor = providerUtils.getTracksCursor(""); + cursor = providerUtils.getTracksCursor(null, null, TracksColumns._ID); if (cursor == null) { return; } diff --git a/MyTracks/src/com/google/android/apps/mytracks/MapActivity.java b/MyTracks/src/com/google/android/apps/mytracks/MapActivity.java index 21524287a..104005590 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/MapActivity.java +++ b/MyTracks/src/com/google/android/apps/mytracks/MapActivity.java @@ -25,11 +25,9 @@ import com.google.android.apps.mytracks.content.TrackDataListener; import com.google.android.apps.mytracks.content.TracksColumns; 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.SendType; +import com.google.android.apps.mytracks.io.sendtogoogle.SendRequest; import com.google.android.apps.mytracks.io.sendtogoogle.UploadServiceChooserActivity; -import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerFactory; import com.google.android.apps.mytracks.stats.TripStatistics; -import com.google.android.apps.mytracks.util.ApiFeatures; import com.google.android.apps.mytracks.util.GeoRect; import com.google.android.apps.mytracks.util.LocationUtils; import com.google.android.maps.GeoPoint; @@ -42,8 +40,8 @@ import android.content.Intent; import android.location.Location; import android.net.Uri; import android.os.Bundle; -import android.os.Parcelable; import android.provider.Settings; +import android.speech.tts.TextToSpeech; import android.util.Log; import android.view.ContextMenu; import android.view.ContextMenu.ContextMenuInfo; @@ -86,6 +84,25 @@ public class MapActivity extends com.google.android.maps.MapActivity */ private boolean keepMyLocationVisible; + /** + * The ID of a track on which we want to show a waypoint. + * The waypoint will be shown as soon as the track is loaded. + */ + private long showWaypointTrackId; + + /** + * The ID of a waypoint which we want to show. + * The waypoint will be shown as soon as its track is loaded. + */ + private long showWaypointId; + + /** + * The track that's currently selected. + * This differs from {@link TrackDataHub#getSelectedTrackId} in that this one is only set after + * actual track data has been received. + */ + private long selectedTrackId; + /** * The current pointer location. * This is kept to quickly center on it when the user requests. @@ -134,9 +151,7 @@ public class MapActivity extends com.google.android.maps.MapActivity super.onCreate(bundle); // The volume we want to control is the Text-To-Speech volume - int volumeStream = - new StatusAnnouncerFactory(ApiFeatures.getInstance()).getVolumeStream(); - setVolumeControlStream(volumeStream); + setVolumeControlStream(TextToSpeech.Engine.DEFAULT_STREAM); // We don't need a window title bar: requestWindowFeature(Window.FEATURE_NO_TITLE); @@ -332,6 +347,47 @@ public class MapActivity extends com.google.android.maps.MapActivity } } + /** + * Zooms and pans the map so that the given waypoint is visible, when the given track is loaded. + * If the track is already loaded, it does that immediately. + * + * @param trackId the ID of the track on which to show the waypoint + * @param waypointId the ID of the waypoint to show + */ + public void showWaypoint(long trackId, long waypointId) { + synchronized (this) { + if (trackId == selectedTrackId) { + showWaypoint(waypointId); + return; + } + + showWaypointTrackId = trackId; + showWaypointId = waypointId; + } + } + + /** + * Does the proper zooming/panning for a just-loaded track. + * This may be either zooming to a waypoint that has been previously selected, or + * zooming to the whole track. + * + * @param track the loaded track + */ + private void zoomLoadedTrack(Track track) { + synchronized (this) { + if (track.getId() == showWaypointTrackId) { + // There's a waypoint to show in this track. + showWaypoint(showWaypointId); + + showWaypointId = 0L; + showWaypointTrackId = 0L; + } else { + // Zoom out to show the whole track. + zoomMapToBoundaries(track); + } + } + } + @Override public void onSelectedTrackChanged(final Track track, final boolean isRecording) { runOnUiThread(new Runnable() { @@ -345,7 +401,12 @@ public class MapActivity extends com.google.android.maps.MapActivity if (trackSelected) { busyPane.setVisibility(View.VISIBLE); - zoomMapToBoundaries(track); + synchronized (this) { + // Need to get the track ID only at this point, to prevent a race condition + // among showWaypoint, zoomLoadedTrack and dataHub.loadTrack. + selectedTrackId = track.getId(); + zoomLoadedTrack(track); + } mapOverlay.setShowEndMarker(!isRecording); busyPane.setVisibility(View.GONE); @@ -404,21 +465,18 @@ public class MapActivity extends com.google.android.maps.MapActivity long trackId = dataHub.getSelectedTrackId(); switch (item.getItemId()) { case Constants.MENU_SEND_TO_GOOGLE: - intent = new Intent(this, UploadServiceChooserActivity.class); - intent.putExtra(UploadServiceChooserActivity.TRACK_ID, trackId); + intent = new Intent(this, UploadServiceChooserActivity.class) + .putExtra(SendRequest.SEND_REQUEST_KEY, new SendRequest(trackId, true, true, true)); startActivity(intent); return true; case Constants.MENU_SHARE_MAP: - intent = new Intent(this, UploadServiceChooserActivity.class); - intent.putExtra(UploadServiceChooserActivity.TRACK_ID, trackId); - intent.putExtra(UploadServiceChooserActivity.SEND_TYPE, (Parcelable) SendType.MAPS); + intent = new Intent(this, UploadServiceChooserActivity.class) + .putExtra(SendRequest.SEND_REQUEST_KEY, new SendRequest(trackId, true, false, false)); startActivity(intent); return true; case Constants.MENU_SHARE_FUSION_TABLE: - intent = new Intent(this, UploadServiceChooserActivity.class); - intent.putExtra(UploadServiceChooserActivity.TRACK_ID, trackId); - intent.putExtra( - UploadServiceChooserActivity.SEND_TYPE, (Parcelable) SendType.FUSION_TABLES); + intent = new Intent(this, UploadServiceChooserActivity.class) + .putExtra(SendRequest.SEND_REQUEST_KEY, new SendRequest(trackId, false, true, false)); startActivity(intent); return true; case Constants.MENU_SAVE_GPX_FILE: diff --git a/MyTracks/src/com/google/android/apps/mytracks/MapsList.java b/MyTracks/src/com/google/android/apps/mytracks/MapsList.java deleted file mode 100644 index 8e3f873ab..000000000 --- a/MyTracks/src/com/google/android/apps/mytracks/MapsList.java +++ /dev/null @@ -1,175 +0,0 @@ -/* - * 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; - -import static com.google.android.apps.mytracks.Constants.TAG; - -import com.google.android.apps.mytracks.io.AuthManager; -import com.google.android.apps.mytracks.io.AuthManager.AuthCallback; -import com.google.android.apps.mytracks.io.AuthManagerFactory; -import com.google.android.apps.mytracks.io.maps.MapsConstants; -import com.google.android.apps.mytracks.io.maps.MapsFacade; -import com.google.android.maps.mytracks.R; - -import android.app.Activity; -import android.content.Intent; -import android.os.Bundle; -import android.util.Log; -import android.view.View; -import android.widget.AdapterView; -import android.widget.AdapterView.OnItemClickListener; -import android.widget.ListView; -import android.widget.TextView; - -/** - * Activity which displays the list of current Google Maps for the user. - * Returns RESULT_OK if the user picked a map, and returns "mapid" as an extra. - * - * @author Rodrigo Damazio - */ -public class MapsList extends Activity implements MapsFacade.MapsListCallback { - 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 AuthManager auth; - private MapsListAdapter listAdapter; - - private final OnItemClickListener clickListener = - new OnItemClickListener() { - @Override - public void onItemClick(AdapterView parent, View v, int position, - long id) { - Intent result = new Intent(); - result.putExtra("mapid", (String) listAdapter.getItem(position)); - setResult(RESULT_OK, result); - finish(); - } - }; - - /** Called when the activity is first created. */ - @Override - public void onCreate(Bundle icicle) { - super.onCreate(icicle); - - auth = AuthManagerFactory.getAuthManager(this, GET_LOGIN, null, true, - MapsConstants.SERVICE_NAME); - - setContentView(R.layout.list); - - listAdapter = new MapsListAdapter(this); - - ListView list = (ListView) findViewById(R.id.maplist); - list.setOnItemClickListener(clickListener); - list.setAdapter(listAdapter); - - startLogin(); - } - - private void startLogin() { - // Starts in the UI thread. - // TODO fix this for non-froyo devices. - if (AuthManagerFactory.useModernAuthManager()) { - Intent intent = getIntent(); - String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME); - String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE); - if (accountName == null || accountType == null) { - Log.e(TAG, "Didn't receive account name or type"); - setResult(RESULT_CANCELED); - finish(); - return; - } - - doLogin(auth.getAccountObject(accountName, accountType)); - } else { - doLogin(null); - } - } - - private void doLogin(final Object account) { - // Starts in the UI thread. - auth.doLogin(new AuthCallback() { - @Override - public void onAuthResult(boolean success) { - if (!success) { - setResult(RESULT_CANCELED); - finish(); - return; - } - - // Runs in UI thread. - mapsClient = new MapsFacade(MapsList.this, auth); - - startLookup(); - } - }, account); - } - - private void startLookup() { - // Starts in the UI thread. - new Thread() { - @Override - public void run() { - // Communication with Maps happens in its own thread. - // This will call onReceivedMapListing below. - final boolean success = mapsClient.getMapsList(MapsList.this); - - runOnUiThread(new Runnable() { - @Override - public void run() { - // Updating the UI when done happens in the UI thread. - onLookupDone(success); - } - }); - } - }.start(); - } - - @Override - public void onReceivedMapListing(final String mapId, final String title, - final String description, final boolean isPublic) { - // Starts in the communication thread. - runOnUiThread(new Runnable() { - @Override - public void run() { - // Updating the list with new contents happens in the UI thread. - listAdapter.addMapListing(mapId, title, description, isPublic); - } - }); - } - - private void onLookupDone(boolean success) { - // Starts in the UI thread. - findViewById(R.id.loading).setVisibility(View.GONE); - if (!success) { - findViewById(R.id.failed).setVisibility(View.VISIBLE); - } - TextView emptyView = (TextView) findViewById(R.id.mapslist_empty); - ListView list = (ListView) findViewById(R.id.maplist); - list.setEmptyView(emptyView); - } - - @Override - protected void onActivityResult(int requestCode, int resultCode, - Intent data) { - if (requestCode == GET_LOGIN) { - auth.authResult(resultCode, data); - } - super.onActivityResult(requestCode, resultCode, data); - } -} diff --git a/MyTracks/src/com/google/android/apps/mytracks/MapsListAdapter.java b/MyTracks/src/com/google/android/apps/mytracks/MapsListAdapter.java deleted file mode 100644 index 1f6d4bbce..000000000 --- a/MyTracks/src/com/google/android/apps/mytracks/MapsListAdapter.java +++ /dev/null @@ -1,135 +0,0 @@ -package com.google.android.apps.mytracks; - -import com.google.android.maps.mytracks.R; - -import android.app.Activity; -import android.database.DataSetObserver; -import android.graphics.Color; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ListAdapter; -import android.widget.TextView; - -import java.util.HashSet; -import java.util.Iterator; -import java.util.Set; -import java.util.Vector; - -public class MapsListAdapter implements ListAdapter { - private Vector mapsList; - private Vector publicList; - private Set observerSet; - private final Activity activity; - - public MapsListAdapter(Activity activity) { - this.activity = activity; - - mapsList = new Vector(); - publicList = new Vector(); - observerSet = new HashSet(); - } - - public void addMapListing(String mapId, String title, - String description, boolean isPublic) { - synchronized (mapsList) { - // Search through the maps list to see if it has the mapid and - // remove it if so, so that we can replace it with updated info - for (int i = 0; i < mapsList.size(); ++i) { - if (mapId.equals(mapsList.get(i)[0])) { - mapsList.remove(i); - publicList.remove(i); - --i; - } - } - mapsList.add(new String[] { mapId, title, description }); - publicList.add(isPublic); - } - - Iterator iter = observerSet.iterator(); - while (iter.hasNext()) { - iter.next().onChanged(); - } - } - - public String[] getMapListingArray(int position) { - return mapsList.get(position); - } - - @Override - public boolean areAllItemsEnabled() { - return true; - } - - @Override - public boolean isEnabled(int position) { - return true; - } - - @Override - public int getCount() { - return mapsList.size(); - } - - @Override - public Object getItem(int position) { - return mapsList.get(position)[0]; - } - - @Override - public long getItemId(int position) { - return position; - } - - @Override - public int getItemViewType(int position) { - return 0; - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - if (convertView == null) { - convertView = - activity.getLayoutInflater().inflate(R.layout.listitem, parent, false); - } - - String[] map = mapsList.get(position); - ((TextView) convertView.findViewById(R.id.maplistitem)).setText(map[1]); - ((TextView) convertView.findViewById(R.id.maplistdesc)).setText(map[2]); - TextView publicUnlisted = - (TextView) convertView.findViewById(R.id.maplistpublic); - if (publicList.get(position)) { - publicUnlisted.setTextColor(Color.RED); - publicUnlisted.setText(R.string.maps_list_public_label); - } else { - publicUnlisted.setTextColor(Color.GREEN); - publicUnlisted.setText(R.string.maps_list_unlisted_label); - } - - return convertView; - } - - @Override - public int getViewTypeCount() { - return 1; - } - - @Override - public boolean hasStableIds() { - return false; - } - - @Override - public boolean isEmpty() { - return mapsList.isEmpty(); - } - - @Override - public void registerDataSetObserver(DataSetObserver observer) { - observerSet.add(observer); - } - - @Override - public void unregisterDataSetObserver(DataSetObserver observer) { - observerSet.remove(observer); - } -} diff --git a/MyTracks/src/com/google/android/apps/mytracks/MenuManager.java b/MyTracks/src/com/google/android/apps/mytracks/MenuManager.java index 468680efd..52dcf2c25 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/MenuManager.java +++ b/MyTracks/src/com/google/android/apps/mytracks/MenuManager.java @@ -37,6 +37,8 @@ class MenuManager { public boolean onCreateOptionsMenu(Menu menu) { activity.getMenuInflater().inflate(R.menu.main, menu); + + // TODO: Replace search button with search widget if API level >= 11 return true; } @@ -85,6 +87,10 @@ class MenuManager { case R.id.menu_help: { return startActivity(WelcomeActivity.class); } + case R.id.menu_search: { + // TODO: Pass the current track ID and current location to do some fancier ranking. + activity.onSearchRequested(); + } } return false; } diff --git a/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java b/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java index e22dc054a..8836fb69d 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java +++ b/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java @@ -21,13 +21,14 @@ import com.google.android.apps.analytics.GoogleAnalyticsTracker; import com.google.android.apps.mytracks.content.MyTracksProviderUtils; import com.google.android.apps.mytracks.content.TrackDataHub; import com.google.android.apps.mytracks.content.TracksColumns; +import com.google.android.apps.mytracks.content.Waypoint; import com.google.android.apps.mytracks.content.WaypointCreationRequest; +import com.google.android.apps.mytracks.content.WaypointsColumns; import com.google.android.apps.mytracks.io.file.TempFileCleaner; import com.google.android.apps.mytracks.services.ITrackRecordingService; import com.google.android.apps.mytracks.services.ServiceUtils; import com.google.android.apps.mytracks.services.TrackRecordingServiceConnection; -import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerFactory; -import com.google.android.apps.mytracks.util.ApiFeatures; +import com.google.android.apps.mytracks.util.ApiAdapterFactory; import com.google.android.apps.mytracks.util.EulaUtil; import com.google.android.apps.mytracks.util.SystemUtils; import com.google.android.apps.mytracks.util.UriUtils; @@ -46,6 +47,7 @@ import android.content.res.Resources; import android.net.Uri; import android.os.Bundle; import android.os.RemoteException; +import android.speech.tts.TextToSpeech; import android.util.Log; import android.view.Menu; import android.view.MenuItem; @@ -67,7 +69,7 @@ import android.widget.Toast; @SuppressWarnings("deprecation") public class MyTracks extends TabActivity implements OnTouchListener { private static final int DIALOG_EULA_ID = 0; - + private TrackDataHub dataHub; /** @@ -139,9 +141,8 @@ public class MyTracks extends TabActivity implements OnTouchListener { protected void onCreate(Bundle savedInstanceState) { Log.d(TAG, "MyTracks.onCreate"); super.onCreate(savedInstanceState); - ApiFeatures apiFeatures = ApiFeatures.getInstance(); if (!SystemUtils.isRelease(this)) { - apiFeatures.getApiAdapter().enableStrictMode(); + ApiAdapterFactory.getApiAdapter().enableStrictMode(); } tracker = GoogleAnalyticsTracker.getInstance(); @@ -157,14 +158,14 @@ public class MyTracks extends TabActivity implements OnTouchListener { menuManager = new MenuManager(this); serviceConnection = new TrackRecordingServiceConnection(this, serviceBindCallback); - // The volume we want to control is the Text-To-Speech volume - int volumeStream = - new StatusAnnouncerFactory(apiFeatures).getVolumeStream(); - setVolumeControlStream(volumeStream); + setVolumeControlStream(TextToSpeech.Engine.DEFAULT_STREAM); // We don't need a window title bar: requestWindowFeature(Window.FEATURE_NO_TITLE); + // If the user just starts typing (on a device with a keyboard), we start a search. + setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL); + final Resources res = getResources(); final TabHost tabHost = getTabHost(); tabHost.addTab(tabHost.newTabSpec("tab1") @@ -199,7 +200,7 @@ public class MyTracks extends TabActivity implements OnTouchListener { showDialog(DIALOG_EULA_ID); } } - + @Override protected void onStart() { Log.d(TAG, "MyTracks.onStart"); @@ -219,6 +220,19 @@ public class MyTracks extends TabActivity implements OnTouchListener { && UriUtils.matchesContentUri(data, TracksColumns.CONTENT_URI)) { long trackId = ContentUris.parseId(data); dataHub.loadTrack(trackId); + } else if (Intent.ACTION_VIEW.equals(action) + && WaypointsColumns.CONTENT_ITEMTYPE.equals(intent.getType()) + && UriUtils.matchesContentUri(data, WaypointsColumns.CONTENT_URI)) { + // TODO(rdamazio): Waypoint URIs should be base/trackid/waypointid + long waypointId = ContentUris.parseId(data); + Waypoint waypoint = providerUtils.getWaypoint(waypointId); + long trackId = waypoint.getTrackId(); + + // Request that the waypoint is shown (now or when the right track is loaded). + showWaypoint(trackId, waypointId); + + // Load the right track, if not loaded already. + dataHub.loadTrack(trackId); } } @@ -293,7 +307,7 @@ public class MyTracks extends TabActivity implements OnTouchListener { return null; } } - + @Override public boolean onCreateOptionsMenu(Menu menu) { super.onCreateOptionsMenu(menu); @@ -366,12 +380,7 @@ public class MyTracks extends TabActivity implements OnTouchListener { if (results != null) { final long waypointId = results.getLongExtra(WaypointDetails.WAYPOINT_ID_EXTRA, -1); if (waypointId >= 0) { - MapActivity map = - (MapActivity) getLocalActivityManager().getActivity("tab1"); - if (map != null) { - getTabHost().setCurrentTab(0); - map.showWaypoint(waypointId); - } + showWaypoint(trackId, waypointId); } } break; @@ -387,6 +396,17 @@ public class MyTracks extends TabActivity implements OnTouchListener { } } + private void showWaypoint(long trackId, long waypointId) { + MapActivity map = + (MapActivity) getLocalActivityManager().getActivity("tab1"); + if (map != null) { + getTabHost().setCurrentTab(0); + map.showWaypoint(trackId, waypointId); + } else { + Log.e(TAG, "Couldnt' get map tab"); + } + } + @Override public boolean onTouch(View v, MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_DOWN) { diff --git a/MyTracks/src/com/google/android/apps/mytracks/SearchActivity.java b/MyTracks/src/com/google/android/apps/mytracks/SearchActivity.java new file mode 100644 index 000000000..54eed6cd4 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/SearchActivity.java @@ -0,0 +1,217 @@ +/* + * 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; + +import static com.google.android.apps.mytracks.Constants.TAG; + +import com.google.android.apps.mytracks.content.SearchEngine; +import com.google.android.apps.mytracks.content.SearchEngine.ScoredResult; +import com.google.android.apps.mytracks.content.SearchEngine.SearchQuery; +import com.google.android.apps.mytracks.content.SearchEngineProvider; +import com.google.android.apps.mytracks.content.Track; +import com.google.android.apps.mytracks.content.TracksColumns; +import com.google.android.apps.mytracks.content.Waypoint; +import com.google.android.apps.mytracks.content.WaypointsColumns; +import com.google.android.maps.mytracks.R; + +import android.app.ListActivity; +import android.app.SearchManager; +import android.content.ContentUris; +import android.content.Intent; +import android.location.Location; +import android.location.LocationManager; +import android.net.Uri; +import android.os.Bundle; +import android.provider.SearchRecentSuggestions; +import android.util.Log; +import android.view.View; +import android.widget.ListView; +import android.widget.SimpleAdapter; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.SortedSet; + +/** + * Activity to search for tracks or waypoints. + * + * @author Rodrigo Damazio + */ +public class SearchActivity extends ListActivity { + + private static final String EXTRA_CURRENT_TRACK_ID = "trackId"; + + private static final boolean LOG_SCORES = true; + + private SearchEngine engine; + private LocationManager locationManager; + + private SearchRecentSuggestions suggestions; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + engine = new SearchEngine(this); + suggestions = SearchEngineProvider.newHelper(this); + locationManager = (LocationManager) getSystemService(LOCATION_SERVICE); + + handleIntent(getIntent()); + } + + @Override + protected void onNewIntent(Intent intent) { + setIntent(intent); + handleIntent(intent); + } + + private void handleIntent(Intent intent) { + if (!Intent.ACTION_SEARCH.equals(intent.getAction())) { + Log.e(TAG, "Got bad search intent: " + intent); + finish(); + return; + } + + String textQuery = intent.getStringExtra(SearchManager.QUERY); + Location currentLocation = locationManager.getLastKnownLocation("gps"); + long currentTrackId = intent.getLongExtra(EXTRA_CURRENT_TRACK_ID, -1); + long currentTimestamp = System.currentTimeMillis(); + + final SearchQuery query = + new SearchQuery(textQuery, currentLocation, currentTrackId, currentTimestamp); + + // Do the actual search in a separate thread. + new Thread() { + @Override + public void run() { + doSearch(query); + } + }.start(); + } + + private void doSearch(SearchQuery query) { + SortedSet scoredResults = engine.search(query); + + final List> displayResults = prepareResultsforDisplay(scoredResults); + + if (LOG_SCORES) { + Log.i(TAG, "Search scores: " + displayResults); + } + + // Then go back to the UI thread to display them. + runOnUiThread(new Runnable() { + @Override + public void run() { + showSearchResults(displayResults); + } + }); + + // Save the query as a suggestion for the future. + suggestions.saveRecentQuery(query.textQuery, null); + } + + private List> prepareResultsforDisplay( + Collection scoredResults) { + ArrayList> output = new ArrayList>(scoredResults.size()); + for (ScoredResult result : scoredResults) { + Map resultMap = new HashMap(); + if (result.track != null) { + prepareTrackForDisplay(result.track, resultMap); + } else { + prepareWaypointForDisplay(result.waypoint, resultMap); + } + + resultMap.put("score", result.score); + + output.add(resultMap); + } + + return output; + } + + private void prepareWaypointForDisplay(Waypoint waypoint, Map resultMap) { + // TODO: Yellow pushpin for statistics marker. + resultMap.put("icon", R.drawable.blue_pushpin); + resultMap.put("name", waypoint.getName()); + resultMap.put("description", waypoint.getDescription()); + resultMap.put("category", waypoint.getCategory()); + resultMap.put("time", String.format("%tc", waypoint.getLocation().getTime())); + resultMap.put("trackId", waypoint.getTrackId()); + resultMap.put("waypointId", waypoint.getId()); + } + + private void prepareTrackForDisplay(Track track, Map resultMap) { + resultMap.put("icon", R.drawable.track); + resultMap.put("name", track.getName()); + resultMap.put("description", track.getDescription()); + resultMap.put("category", track.getCategory()); + resultMap.put("time", String.format("%tc", track.getStatistics().getStartTime())); + resultMap.put("trackId", track.getId()); + } + + /** + * Shows the given search results. + * Must be run from the UI thread. + * + * @param data the results to show, properly ordered + */ + private void showSearchResults(List> data) { + SimpleAdapter adapter = new SimpleAdapter(this, data, + // TODO: Custom view for search results. + R.layout.mytracks_list_item, + new String[] { + "icon", + "name", + "description", + "category", + "time" + }, + new int[] { + R.id.trackdetails_item_icon, + R.id.trackdetails_item_name, + R.id.trackdetails_item_description, + R.id.trackdetails_item_category, + R.id.trackdetails_item_time + }); + + setListAdapter(adapter); + } + + @Override + protected void onListItemClick(ListView l, View v, int position, long id) { + @SuppressWarnings("unchecked") + Map clickedData = (Map) getListAdapter().getItem(position); + + startActivity(createViewDataIntent(clickedData)); + } + + private Intent createViewDataIntent(Map clickedData) { + Intent intent = new Intent(Intent.ACTION_VIEW); + if (clickedData.containsKey("waypointId")) { + long waypointId = (Long) clickedData.get("waypointId"); + Uri uri = ContentUris.withAppendedId(WaypointsColumns.CONTENT_URI, waypointId); + intent.setDataAndType(uri, WaypointsColumns.CONTENT_ITEMTYPE); + } else { + long trackId = (Long) clickedData.get("trackId"); + Uri uri = ContentUris.withAppendedId(TracksColumns.CONTENT_URI, trackId); + intent.setDataAndType(uri, TracksColumns.CONTENT_ITEMTYPE); + } + return intent; + } +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/SettingsActivity.java b/MyTracks/src/com/google/android/apps/mytracks/SettingsActivity.java index 45a872cb9..a61fd744d 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/SettingsActivity.java +++ b/MyTracks/src/com/google/android/apps/mytracks/SettingsActivity.java @@ -20,13 +20,13 @@ import static com.google.android.apps.mytracks.Constants.TAG; import com.google.android.apps.mytracks.io.backup.BackupActivityHelper; import com.google.android.apps.mytracks.io.backup.BackupPreferencesListener; import com.google.android.apps.mytracks.services.sensors.ant.AntUtils; -import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerFactory; -import com.google.android.apps.mytracks.util.ApiFeatures; +import com.google.android.apps.mytracks.util.ApiAdapterFactory; import com.google.android.apps.mytracks.util.BluetoothDeviceUtils; import com.google.android.apps.mytracks.util.UnitConversions; import com.google.android.maps.mytracks.R; import android.app.AlertDialog; +import android.bluetooth.BluetoothAdapter; import android.content.DialogInterface; import android.content.DialogInterface.OnClickListener; import android.content.Intent; @@ -44,6 +44,7 @@ import android.preference.PreferenceCategory; import android.preference.PreferenceManager; import android.preference.PreferenceScreen; import android.provider.Settings; +import android.speech.tts.TextToSpeech; import android.util.Log; import android.widget.Toast; @@ -102,10 +103,7 @@ public class SettingsActivity extends PreferenceActivity { super.onCreate(icicle); // The volume we want to control is the Text-To-Speech volume - ApiFeatures apiFeatures = ApiFeatures.getInstance(); - int volumeStream = - new StatusAnnouncerFactory(apiFeatures).getVolumeStream(); - setVolumeControlStream(volumeStream); + setVolumeControlStream(TextToSpeech.Engine.DEFAULT_STREAM); // Tell it where to read/write preferences PreferenceManager preferenceManager = getPreferenceManager(); @@ -113,23 +111,13 @@ public class SettingsActivity extends PreferenceActivity { preferenceManager.setSharedPreferencesMode(0); // Set up automatic preferences backup - backupListener = apiFeatures.getApiAdapter().getBackupPreferencesListener(this); + backupListener = ApiAdapterFactory.getApiAdapter().getBackupPreferencesListener(this); preferences = preferenceManager.getSharedPreferences(); preferences.registerOnSharedPreferenceChangeListener(backupListener); // Load the preferences to be displayed addPreferencesFromResource(R.xml.preferences); - // Disable voice announcement if not available - if (!apiFeatures.hasTextToSpeech()) { - IntegerListPreference announcementFrequency = - (IntegerListPreference) findPreference( - getString(R.string.announcement_frequency_key)); - announcementFrequency.setEnabled(false); - announcementFrequency.setValue(TASK_FREQUENCY_OFF); - announcementFrequency.setSummary(R.string.settings_recording_voice_not_available); - } - setRecordingIntervalOptions(); setAutoResumeTimeoutOptions(); @@ -557,20 +545,17 @@ public class SettingsActivity extends PreferenceActivity { * Configures preference actions related to bluetooth. */ private void configureBluetoothPreferences() { - if (BluetoothDeviceUtils.isBluetoothMethodSupported()) { - // Populate the list of bluetooth devices - populateBluetoothDeviceList(); - - // Make the pair devices preference go to the system preferences - findPreference(getString(R.string.bluetooth_pairing_key)) - .setOnPreferenceClickListener(new OnPreferenceClickListener() { - public boolean onPreferenceClick(Preference preference) { - Intent settingsIntent = new Intent(Settings.ACTION_BLUETOOTH_SETTINGS); - startActivity(settingsIntent); - return false; - } - }); - } + // Populate the list of bluetooth devices + populateBluetoothDeviceList(); + // Make the pair devices preference go to the system preferences + findPreference(getString(R.string.bluetooth_pairing_key)).setOnPreferenceClickListener( + new OnPreferenceClickListener() { + public boolean onPreferenceClick(Preference preference) { + Intent settingsIntent = new Intent(Settings.ACTION_BLUETOOTH_SETTINGS); + startActivity(settingsIntent); + return false; + } + }); } /** @@ -582,7 +567,10 @@ public class SettingsActivity extends PreferenceActivity { List entryValues = new ArrayList(); // The actual devices - BluetoothDeviceUtils.getInstance().populateDeviceLists(entries, entryValues); + BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); + if (bluetoothAdapter != null) { + BluetoothDeviceUtils.populateDeviceLists(bluetoothAdapter, entries, entryValues); + } CharSequence[] entriesArray = entries.toArray(new CharSequence[entries.size()]); CharSequence[] entryValuesArray = entryValues.toArray(new CharSequence[entryValues.size()]); @@ -685,7 +673,7 @@ public class SettingsActivity extends PreferenceActivity { SharedPreferences prefs = getPreferenceManager().getSharedPreferences(); Editor editor = prefs.edit(); editor.putString(getString(id), metricspeed); - ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges(editor); + ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor); } /** diff --git a/MyTracks/src/com/google/android/apps/mytracks/StatsActivity.java b/MyTracks/src/com/google/android/apps/mytracks/StatsActivity.java index 2c705fb29..7ecad6d02 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/StatsActivity.java +++ b/MyTracks/src/com/google/android/apps/mytracks/StatsActivity.java @@ -23,8 +23,6 @@ import com.google.android.apps.mytracks.content.TrackDataHub.ListenerDataType; import com.google.android.apps.mytracks.content.TrackDataListener; import com.google.android.apps.mytracks.content.Waypoint; import com.google.android.apps.mytracks.services.ServiceUtils; -import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerFactory; -import com.google.android.apps.mytracks.util.ApiFeatures; import com.google.android.maps.mytracks.R; import android.app.Activity; @@ -32,6 +30,7 @@ import android.content.Context; import android.content.SharedPreferences; import android.location.Location; import android.os.Bundle; +import android.speech.tts.TextToSpeech; import android.util.DisplayMetrics; import android.util.Log; import android.view.Window; @@ -105,9 +104,7 @@ public class StatsActivity extends Activity implements TrackDataListener { utils = new StatsUtilities(this); // The volume we want to control is the Text-To-Speech volume - int volumeStream = - new StatusAnnouncerFactory(ApiFeatures.getInstance()).getVolumeStream(); - setVolumeControlStream(volumeStream); + setVolumeControlStream(TextToSpeech.Engine.DEFAULT_STREAM); // We don't need a window title bar: requestWindowFeature(Window.FEATURE_NO_TITLE); diff --git a/MyTracks/src/com/google/android/apps/mytracks/TrackList.java b/MyTracks/src/com/google/android/apps/mytracks/TrackList.java index 94166448a..b0e08bb95 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/TrackList.java +++ b/MyTracks/src/com/google/android/apps/mytracks/TrackList.java @@ -19,7 +19,7 @@ import static com.google.android.apps.mytracks.Constants.TAG; 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.SendType; +import com.google.android.apps.mytracks.io.sendtogoogle.SendRequest; import com.google.android.apps.mytracks.io.sendtogoogle.UploadServiceChooserActivity; import com.google.android.apps.mytracks.services.ServiceUtils; import com.google.android.apps.mytracks.services.TrackRecordingServiceConnection; @@ -36,7 +36,6 @@ import android.database.Cursor; import android.net.Uri; import android.os.Bundle; import android.os.Handler; -import android.os.Parcelable; import android.util.Log; import android.view.ContextMenu; import android.view.ContextMenu.ContextMenuInfo; @@ -175,21 +174,18 @@ public class TrackList extends ListActivity case Constants.MENU_WRITE_TO_SD_CARD: return false; case Constants.MENU_SEND_TO_GOOGLE: - intent = new Intent(this, UploadServiceChooserActivity.class); - intent.putExtra(UploadServiceChooserActivity.TRACK_ID, trackId); + intent = new Intent(this, UploadServiceChooserActivity.class) + .putExtra(SendRequest.SEND_REQUEST_KEY, new SendRequest(trackId, true, true, true)); startActivity(intent); return true; case Constants.MENU_SHARE_MAP: - intent = new Intent(this, UploadServiceChooserActivity.class); - intent.putExtra(UploadServiceChooserActivity.TRACK_ID, trackId); - intent.putExtra(UploadServiceChooserActivity.SEND_TYPE, (Parcelable) SendType.MAPS); + intent = new Intent(this, UploadServiceChooserActivity.class) + .putExtra(SendRequest.SEND_REQUEST_KEY, new SendRequest(trackId, true, false, false)); startActivity(intent); return true; case Constants.MENU_SHARE_FUSION_TABLE: - intent = new Intent(this, UploadServiceChooserActivity.class); - intent.putExtra(UploadServiceChooserActivity.TRACK_ID, trackId); - intent.putExtra( - UploadServiceChooserActivity.SEND_TYPE, (Parcelable) SendType.FUSION_TABLES); + intent = new Intent(this, UploadServiceChooserActivity.class) + .putExtra(SendRequest.SEND_REQUEST_KEY, new SendRequest(trackId, false, true, false)); startActivity(intent); return true; case Constants.MENU_SAVE_GPX_FILE: @@ -286,6 +282,17 @@ public class TrackList extends ListActivity super.onDestroy(); } + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.search_only, menu); + return true; + } + + /* Callback from menu/search_only.xml */ + public void onSearch(@SuppressWarnings("unused") MenuItem i) { + onSearchRequested(); + } + private void updateButtonsEnabled() { View deleteAll = findViewById(R.id.tracklist_btn_delete_all); View exportAll = findViewById(R.id.tracklist_btn_export_all); diff --git a/MyTracks/src/com/google/android/apps/mytracks/WaypointsList.java b/MyTracks/src/com/google/android/apps/mytracks/WaypointsList.java index a9648de6c..c2752f167 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/WaypointsList.java +++ b/MyTracks/src/com/google/android/apps/mytracks/WaypointsList.java @@ -198,6 +198,17 @@ public class WaypointsList extends ListActivity super.onDestroy(); } + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.search_only, menu); + return true; + } + + /* Callback from menu/search_only.xml */ + public void onSearch(@SuppressWarnings("unused") MenuItem i) { + onSearchRequested(); + } + @Override public void onClick(View v) { WaypointCreationRequest request; @@ -266,10 +277,10 @@ public class WaypointsList extends ListActivity if (columnIndex == timeIdx) { long time = cursor.getLong(timeIdx); TextView textView = (TextView) view; - + if (time == 0) { textView.setVisibility(View.GONE); - } else { + } else { textView.setText(StringUtils.formatDateTime(WaypointsList.this, time)); textView.setVisibility(View.VISIBLE); } diff --git a/MyTracks/src/com/google/android/apps/mytracks/content/MyTracksProvider.java b/MyTracks/src/com/google/android/apps/mytracks/content/MyTracksProvider.java index bba0b5448..2f959a575 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/content/MyTracksProvider.java +++ b/MyTracks/src/com/google/android/apps/mytracks/content/MyTracksProvider.java @@ -16,7 +16,6 @@ package com.google.android.apps.mytracks.content; import com.google.android.apps.mytracks.Constants; -import com.google.android.apps.mytracks.util.ApiFeatures; import com.google.android.maps.mytracks.R; import android.content.ContentProvider; @@ -417,12 +416,9 @@ public class MyTracksProvider extends ContentProvider { } else { throw new IllegalArgumentException("Unknown URL " + url); } - - if (ApiFeatures.getInstance().canReuseSQLiteQueryBuilder()) { - Log.i(Constants.TAG, - "Build query: " + qb.buildQuery(projection, selection, selectionArgs, - null, null, sortOrder, null)); - } + Log.i(Constants.TAG, "Build query: " + + qb.buildQuery(projection, selection, selectionArgs, null, null, sortOrder, null)); + Cursor c = qb.query(db, projection, selection, selectionArgs, null, null, sortOrder); c.setNotificationUri(getContext().getContentResolver(), url); diff --git a/MyTracks/src/com/google/android/apps/mytracks/content/SearchEngine.java b/MyTracks/src/com/google/android/apps/mytracks/content/SearchEngine.java new file mode 100644 index 000000000..7eab1caa6 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/content/SearchEngine.java @@ -0,0 +1,405 @@ +/* + * Copyright 2012 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.content; + +import com.google.android.apps.mytracks.stats.TripStatistics; + +import android.content.Context; +import android.database.Cursor; +import android.location.Location; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.SortedSet; +import java.util.TreeSet; + +/** + * Engine for searching for tracks and waypoints by text. + * + * @author Rodrigo Damazio + */ +public class SearchEngine { + + /** WHERE query to get tracks by name. */ + private static final String TRACK_SELECTION_QUERY = + TracksColumns.NAME + " LIKE ? OR " + + TracksColumns.DESCRIPTION + " LIKE ? OR " + + TracksColumns.CATEGORY + " LIKE ?"; + + /** WHERE query to get waypoints by name. */ + private static final String WAYPOINT_SELECTION_QUERY = + WaypointsColumns.NAME + " LIKE ? OR " + + WaypointsColumns.DESCRIPTION + " LIKE ? OR " + + WaypointsColumns.CATEGORY + " LIKE ?"; + + /** Order of track results. */ + private static final String TRACK_SELECTION_ORDER = TracksColumns._ID + " DESC LIMIT 1000"; + + /** Order of waypoint results. */ + private static final String WAYPOINT_SELECTION_ORDER = WaypointsColumns._ID + " DESC"; + + /** How much we promote a match in the track category. */ + private static final double TRACK_CATEGORY_PROMOTION = 2.0; + + /** How much we promote a match in the track description. */ + private static final double TRACK_DESCRIPTION_PROMOTION = 8.0; + + /** How much we promote a match in the track name. */ + private static final double TRACK_NAME_PROMOTION = 16.0; + + /** How much we promote a waypoint result if it's in the currently-selected track. */ + private static final double CURRENT_TRACK_WAYPOINT_PROMOTION = 2.0; + + /** How much we promote a track result if it's the currently-selected track. */ + private static final double CURRENT_TRACK_DEMOTION = 0.5; + + /** Maximum number of waypoints which will be retrieved and scored. */ + private static final int MAX_SCORED_WAYPOINTS = 100; + + /** Oldest timestamp for which we rank based on time (2000-01-01 00:00:00.000) */ + private static final long OLDEST_ALLOWED_TIMESTAMP = 946692000000L; + + /** + * Description of a search query, along with all contextual data needed to execute it. + */ + public static class SearchQuery { + public SearchQuery(String textQuery, Location currentLocation, long currentTrackId, + long currentTimestamp) { + this.textQuery = textQuery.toLowerCase(); + this.currentLocation = currentLocation; + this.currentTrackId = currentTrackId; + this.currentTimestamp = currentTimestamp; + } + + public final String textQuery; + public final Location currentLocation; + public final long currentTrackId; + public final long currentTimestamp; + } + + /** + * Description of a search result which has been retrieved and scored. + */ + public static class ScoredResult { + ScoredResult(Track track, double score) { + this.track = track; + this.waypoint = null; + this.score = score; + } + + ScoredResult(Waypoint waypoint, double score) { + this.track = null; + this.waypoint = waypoint; + this.score = score; + } + + public final Track track; + public final Waypoint waypoint; + public final double score; + + @Override + public String toString() { + return "ScoredResult [" + + (track != null ? ("trackId=" + track.getId() + ", ") : "") + + (waypoint != null ? ("wptId=" + waypoint.getId() + ", ") : "") + + "score=" + score + "]"; + } + } + + /** Comparador for scored results. */ + private static final Comparator SCORED_RESULT_COMPARATOR = + new Comparator() { + @Override + public int compare(ScoredResult r1, ScoredResult r2) { + // Score ordering. + int scoreDiff = Double.compare(r2.score, r1.score); + if (scoreDiff != 0) { + return scoreDiff; + } + + // Make tracks come before waypoints. + if (r1.waypoint != null && r2.track != null) { + return 1; + } else if (r1.track != null && r2.waypoint != null) { + return -1; + } + + // Finally, use arbitrary ordering, by ID. + long id1 = r1.track != null ? r1.track.getId() : r1.waypoint.getId(); + long id2 = r2.track != null ? r2.track.getId() : r2.waypoint.getId(); + long idDiff = id2 - id1; + return Long.signum(idDiff); + } + }; + + private final MyTracksProviderUtils providerUtils; + + public SearchEngine(Context ctx) { + providerUtils = MyTracksProviderUtils.Factory.get(ctx); + } + + public SearchEngine(MyTracksProviderUtils providerUtils) { + this.providerUtils = providerUtils; + } + + /** + * Executes a search query and returns a set of sorted results. + * + * @param query the query to execute + * @return a set of results, sorted according to their score + */ + public SortedSet search(SearchQuery query) { + ArrayList tracks = new ArrayList(); + ArrayList waypoints = new ArrayList(); + TreeSet scoredResults = new TreeSet(SCORED_RESULT_COMPARATOR); + + retrieveTracks(query, tracks); + retrieveWaypoints(query, waypoints); + + scoreTrackResults(tracks, query, scoredResults); + scoreWaypointResults(waypoints, query, scoredResults); + + return scoredResults; + } + + /** + * Retrieves tracks matching the given query from the database. + * + * @param query the query to retrieve for + * @param tracks list to fill with the resulting tracks + */ + private void retrieveTracks(SearchQuery query, ArrayList tracks) { + String queryLikeSelection = "%" + query.textQuery + "%"; + String[] trackSelectionArgs = new String[] { + queryLikeSelection, + queryLikeSelection, + queryLikeSelection }; + + Cursor tracksCursor = providerUtils.getTracksCursor( + TRACK_SELECTION_QUERY, trackSelectionArgs, TRACK_SELECTION_ORDER); + if (tracksCursor != null) { + try { + tracks.ensureCapacity(tracksCursor.getCount()); + + while (tracksCursor.moveToNext()) { + tracks.add(providerUtils.createTrack(tracksCursor)); + } + } finally { + tracksCursor.close(); + } + } + } + + /** + * Retrieves waypoints matching the given query from the database. + * + * @param query the query to retrieve for + * @param waypoints list to fill with the resulting waypoints + */ + private void retrieveWaypoints(SearchQuery query, ArrayList waypoints) { + String queryLikeSelection2 = "%" + query.textQuery + "%"; + String[] waypointSelectionArgs = new String[] { + queryLikeSelection2, + queryLikeSelection2, + queryLikeSelection2 }; + Cursor waypointsCursor = providerUtils.getWaypointsCursor( + WAYPOINT_SELECTION_QUERY, waypointSelectionArgs, WAYPOINT_SELECTION_ORDER, + MAX_SCORED_WAYPOINTS); + if (waypointsCursor != null) { + try { + waypoints.ensureCapacity(waypointsCursor.getCount()); + + while (waypointsCursor.moveToNext()) { + waypoints.add(providerUtils.createWaypoint(waypointsCursor)); + } + } finally { + waypointsCursor.close(); + } + } + } + + /** + * Scores a collection of track results. + * + * @param tracks the results to score + * @param query the query to score for + * @param output the collection to fill with scored results + */ + private void scoreTrackResults(Collection tracks, SearchQuery query, Collection output) { + for (Track track : tracks) { + // Calculate the score. + double score = scoreTrackResult(query, track); + + // Add to the output. + output.add(new ScoredResult(track, score)); + } + } + + /** + * Scores a single track result. + * + * @param query the query to score for + * @param track the results to score + * @return the score for the track + */ + private double scoreTrackResult(SearchQuery query, Track track) { + double score = 1.0; + + score *= getTitleBoost(query, track.getName(), track.getDescription(), track.getCategory()); + + TripStatistics statistics = track.getStatistics(); + // TODO: Also boost for proximity to the currently-centered position on the map. + score *= getDistanceBoost(query, statistics.getMeanLatitude(), statistics.getMeanLongitude()); + + long meanTimestamp = (statistics.getStartTime() + statistics.getStopTime()) / 2L; + score *= getTimeBoost(query, meanTimestamp); + + // Score the currently-selected track lower (user is already there, wouldn't be searching for it). + if (track.getId() == query.currentTrackId) { + score *= CURRENT_TRACK_DEMOTION; + } + + return score; + } + + /** + * Scores a collection of waypoint results. + * + * @param waypoints the results to score + * @param query the query to score for + * @param output the collection to fill with scored results + */ + private void scoreWaypointResults(Collection waypoints, SearchQuery query, Collection output) { + for (Waypoint waypoint : waypoints) { + // Calculate the score. + double score = scoreWaypointResult(query, waypoint); + + // Add to the output. + output.add(new ScoredResult(waypoint, score)); + } + } + + /** + * Scores a single waypoint result. + * + * @param query the query to score for + * @param waypoint the results to score + * @return the score for the waypoint + */ + private double scoreWaypointResult(SearchQuery query, Waypoint waypoint) { + double score = 1.0; + + Location location = waypoint.getLocation(); + score *= getTitleBoost(query, waypoint.getName(), waypoint.getDescription(), waypoint.getCategory()); + // TODO: Also boost for proximity to the currently-centered position on the map. + score *= getDistanceBoost(query, location.getLatitude(), location.getLongitude()); + score *= getTimeBoost(query, location.getTime()); + + // Score waypoints in the currently-selected track higher (searching inside the current track). + if (query.currentTrackId != -1 && waypoint.getTrackId() == query.currentTrackId) { + score *= CURRENT_TRACK_WAYPOINT_PROMOTION; + } + + return score; + } + + /** + * Calculates the boosting of the score due to the field(s) in which the match occured. + * + * @param query the query to boost for + * @param name the name of the track or waypoint + * @param description the description of the track or waypoint + * @param category the category of the track or waypoint + * @return the total boost to be applied to the result + */ + private double getTitleBoost(SearchQuery query, + String name, String description, String category) { + // Title boost: track name > description > category. + double boost = 1.0; + if (name.toLowerCase().contains(query.textQuery)) { + boost *= TRACK_NAME_PROMOTION; + } + if (description.toLowerCase().contains(query.textQuery)) { + boost *= TRACK_DESCRIPTION_PROMOTION; + } + if (category.toLowerCase().contains(query.textQuery)) { + boost *= TRACK_CATEGORY_PROMOTION; + } + return boost; + } + + /** + * Calculates the boosting of the score due to the recency of the matched entity. + * + * @param query the query to boost for + * @param timestamp the timestamp to calculate the boost for + * @return the total boost to be applied to the result + */ + private double getTimeBoost(SearchQuery query, long timestamp) { + if (timestamp < OLDEST_ALLOWED_TIMESTAMP) { + // Safety: if timestamp is too old or invalid, don't rank based on time. + return 1.0; + } + + // Score recent tracks higher. + long timeAgoHours = (query.currentTimestamp - timestamp) / (60L * 60L * 1000L); + if (timeAgoHours > 0L) { + return squash(timeAgoHours); + } else { + // Should rarely happen (track recorded in the last hour). + return Double.POSITIVE_INFINITY; + } + } + + /** + * Calculates the boosting of the score due to proximity to a location. + * + * @param query the query to boost for + * @param latitude the latitude to calculate the boost for + * @param longitude the longitude to calculate the boost for + * @return the total boost to be applied to the result + */ + private double getDistanceBoost(SearchQuery query, double latitude, double longitude) { + if (query.currentLocation == null) { + return 1.0; + } + + float[] distanceResults = new float[1]; + + Location.distanceBetween( + latitude, longitude, + query.currentLocation.getLatitude(), query.currentLocation.getLongitude(), + distanceResults); + + // Score tracks close to the current location higher. + double distanceKm = distanceResults[0] / 1000.0; + if (distanceKm > 0.0) { + // Use the inverse of the amortized distance. + return squash(distanceKm); + } else { + // Should rarely happen (distance is exactly 0). + return Double.POSITIVE_INFINITY; + } + } + + /** + * Squashes a number by calculating 1 / log (1 + x). + */ + private static double squash(double x) { + return 1.0 / Math.log1p(x); + } +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/content/SearchEngineProvider.java b/MyTracks/src/com/google/android/apps/mytracks/content/SearchEngineProvider.java new file mode 100644 index 000000000..ae7c64fe1 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/content/SearchEngineProvider.java @@ -0,0 +1,44 @@ +/* + * Copyright 2012 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.content; + +import android.content.Context; +import android.content.SearchRecentSuggestionsProvider; +import android.provider.SearchRecentSuggestions; + +/** + * Content provider for search suggestions. + * + * @author Rodrigo Damazio + */ +public class SearchEngineProvider extends SearchRecentSuggestionsProvider { + + private static final String AUTHORITY = "com.google.android.maps.mytracks.search"; + private static final int MODE = DATABASE_MODE_QUERIES; + + public SearchEngineProvider() { + setupSuggestions(AUTHORITY, MODE); + } + + // TODO: Also add suggestions from the database. + + /** + * Creates and returns a helper for adding recent queries or clearing the recent query history. + */ + public static SearchRecentSuggestions newHelper(Context context) { + return new SearchRecentSuggestions(context, AUTHORITY, MODE); + } +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/content/TrackDataHub.java b/MyTracks/src/com/google/android/apps/mytracks/content/TrackDataHub.java index b4968f680..4060acff8 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/content/TrackDataHub.java +++ b/MyTracks/src/com/google/android/apps/mytracks/content/TrackDataHub.java @@ -28,7 +28,7 @@ import com.google.android.apps.mytracks.content.MyTracksProviderUtils.DoubleBuff import com.google.android.apps.mytracks.content.MyTracksProviderUtils.LocationIterator; import com.google.android.apps.mytracks.content.TrackDataListener.ProviderState; import com.google.android.apps.mytracks.content.TrackDataListeners.ListenerRegistration; -import com.google.android.apps.mytracks.util.ApiFeatures; +import com.google.android.apps.mytracks.util.ApiAdapterFactory; import com.google.android.apps.mytracks.util.LocationUtils; import com.google.android.maps.mytracks.R; @@ -196,7 +196,7 @@ public class TrackDataHub { private DoubleBufferedLocationFactory locationFactory; private boolean started = false; - + /** * Builds a new {@link TrackDataHub} instance. */ @@ -274,7 +274,7 @@ public class TrackDataHub { Log.w(TAG, "Not started, ignoring"); return; } - + // Unregister internal listeners even if there are external listeners registered. dataSourceManager.unregisterAllListeners(); listenerHandlerThread.getLooper().quit(); @@ -399,7 +399,7 @@ public class TrackDataHub { // Save the selection to memory and flush. selectedTrackId = trackId; - ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges( + ApiAdapterFactory.getApiAdapter().applyPreferenceChanges( preferences.edit().putLong(SELECTED_TRACK_KEY, trackId)); // Force it to reload data from the beginning. diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/AuthManager.java b/MyTracks/src/com/google/android/apps/mytracks/io/AuthManager.java deleted file mode 100644 index 6276a158b..000000000 --- a/MyTracks/src/com/google/android/apps/mytracks/io/AuthManager.java +++ /dev/null @@ -1,93 +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.io; - -import android.content.Intent; - -/** - * This interface describes a class that will fetch and maintain a Google - * authentication token. - * - * @author Sandor Dornbush - */ -public interface AuthManager { - /** - * Callback for authentication token retrieval operations. - */ - public interface AuthCallback { - /** - * Indicates that we're done fetching an auth token. - * - * @param success if true, indicates we have the requested auth token available - * to be retrieved using {@link AuthManager#getAuthToken} - */ - void onAuthResult(boolean success); - } - - /** - * Initializes the login process. The user should be asked to login if they - * haven't already. The {@link AuthCallback} provided will be executed when the - * auth token fetching is done (successfully or not). - * - * @param whenFinished A {@link AuthCallback} to execute when the auth token - * fetching is done - */ - void doLogin(AuthCallback whenFinished, Object o); - - /** - * The {@link android.app.Activity} owner of this class should call this - * function when it gets {@link android.app.Activity#onActivityResult} with - * the request code passed into the constructor. The resultCode and results - * should come directly from the {@link android.app.Activity#onActivityResult} - * function. This function will return true if an auth token was successfully - * fetched or the process is not finished. - * - * @param resultCode The result code passed in to the - * {@link android.app.Activity}'s - * {@link android.app.Activity#onActivityResult} function - * @param results The data passed in to the {@link android.app.Activity}'s - * {@link android.app.Activity#onActivityResult} function - */ - void authResult(int resultCode, Intent results); - - /** - * Returns the current auth token. Response may be null if no valid auth - * token has been fetched. - * - * @return The current auth token or null if no auth token has been - * fetched - */ - String getAuthToken(); - - /** - * Invalidates the existing auth token and request a new one. The - * {@link Runnable} provided will be executed when the new auth token is - * successfully fetched. - * - * @param whenFinished A {@link Runnable} to execute when a new auth token - * is successfully fetched - */ - void invalidateAndRefresh(AuthCallback whenFinished); - - /** - * Returns an object that represents the given account, if possible. - * - * @param accountName the name of the account - * @param accountType the type of the account - * @return the account object - */ - Object getAccountObject(String accountName, String accountType); -} diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/AuthManagerFactory.java b/MyTracks/src/com/google/android/apps/mytracks/io/AuthManagerFactory.java deleted file mode 100644 index 450449b58..000000000 --- a/MyTracks/src/com/google/android/apps/mytracks/io/AuthManagerFactory.java +++ /dev/null @@ -1,57 +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.io; - -import static com.google.android.apps.mytracks.Constants.TAG; - -import android.app.Activity; -import android.os.Build; -import android.os.Bundle; -import android.util.Log; - -/** - * A factory for getting the platform specific AuthManager. - * - * @author Sandor Dornbush - */ -public class AuthManagerFactory { - - private AuthManagerFactory() { - } - - /** - * Returns whether the modern AuthManager should be used - */ - public static boolean useModernAuthManager() { - return Integer.parseInt(Build.VERSION.SDK) >= 7; - } - - /** - * Get a right {@link AuthManager} for the platform. - * @return A new AuthManager - */ - public static AuthManager getAuthManager(Activity activity, int code, - Bundle extras, boolean requireGoogle, String service) { - if (useModernAuthManager()) { - Log.i(TAG, "Creating modern auth manager: " + service); - return new ModernAuthManager(activity, service); - } else { - Log.i(TAG, "Creating legacy auth manager: " + service); - return new AuthManagerOld(activity, code, extras, requireGoogle, service); - } - } - -} diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/AuthManagerOld.java b/MyTracks/src/com/google/android/apps/mytracks/io/AuthManagerOld.java deleted file mode 100644 index 4e80345fb..000000000 --- a/MyTracks/src/com/google/android/apps/mytracks/io/AuthManagerOld.java +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Copyright 2009 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ -package com.google.android.apps.mytracks.io; - -import com.google.android.googlelogindist.GoogleLoginServiceConstants; -import com.google.android.googlelogindist.GoogleLoginServiceHelper; - -import android.app.Activity; -import android.content.Intent; -import android.os.Bundle; - -/** - * AuthManager keeps track of the current auth token for a user. The advantage - * over just passing around a String is that this class can renew the auth - * token if necessary, and it will change for all classes using this - * AuthManager. - */ -public class AuthManagerOld implements AuthManager { - /** The activity that will handle auth result callbacks. */ - private final Activity activity; - - /** The code used to tell the activity that it is an auth result. */ - private final int code; - - /** Extras to pass into the getCredentials function. */ - private final Bundle extras; - - /** True if the account must be a Google account (not a domain account). */ - private final boolean requireGoogle; - - /** The name of the service to authorize for. */ - private final String service; - - /** The handler to call when a new auth token is fetched. */ - private AuthCallback authCallback; - - /** The most recently fetched auth token or null if none is available. */ - private String authToken; - - /** - * AuthManager requires many of the same parameters as - * {@link GoogleLoginServiceHelper#getCredentials(Activity, int, Bundle, - * boolean, String, boolean)}. The activity must have - * a handler in {@link Activity#onActivityResult} that calls - * {@link #authResult(int, Intent)} if the request code is the code given - * here. - * - * @param activity An activity with a handler in - * {@link Activity#onActivityResult} that calls - * {@link #authResult(int, Intent)} when {@literal code} is the request - * code - * @param code The request code to pass to - * {@link Activity#onActivityResult} when - * {@link #authResult(int, Intent)} should be called - * @param extras A {@link Bundle} of extras for - * {@link GoogleLoginServiceHelper} - * @param requireGoogle True if the account must be a Google account - * @param service The name of the service to authenticate as - */ - public AuthManagerOld(Activity activity, int code, Bundle extras, - boolean requireGoogle, String service) { - this.activity = activity; - this.code = code; - this.extras = extras; - this.requireGoogle = requireGoogle; - this.service = service; - } - - @Override - public void doLogin(AuthCallback callback, Object o) { - authCallback = callback; - activity.runOnUiThread(new LoginRunnable()); - } - - /** - * Runnable which actually gets login credentials. - */ - private class LoginRunnable implements Runnable { - @Override - public void run() { - GoogleLoginServiceHelper.getCredentials( - activity, code, extras, requireGoogle, service, true); - } - } - - @Override - public void authResult(int resultCode, Intent results) { - if (resultCode != Activity.RESULT_OK) { - runAuthCallback(false); - return; - } - - authToken = results.getStringExtra(GoogleLoginServiceConstants.AUTHTOKEN_KEY); - if (authToken == null) { - // Retry, without prompting the user. - GoogleLoginServiceHelper.getCredentials( - activity, code, extras, requireGoogle, service, false); - } else { - // Notify all active listeners that we have a new auth token. - runAuthCallback(true); - } - } - - private void runAuthCallback(boolean success) { - authCallback.onAuthResult(success); - authCallback = null; - } - - @Override - public String getAuthToken() { - return authToken; - } - - @Override - public void invalidateAndRefresh(AuthCallback callback) { - authCallback = callback; - activity.runOnUiThread(new Runnable() { - public void run() { - GoogleLoginServiceHelper.invalidateAuthToken(activity, code, authToken); - } - }); - } - - @Override - public Object getAccountObject(String accountName, String accountType) { - throw new UnsupportedOperationException("Legacy auth manager knows nothing about accounts"); - } -} diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/ModernAuthManager.java b/MyTracks/src/com/google/android/apps/mytracks/io/ModernAuthManager.java deleted file mode 100644 index 7dcbfcd1d..000000000 --- a/MyTracks/src/com/google/android/apps/mytracks/io/ModernAuthManager.java +++ /dev/null @@ -1,217 +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.io; - -import static com.google.android.apps.mytracks.Constants.ACCOUNT_TYPE; -import static com.google.android.apps.mytracks.Constants.TAG; - -import com.google.android.apps.mytracks.AccountChooser; - -import android.accounts.Account; -import android.accounts.AccountManager; -import android.accounts.AccountManagerCallback; -import android.accounts.AccountManagerFuture; -import android.accounts.AuthenticatorException; -import android.accounts.OperationCanceledException; -import android.app.Activity; -import android.content.Intent; -import android.os.Bundle; -import android.util.Log; - -import java.io.IOException; - -/** - * AuthManager keeps track of the current auth token for a user. The advantage - * over just passing around a String is that this class can renew the auth - * token if necessary, and it will change for all classes using this - * AuthManager. - */ -public class ModernAuthManager implements AuthManager { - /** The activity that will handle auth result callbacks. */ - private final Activity activity; - - /** The name of the service to authorize for. */ - private final String service; - - /** The most recently fetched auth token or null if none is available. */ - private String authToken; - - private final AccountManager accountManager; - - private AuthCallback authCallback; - - private Account lastAccount; - - /** - * AuthManager requires many of the same parameters as - * {@link com.google.android.googlelogindist.GoogleLoginServiceHelper - * #getCredentials(Activity, int, Bundle, boolean, String, boolean)}. - * The activity must have a handler in {@link Activity#onActivityResult} that - * calls {@link #authResult(int, Intent)} if the request code is the code - * given here. - * - * @param activity An activity with a handler in - * {@link Activity#onActivityResult} that calls - * {@link #authResult(int, Intent)} when {@literal code} is the request - * code - * @param service The name of the service to authenticate as - */ - public ModernAuthManager(Activity activity, String service) { - this.activity = activity; - this.service = service; - this.accountManager = AccountManager.get(activity); - } - - /** - * Call this to do the initial login. The user will be asked to login if - * they haven't already. The {@link Runnable} provided will be executed - * when the auth token is successfully fetched. - * - * @param runnable A {@link Runnable} to execute when the auth token - * has been successfully fetched and is available via - * {@link #getAuthToken()} - */ - public void doLogin(AuthCallback runnable, Object o) { - this.authCallback = runnable; - if (!(o instanceof Account)) { - throw new IllegalArgumentException("ModernAuthManager requires an account."); - } - Account account = (Account) o; - doLogin(account); - } - - private void doLogin(Account account) { - // Keep the account in case we need to retry. - this.lastAccount = account; - - // NOTE: Many Samsung phones have a crashing bug in - // AccountManager#getAuthToken(Account, String, boolean, AccountManagerCallback) - // so we use the other version of the method. - // More details here: - // http://forum.xda-developers.com/showthread.php?p=15155487 - // http://android.git.kernel.org/?p=platform/frameworks/base.git;a=blob;f=core/java/android/accounts/AccountManagerService.java - accountManager.getAuthToken(account, service, null, activity, - new AccountManagerCallback() { - public void run(AccountManagerFuture future) { - try { - authToken = future.getResult().getString( - AccountManager.KEY_AUTHTOKEN); - Log.i(TAG, "Got auth token"); - } catch (OperationCanceledException e) { - Log.e(TAG, "Auth token operation Canceled", e); - } catch (IOException e) { - Log.e(TAG, "Auth token IO exception", e); - } catch (AuthenticatorException e) { - Log.e(TAG, "Authentication Failed", e); - } - - runAuthCallback(); - } - }, null /* handler */); - } - - /** - * The {@link Activity} passed into the constructor should call this - * function when it gets {@link Activity#onActivityResult} with the request - * code passed into the constructor. The resultCode and results should - * come directly from the {@link Activity#onActivityResult} function. This - * function will return true if an auth token was successfully fetched or - * the process is not finished. - * - * @param resultCode The result code passed in to the {@link Activity}'s - * {@link Activity#onActivityResult} function - * @param results The data passed in to the {@link Activity}'s - * {@link Activity#onActivityResult} function - */ - public void authResult(int resultCode, Intent results) { - boolean retry = false; - if (results == null) { - Log.e(TAG, "No auth token!!"); - } else { - authToken = results.getStringExtra(AccountManager.KEY_AUTHTOKEN); - retry = results.getBooleanExtra("retry", false); - } - - if (authToken == null && retry) { - Log.i(TAG, "Retrying to get auth result"); - doLogin(lastAccount); - return; - } - - runAuthCallback(); - } - - /** - * Returns the current auth token. Response may be null if no valid auth - * token has been fetched. - * - * @return The current auth token or null if no auth token has been - * fetched - */ - public String getAuthToken() { - return authToken; - } - - /** - * Invalidates the existing auth token and request a new one. The callback - * provided will be executed when the new auth token is successfully fetched. - * - * @param callback A callback to execute when a new auth token is successfully - * fetched - */ - public void invalidateAndRefresh(final AuthCallback callback) { - this.authCallback = callback; - - activity.runOnUiThread(new Runnable() { - public void run() { - accountManager.invalidateAuthToken(ACCOUNT_TYPE, authToken); - authToken = null; - - AccountChooser accountChooser = new AccountChooser(); - accountChooser.chooseAccount(activity, - new AccountChooser.AccountHandler() { - @Override - public void onAccountSelected(Account account) { - if (account != null) { - doLogin(account); - } else { - runAuthCallback(); - } - } - }); - } - }); - } - - private void runAuthCallback() { - lastAccount = null; - - if (authCallback != null) { - (new Thread() { - @Override - public void run() { - authCallback.onAuthResult(authToken != null); - authCallback = null; - } - }).start(); - } - } - - @Override - public Object getAccountObject(String accountName, String accountType) { - return new Account(accountName, accountType); - } -} diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/SendToDocs.java b/MyTracks/src/com/google/android/apps/mytracks/io/SendToDocs.java deleted file mode 100644 index ad575e278..000000000 --- a/MyTracks/src/com/google/android/apps/mytracks/io/SendToDocs.java +++ /dev/null @@ -1,280 +0,0 @@ -/* - * Copyright 2008 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ -package com.google.android.apps.mytracks.io; - -import com.google.android.apps.mytracks.Constants; -import com.google.android.apps.mytracks.ProgressIndicator; -import com.google.android.apps.mytracks.content.MyTracksProviderUtils; -import com.google.android.apps.mytracks.content.Track; -import com.google.android.apps.mytracks.io.docs.DocsHelper; -import com.google.android.apps.mytracks.io.gdata.GDataClientFactory; -import com.google.android.apps.mytracks.io.gdata.GDataWrapper; -import com.google.android.common.gdata.AndroidXmlParserFactory; -import com.google.android.maps.mytracks.R; -import com.google.wireless.gdata.client.GDataClient; -import com.google.wireless.gdata.client.GDataServiceClient; -import com.google.wireless.gdata.docs.DocumentsClient; -import com.google.wireless.gdata.docs.SpreadsheetsClient; -import com.google.wireless.gdata.docs.XmlDocsGDataParserFactory; - -import android.app.Activity; -import android.content.Context; -import android.content.SharedPreferences; -import android.util.Log; - -import java.io.IOException; - -/** - * A helper class used to transmit tracks statistics to Google Docs/Trix. - * - * @author Sandor Dornbush - */ -public class SendToDocs { - /** The GData service name for Google Spreadsheets (aka Trix) */ - public static final String GDATA_SERVICE_NAME_TRIX = "wise"; - - /** The GData service name for the Google Docs Document List */ - public static final String GDATA_SERVICE_NAME_DOCLIST = "writely"; - - private final Activity activity; - private final AuthManager trixAuth; - private final AuthManager docListAuth; - private final ProgressIndicator progressIndicator; - private final boolean metricUnits; - - private boolean success = true; - private Runnable onCompletion = null; - - - public SendToDocs(Activity activity, AuthManager trixAuth, - AuthManager docListAuth, ProgressIndicator progressIndicator) { - this.activity = activity; - this.trixAuth = trixAuth; - this.docListAuth = docListAuth; - this.progressIndicator = progressIndicator; - - SharedPreferences preferences = activity.getSharedPreferences( - Constants.SETTINGS_NAME, Context.MODE_PRIVATE); - if (preferences != null) { - metricUnits = - preferences.getBoolean(activity.getString(R.string.metric_units_key), - true); - } else { - metricUnits = true; - } - } - - public void sendToDocs(final long trackId) { - Log.d(Constants.TAG, - "Sending to Google Docs: trackId = " + trackId); - new Thread("SendToGoogleDocs") { - @Override - public void run() { - doUpload(trackId); - } - }.start(); - } - - private void doUpload(long trackId) { - success = false; - try { - if (trackId == -1) { - Log.w(Constants.TAG, "Cannot get track id."); - return; - } - - // Get the track from the provider: - Track track = - MyTracksProviderUtils.Factory.get(activity).getTrack(trackId); - if (track == null) { - Log.w(Constants.TAG, "Cannot get track."); - return; - } - - // Transmit track stats via GData feed: - // ------------------------------- - - Log.d(Constants.TAG, "SendToDocs: Uploading to spreadsheet"); - success = uploadToDocs(track); - Log.d(Constants.TAG, "SendToDocs: Done."); - } finally { - if (onCompletion != null) { - activity.runOnUiThread(onCompletion); - } - } - } - - public boolean wasSuccess() { - return success; - } - - public void setOnCompletion(Runnable onCompletion) { - this.onCompletion = onCompletion; - } - - /** - * Uploads the statistics about a track to Google Docs using the docs GData - * feed. - * - * @param track the track - */ - private boolean uploadToDocs(Track track) { - GDataWrapper docListWrapper = new GDataWrapper(); - docListWrapper.setAuthManager(docListAuth); - docListWrapper.setRetryOnAuthFailure(true); - - GDataWrapper trixWrapper = new GDataWrapper(); - trixWrapper.setAuthManager(trixAuth); - trixWrapper.setRetryOnAuthFailure(true); - - DocsHelper docsHelper = new DocsHelper(); - - GDataClient androidClient = null; - try { - androidClient = GDataClientFactory.getGDataClient(activity); - SpreadsheetsClient gdataClient = new SpreadsheetsClient(androidClient, - new XmlDocsGDataParserFactory(new AndroidXmlParserFactory())); - trixWrapper.setClient(gdataClient); - Log.d(Constants.TAG, - "GData connection prepared: " + this.docListAuth); - String sheetTitle = "My Tracks"; - - if (track.getCategory() != null && !track.getCategory().equals("")) { - sheetTitle += "-" + track.getCategory(); - } - - DocumentsClient docsGdataClient = new DocumentsClient(androidClient, - new XmlDocsGDataParserFactory(new AndroidXmlParserFactory())); - docListWrapper.setClient(docsGdataClient); - - // First try to find the spreadsheet: - String spreadsheetId = null; - try { - spreadsheetId = docsHelper.requestSpreadsheetId(docListWrapper, - sheetTitle); - } catch (IOException e) { - Log.i(Constants.TAG, "Spreadsheet lookup failed.", e); - return false; - } - - if (spreadsheetId == null) { - progressIndicator.setProgressValue(65); - // Waiting a few seconds and trying again. Maybe the server just had a - // hickup (unfortunately that happens quite a lot...). - try { - Thread.sleep(5000); - } catch (InterruptedException e) { - Log.e(Constants.TAG, "Sleep interrupted", e); - } - - try { - spreadsheetId = docsHelper.requestSpreadsheetId(docListWrapper, - sheetTitle); - } catch (IOException e) { - Log.i(Constants.TAG, "2nd spreadsheet lookup failed.", e); - return false; - } - } - - // We were unable to find an existing spreadsheet, so create a new one. - progressIndicator.setProgressValue(70); - if (spreadsheetId == null) { - Log.i(Constants.TAG, "Creating new spreadsheet: " + sheetTitle); - - try { - spreadsheetId = docsHelper.createSpreadsheet(activity, docListWrapper, - sheetTitle); - } catch (IOException e) { - Log.i(Constants.TAG, "Failed to create new spreadsheet " - + sheetTitle, e); - return false; - } - progressIndicator.setProgressValue(80); - - if (spreadsheetId == null) { - progressIndicator.setProgressValue(85); - // The previous creation might have succeeded even though GData - // reported an error. Seems to be a know bug, - // see http://code.google.com/p/gdata-issues/issues/detail?id=929 - // Try to find the created spreadsheet: - Log.w(Constants.TAG, - "Create might have failed. Trying to find created document."); - try { - Thread.sleep(5000); - } catch (InterruptedException e) { - Log.e(Constants.TAG, "Sleep interrupted", e); - } - - try { - spreadsheetId = docsHelper.requestSpreadsheetId(docListWrapper, - sheetTitle); - } catch (IOException e) { - Log.i(Constants.TAG, "Failed create-failed lookup", e); - return false; - } - - if (spreadsheetId == null) { - progressIndicator.setProgressValue(87); - // Re-try - try { - Thread.sleep(5000); - } catch (InterruptedException e) { - Log.e(Constants.TAG, "Sleep interrupted", e); - } - - try { - spreadsheetId = docsHelper.requestSpreadsheetId(docListWrapper, - sheetTitle); - } catch (IOException e) { - Log.i(Constants.TAG, "Failed create-failed relookup", e); - return false; - } - } - if (spreadsheetId == null) { - Log.i(Constants.TAG, - "Creating new spreadsheet really failed."); - return false; - } - } - } - - String worksheetId = null; - try { - worksheetId = docsHelper.getWorksheetId(trixWrapper, spreadsheetId); - if (worksheetId == null) { - throw new IOException("Worksheet ID lookup returned empty"); - } - } catch (IOException e) { - Log.i(Constants.TAG, "Looking up worksheet id failed.", e); - return false; - } - - progressIndicator.setProgressValue(90); - - docsHelper.addTrackRow(activity, trixAuth, spreadsheetId, worksheetId, - track, metricUnits); - Log.i(Constants.TAG, "Done uploading to docs."); - } catch (IOException e) { - Log.e(Constants.TAG, "Unable to upload docs.", e); - return false; - } finally { - if (androidClient != null) { - androidClient.close(); - } - } - return true; - } -} diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/SendToFusionTables.java b/MyTracks/src/com/google/android/apps/mytracks/io/SendToFusionTables.java deleted file mode 100644 index 48e55938b..000000000 --- a/MyTracks/src/com/google/android/apps/mytracks/io/SendToFusionTables.java +++ /dev/null @@ -1,716 +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.io; - -import static com.google.android.apps.mytracks.Constants.TAG; - -import com.google.android.apps.mytracks.Constants; -import com.google.android.apps.mytracks.ProgressIndicator; -import com.google.android.apps.mytracks.content.MyTracksProviderUtils; -import com.google.android.apps.mytracks.content.Track; -import com.google.android.apps.mytracks.content.Waypoint; -import com.google.android.apps.mytracks.io.gdata.GDataWrapper; -import com.google.android.apps.mytracks.io.gdata.GDataWrapper.QueryFunction; -import com.google.android.apps.mytracks.io.sendtogoogle.SendType; -import com.google.android.apps.mytracks.stats.DoubleBuffer; -import com.google.android.apps.mytracks.stats.TripStatistics; -import com.google.android.apps.mytracks.util.ApiFeatures; -import com.google.android.apps.mytracks.util.LocationUtils; -import com.google.android.apps.mytracks.util.StringUtils; -import com.google.android.apps.mytracks.util.SystemUtils; -import com.google.android.apps.mytracks.util.UnitConversions; -import com.google.android.maps.mytracks.R; -import com.google.api.client.googleapis.GoogleHeaders; -import com.google.api.client.googleapis.MethodOverride; -import com.google.api.client.http.GenericUrl; -import com.google.api.client.http.HttpRequest; -import com.google.api.client.http.HttpRequestFactory; -import com.google.api.client.http.HttpResponse; -import com.google.api.client.http.HttpResponseException; -import com.google.api.client.http.HttpTransport; -import com.google.api.client.http.InputStreamContent; -import com.google.api.client.util.Strings; - -import android.app.Activity; -import android.content.Context; -import android.content.SharedPreferences; -import android.database.Cursor; -import android.location.Location; -import android.util.Log; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.net.URLEncoder; -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; -import java.util.Vector; - -/** - * A helper class used to transmit tracks to Google Fusion Tables. - * A new instance should be used for each upload. - * - * @author Leif Hendrik Wilden - */ -public class SendToFusionTables implements Runnable { - - private static final String CONTENT_TYPE = "application/x-www-form-urlencoded"; - - /** - * Listener invoked when sending to fusion tables completes. - */ - public interface OnSendCompletedListener { - void onSendCompleted(String tableId, boolean success); - } - - /** The GData service id for Fusion Tables. */ - public static final String SERVICE_ID = "fusiontables"; - - /** The path for viewing a map visualization of a table. */ - private static final String FUSIONTABLES_MAP = - "https://www.google.com/fusiontables/embedviz?" + - "viz=MAP&q=select+col0,+col1,+col2,+col3+from+%s+&h=false&" + - "lat=%f&lng=%f&z=%d&t=1&l=col2"; - - /** Standard base feed url for Fusion Tables. */ - private static final String FUSIONTABLES_BASE_FEED_URL = - "https://www.google.com/fusiontables/api/query"; - - private static final int MAX_POINTS_PER_UPLOAD = 2048; - - private static final String GDATA_VERSION = "2"; - - // This class reports upload status to the user as a completion percentage - // using a progress bar. Progress is defined as follows: - // - // 0% Getting track metadata - // 5% Creating Fusion Table (GData to FT server) - // 10%-90% Uploading the track data to Fusion Tables - // 95% Uploading waypoints - // 100% Done - private static final int PROGRESS_INITIALIZATION = 0; - private static final int PROGRESS_FUSION_TABLE_CREATE = 5; - private static final int PROGRESS_UPLOAD_DATA_MIN = 10; - private static final int PROGRESS_UPLOAD_DATA_MAX = 90; - private static final int PROGRESS_UPLOAD_WAYPOINTS = 95; - private static final int PROGRESS_COMPLETE = 100; - - private final Activity context; - private final AuthManager auth; - private final long trackId; - private final ProgressIndicator progressIndicator; - private final OnSendCompletedListener onCompletion; - private final StringUtils stringUtils; - private final MyTracksProviderUtils providerUtils; - - // Progress status - private int totalLocationsRead; - private int totalLocationsPrepared; - private int totalLocationsUploaded; - private int totalLocations; - private int totalSegmentsUploaded; - - private HttpRequestFactory httpRequestFactory; - private String tableId; - - private static String MARKER_TYPE_START = "large_green"; - private static String MARKER_TYPE_END = "large_red"; - private static String MARKER_TYPE_WAYPOINT = "large_yellow"; - - public SendToFusionTables(Activity context, AuthManager auth, long trackId, - ProgressIndicator progressIndicator, OnSendCompletedListener onCompletion) { - this.context = context; - this.auth = auth; - this.trackId = trackId; - this.progressIndicator = progressIndicator; - this.onCompletion = onCompletion; - this.stringUtils = new StringUtils(context); - this.providerUtils = MyTracksProviderUtils.Factory.get(context); - - HttpTransport transport = ApiFeatures.getInstance().getApiAdapter().getHttpTransport(); - httpRequestFactory = transport.createRequestFactory(new MethodOverride()); - } - - @Override - public void run() { - Log.d(Constants.TAG, "Sending to Fusion tables: trackId = " + trackId); - doUpload(); - } - - 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. - TripStatistics stats = track.getStatistics(); - double latE6 = stats.getBottom() + (stats.getTop() - stats.getBottom()) / 2; - double lonE6 = stats.getLeft() + (stats.getRight() - stats.getLeft()) / 2; - int z = 15; - - // We explicitly format with Locale.US because we need the latitude and - // longitude to be formatted in a locale-independent manner. Specifically, - // we need the decimal separator to be a period rather than a comma. - return String.format(Locale.US, FUSIONTABLES_MAP, track.getTableId(), - latE6 / 1.E6, lonE6 / 1.E6, z); - } - - private void doUpload() { - boolean success = true; - try { - progressIndicator.setProgressValue(PROGRESS_INITIALIZATION); - progressIndicator.setProgressMessage( - context.getString(R.string.send_google_progress_reading_track)); - - // Get the track meta-data - Track track = providerUtils.getTrack(trackId); - if (track == null) { - Log.w(Constants.TAG, "Cannot get track."); - return; - } - - String originalDescription = track.getDescription(); - - // Create a new table: - progressIndicator.setProgressValue(PROGRESS_FUSION_TABLE_CREATE); - String creatingFormat = context.getString(R.string.send_google_progress_creating); - String serviceName = context.getString(SendType.FUSION_TABLES.getServiceName()); - progressIndicator.setProgressMessage(String.format(creatingFormat, serviceName)); - if (!createNewTable(track) || !makeTableUnlisted()) { - return; - } - - progressIndicator.setProgressValue(PROGRESS_UPLOAD_DATA_MIN); - String sendingFormat = context.getString(R.string.send_google_progress_sending); - progressIndicator.setProgressMessage(String.format(sendingFormat, serviceName)); - - // Upload all of the segments of the track plus start/end markers - if (!uploadAllTrackPoints(track, originalDescription)) { - return; - } - - progressIndicator.setProgressValue(PROGRESS_UPLOAD_WAYPOINTS); - - // Upload all the waypoints. - if (!uploadWaypoints(track)) { - return; - } - - Log.d(Constants.TAG, "SendToFusionTables: Done: " + success); - progressIndicator.setProgressValue(PROGRESS_COMPLETE); - } finally { - final boolean finalSuccess = success; - context.runOnUiThread(new Runnable() { - public void run() { - if (onCompletion != null) { - onCompletion.onSendCompleted(tableId, finalSuccess); - } - } - }); - } - } - - /** - * Creates a new table. - * If successful sets {@link #tableId}. - * - * @return true in case of success. - */ - private boolean createNewTable(Track track) { - Log.d(Constants.TAG, "Creating a new fusion table."); - String query = "CREATE TABLE '" + sqlEscape(track.getName()) + - "' (name:STRING,description:STRING,geometry:LOCATION,marker:STRING)"; - return runUpdate(query); - } - - private boolean makeTableUnlisted() { - Log.d(Constants.TAG, "Setting visibility to unlisted."); - String query = "UPDATE TABLE " + tableId + " SET VISIBILITY = UNLISTED"; - return runUpdate(query); - } - - /** - * Formats given values SQL style. Escapes single quotes with a backslash. - * - * @param values the values to format - * @return the values formatted as: ('value1','value2',...,'value_n'). - */ - private static String values(String... values) { - StringBuilder builder = new StringBuilder("("); - for (int i = 0; i < values.length; i++) { - if (i > 0) { - builder.append(','); - } - builder.append('\''); - builder.append(sqlEscape(values[i])); - builder.append('\''); - } - builder.append(')'); - return builder.toString(); - } - - private static String sqlEscape(String value) { - return value.replaceAll("'", "''"); - } - - /** - * Creates a new row representing a marker. - * - * @param name the marker name - * @param description the marker description - * @param location the marker location - * @return true in case of success. - */ - private boolean createNewPoint(String name, String description, Location location, - String marker) { - Log.d(Constants.TAG, "Creating a new row with a point."); - String query = "INSERT INTO " + tableId + " (name,description,geometry,marker) VALUES " - + values(name, description, getKmlPoint(location), marker); - return runUpdate(query); - } - - /** - * Creates a new row representing a line segment. - * - * @param track the track/segment to draw - * @return true in case of success. - */ - private boolean createNewLineString(Track track) { - Log.d(Constants.TAG, "Creating a new row with a point."); - String query = "INSERT INTO " + tableId - + " (name,description,geometry) VALUES " - + values(track.getName(), track.getDescription(), getKmlLineString(track)); - return runUpdate(query); - } - - private boolean uploadAllTrackPoints(final Track track, String originalDescription) { - SharedPreferences preferences = context.getSharedPreferences( - Constants.SETTINGS_NAME, Context.MODE_PRIVATE); - boolean metricUnits = true; - if (preferences != null) { - metricUnits = preferences.getBoolean(context.getString(R.string.metric_units_key), true); - } - - Cursor locationsCursor = providerUtils.getLocationsCursor(track.getId(), 0, -1, false); - try { - if (locationsCursor == null || !locationsCursor.moveToFirst()) { - Log.w(Constants.TAG, "Unable to get any points to upload"); - return false; - } - - totalLocationsRead = 0; - totalLocationsPrepared = 0; - totalLocationsUploaded = 0; - totalLocations = locationsCursor.getCount(); - totalSegmentsUploaded = 0; - - // Limit the number of elevation readings. Ideally we would want around 250. - int elevationSamplingFrequency = - Math.max(1, (int) (totalLocations / 250.0)); - Log.d(Constants.TAG, - "Using elevation sampling factor: " + elevationSamplingFrequency - + " on " + totalLocations); - double totalDistance = 0; - - Vector distances = new Vector(); - Vector elevations = new Vector(); - DoubleBuffer elevationBuffer = new DoubleBuffer(Constants.ELEVATION_SMOOTHING_FACTOR); - - List locations = new ArrayList(MAX_POINTS_PER_UPLOAD); - Location lastLocation = null; - do { - if (totalLocationsRead % 100 == 0) { - updateTrackDataUploadProgress(); - } - - Location loc = providerUtils.createLocation(locationsCursor); - locations.add(loc); - - if (totalLocationsRead == 0) { - // Put a marker at the first point of the first valid segment: - String name = track.getName() + " " + context.getString(R.string.marker_label_start); - createNewPoint(name, "", loc, MARKER_TYPE_START); - } - - // Add to the elevation profile. - if (loc != null && LocationUtils.isValidLocation(loc)) { - // All points go into the smoothing buffer... - elevationBuffer.setNext(metricUnits ? loc.getAltitude() - : loc.getAltitude() * UnitConversions.M_TO_FT); - if (lastLocation != null) { - double dist = lastLocation.distanceTo(loc); - totalDistance += dist; - } - - // ...but only a few points are really used to keep the url short. - if (totalLocationsRead % elevationSamplingFrequency == 0) { - distances.add(totalDistance); - elevations.add(elevationBuffer.getAverage()); - } - } - - // If the location was not valid, it's a segment split, so make sure the - // distance between the previous segment and the new one is not accounted - // for in the next iteration. - lastLocation = loc; - - // Every now and then, upload the accumulated points - if (totalLocationsRead % MAX_POINTS_PER_UPLOAD == MAX_POINTS_PER_UPLOAD - 1) { - if (!prepareAndUploadPoints(track, locations)) { - return false; - } - } - - totalLocationsRead++; - } while (locationsCursor.moveToNext()); - - // Do a final upload with what's left - if (!prepareAndUploadPoints(track, locations)) { - return false; - } - - // Put an end marker at the last point of the last valid segment: - if (lastLocation != null) { - track.setDescription("

" + originalDescription + "

" - + stringUtils.generateTrackDescription(track, distances, elevations) - + "

"); - String name = track.getName() + " " + context.getString(R.string.marker_label_end); - return createNewPoint(name, track.getDescription(), lastLocation, MARKER_TYPE_END); - } - - return true; - } finally { - if (locationsCursor != null) { - locationsCursor.close(); - } - } - } - - /** - * Appends the given location to the string in the format: - * longitude,latitude[,altitude] - * - * @param location the location to be added - * @param builder the string builder to use - */ - private void appendCoordinate(Location location, StringBuilder builder) { - builder - .append(location.getLongitude()) - .append(",") - .append(location.getLatitude()); - if (location.hasAltitude()) { - builder.append(","); - builder.append(location.getAltitude()); - } - } - - /** - * Gets a KML Point tag for the given location. - * - * @param location The location. - * @return the kml. - */ - private String getKmlPoint(Location location) { - StringBuilder builder = new StringBuilder(""); - appendCoordinate(location, builder); - builder.append(""); - return builder.toString(); - } - - /** - * Returns a KML LineString. - * - * @param track the track. - * @return the KML LineString. - */ - private String getKmlLineString(Track track) { - StringBuilder builder = new StringBuilder(""); - for (Location location : track.getLocations()) { - appendCoordinate(location, builder); - builder.append(' '); - } - builder.append(""); - return builder.toString(); - } - - private boolean prepareAndUploadPoints(Track track, List locations) { - updateTrackDataUploadProgress(); - - int numLocations = locations.size(); - if (numLocations < 2) { - Log.d(Constants.TAG, "Not preparing/uploading too few points"); - totalLocationsUploaded += numLocations; - return true; - } - - // Prepare/pre-process the points - ArrayList splitTracks = prepareLocations(track, locations); - - // Start uploading them - for (Track splitTrack : splitTracks) { - if (totalSegmentsUploaded > 1) { - splitTrack.setName(splitTrack.getName() + " " - + String.format( - context.getString(R.string.send_google_track_part_label), totalSegmentsUploaded)); - } - totalSegmentsUploaded++; - Log.d(Constants.TAG, - "SendToFusionTables: Prepared feature for upload w/ " - + splitTrack.getLocations().size() + " points."); - - // Transmit tracks via GData feed: - // ------------------------------- - Log.d(Constants.TAG, - "SendToFusionTables: Uploading to table " + tableId + " w/ auth " + auth); - if (!uploadTrackPoints(splitTrack)) { - Log.e(Constants.TAG, "Uploading failed"); - return false; - } - } - - locations.clear(); - totalLocationsUploaded += numLocations; - updateTrackDataUploadProgress(); - return true; - } - - /** - * Prepares a buffer of locations for transmission to google fusion tables. - * - * @param track the original track with meta data - * @param locations locations on the track - * @return an array of tracks each with a sub section of the points in the - * original buffer - */ - private ArrayList prepareLocations( - Track track, Iterable locations) { - ArrayList splitTracks = new ArrayList(); - - // Create segments from each full track: - Track segment = new Track(); - TripStatistics segmentStats = segment.getStatistics(); - TripStatistics trackStats = track.getStatistics(); - segment.setId(track.getId()); - segment.setName(track.getName()); - segment.setDescription(/* track.getDescription() */ ""); - segment.setCategory(track.getCategory()); - segmentStats.setStartTime(trackStats.getStartTime()); - segmentStats.setStopTime(trackStats.getStopTime()); - boolean startNewTrackSegment = false; - for (Location loc : locations) { - if (totalLocationsPrepared % 100 == 0) { - updateTrackDataUploadProgress(); - } - if (loc.getLatitude() > 90) { - startNewTrackSegment = true; - } - - if (startNewTrackSegment) { - // Close up the last segment. - prepareTrackSegment(segment, splitTracks); - - Log.d(Constants.TAG, - "MyTracksSendToFusionTables: Starting new track segment..."); - startNewTrackSegment = false; - segment = new Track(); - segment.setId(track.getId()); - segment.setName(track.getName()); - segment.setDescription(/* track.getDescription() */ ""); - segment.setCategory(track.getCategory()); - } - - if (loc.getLatitude() <= 90) { - segment.addLocation(loc); - if (segmentStats.getStartTime() < 0) { - segmentStats.setStartTime(loc.getTime()); - } - } - totalLocationsPrepared++; - } - - prepareTrackSegment(segment, splitTracks); - - return splitTracks; - } - - /** - * Prepares a track segment for sending to google fusion tables. - * The main steps are: - * - correcting end time - * - decimating locations - * - splitting into smaller tracks. - * - * The final track pieces will be put in the array list splitTracks. - * - * @param segment the original segment of the track - * @param splitTracks an array of smaller track segments - */ - private void prepareTrackSegment( - Track segment, ArrayList splitTracks) { - TripStatistics segmentStats = segment.getStatistics(); - if (segmentStats.getStopTime() < 0 - && segment.getLocations().size() > 0) { - segmentStats.setStopTime(segment.getLocations().size() - 1); - } - - /* - * Decimate to 2 meter precision. Fusion tables doesn't like too many - * points: - */ - LocationUtils.decimate(segment, 2.0); - - /* If the track still has > 2500 points, split it in pieces: */ - final int maxPoints = 2500; - if (segment.getLocations().size() > maxPoints) { - splitTracks.addAll(LocationUtils.split(segment, maxPoints)); - } else if (segment.getLocations().size() >= 2) { - splitTracks.add(segment); - } - } - - private boolean uploadTrackPoints(Track splitTrack) { - int numLocations = splitTrack.getLocations().size(); - if (numLocations < 2) { - // Need at least two points for a polyline: - Log.w(Constants.TAG, "Not uploading too few points"); - return true; - } - return createNewLineString(splitTrack); - } - - /** - * Uploads all of the waypoints associated with this track to a table. - * - * @param track The track to upload waypoints for. - * - * @return True on success. - */ - private boolean uploadWaypoints(final Track track) { - // TODO: Stream through the waypoints in chunks. - // I am leaving the number of waypoints very high which should not be a - // problem because we don't try to load them into objects all at the - // same time. - boolean success = true; - Cursor c = null; - try { - c = providerUtils.getWaypointsCursor( - track.getId(), 0, - Constants.MAX_LOADED_WAYPOINTS_POINTS); - if (c != null) { - if (c.moveToFirst()) { - // This will skip the 1st waypoint (it carries the stats for the - // last segment). - while (c.moveToNext()) { - Waypoint wpt = providerUtils.createWaypoint(c); - Log.d(Constants.TAG, "SendToFusionTables: Creating waypoint."); - success = createNewPoint(wpt.getName(), wpt.getDescription(), wpt.getLocation(), - MARKER_TYPE_WAYPOINT); - if (!success) { - break; - } - } - } - } - if (!success) { - Log.w(Constants.TAG, "SendToFusionTables: upload waypoints failed."); - } - return success; - } finally { - if (c != null) { - c.close(); - } - } - } - - private void updateTrackDataUploadProgress() { - // The percent of the total that represents the completed part of this - // segment. We calculate it as an absolute percentage, and then scale it - // to fit the completion percentage range alloted to track data upload. - double totalPercentage = - (totalLocationsRead + totalLocationsPrepared + totalLocationsUploaded) - / (totalLocations * 3.0); - - double scaledPercentage = totalPercentage - * (PROGRESS_UPLOAD_DATA_MAX - PROGRESS_UPLOAD_DATA_MIN) + PROGRESS_UPLOAD_DATA_MIN; - - progressIndicator.setProgressValue((int) scaledPercentage); - } - - /** - * Runs an update query. Handles authentication. - * - * @param query The given SQL like query - * @return true in case of success - */ - private boolean runUpdate(final String query) { - GDataWrapper wrapper = new GDataWrapper(); - wrapper.setAuthManager(auth); - wrapper.setRetryOnAuthFailure(true); - wrapper.setClient(httpRequestFactory); - Log.d(Constants.TAG, "GData connection prepared: " + this.auth); - wrapper.runQuery(new QueryFunction() { - @Override - public void query(HttpRequestFactory factory) - throws IOException, GDataWrapper.ParseException, GDataWrapper.HttpException, - GDataWrapper.AuthenticationException { - GenericUrl url = new GenericUrl(FUSIONTABLES_BASE_FEED_URL); - - String sql = "sql=" + URLEncoder.encode(query, "UTF-8"); - ByteArrayInputStream inputStream = new ByteArrayInputStream(Strings.toBytesUtf8(sql)); - InputStreamContent isc = new InputStreamContent(null, inputStream ); - - HttpRequest request = factory.buildPostRequest(url, isc); - - GoogleHeaders headers = new GoogleHeaders(); - headers.setApplicationName("Google-MyTracks-" + SystemUtils.getMyTracksVersion(context)); - headers.gdataVersion = GDATA_VERSION; - headers.setGoogleLogin(auth.getAuthToken()); - headers.setContentType(CONTENT_TYPE); - request.setHeaders(headers); - - Log.d(Constants.TAG, "Running update query " + url.toString() + ": " + sql); - HttpResponse response; - try { - response = request.execute(); - } catch (HttpResponseException e) { - throw new GDataWrapper.HttpException(e.getResponse().getStatusCode(), - e.getResponse().getStatusMessage()); - } - boolean success = response.isSuccessStatusCode(); - if (success) { - byte[] result = new byte[1024]; - int read = response.getContent().read(result); - String s = new String(result, 0, read, "UTF8"); - String[] lines = s.split(Strings.LINE_SEPARATOR); - if (lines[0].equals("tableid")) { - tableId = lines[1]; - Log.d(Constants.TAG, "tableId = " + tableId); - } else { - Log.w(Constants.TAG, "Unrecognized response: " + lines[0]); - } - } else { - Log.d(Constants.TAG, - "Query failed: " + response.getStatusMessage() + " (" - + response.getStatusCode() + ")"); - throw new GDataWrapper.HttpException( - response.getStatusCode(), response.getStatusMessage()); - } - } - }); - return wrapper.getErrorType() == GDataWrapper.ERROR_NO_ERROR; - } -} diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/SendToMaps.java b/MyTracks/src/com/google/android/apps/mytracks/io/SendToMaps.java deleted file mode 100644 index 2ceb25ca5..000000000 --- a/MyTracks/src/com/google/android/apps/mytracks/io/SendToMaps.java +++ /dev/null @@ -1,458 +0,0 @@ -/* - * Copyright 2008 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ -package com.google.android.apps.mytracks.io; - -import static com.google.android.apps.mytracks.Constants.TAG; - -import com.google.android.apps.mytracks.Constants; -import com.google.android.apps.mytracks.ProgressIndicator; -import com.google.android.apps.mytracks.content.MyTracksProviderUtils; -import com.google.android.apps.mytracks.content.Track; -import com.google.android.apps.mytracks.content.Waypoint; -import com.google.android.apps.mytracks.io.maps.MapsFacade; -import com.google.android.apps.mytracks.io.sendtogoogle.SendType; -import com.google.android.apps.mytracks.stats.DoubleBuffer; -import com.google.android.apps.mytracks.stats.TripStatistics; -import com.google.android.apps.mytracks.util.LocationUtils; -import com.google.android.apps.mytracks.util.StringUtils; -import com.google.android.apps.mytracks.util.UnitConversions; -import com.google.android.maps.mytracks.R; - -import android.app.Activity; -import android.content.Context; -import android.content.SharedPreferences; -import android.database.Cursor; -import android.location.Location; -import android.util.Log; - -import java.util.ArrayList; -import java.util.List; -import java.util.Vector; - -/** - * A helper class used to transmit tracks to Google Maps. - * A new instance should be used for each upload. - * - * IMPORTANT: while this code is Apache-licensed, please notice that usage of - * the Google Maps servers through this API is only allowed for the My Tracks - * application. Other applications looking to upload maps data should look - * into using the Fusion Tables API. - * - * @author Leif Hendrik Wilden - */ -public class SendToMaps implements Runnable { - - public static final String NEW_MAP_ID = "new"; - private static final int MAX_POINTS_PER_UPLOAD = 2048; - - private final Activity context; - private final AuthManager auth; - private final long trackId; - private final ProgressIndicator progressIndicator; - private final OnSendCompletedListener onCompletion; - private final StringUtils stringUtils; - private final MyTracksProviderUtils providerUtils; - private String mapId; - - private MapsFacade mapsClient; - - // Progress status - private int totalLocationsRead; - private int totalLocationsPrepared; - private int totalLocationsUploaded; - private int totalLocations; - private int totalSegmentsUploaded; - - public interface OnSendCompletedListener { - void onSendCompleted(String mapId, boolean success); - } - - public SendToMaps(Activity context, String mapId, AuthManager auth, - long trackId, ProgressIndicator progressIndicator, - OnSendCompletedListener onCompletion) { - this.context = context; - this.mapId = mapId; - this.auth = auth; - this.trackId = trackId; - this.progressIndicator = progressIndicator; - this.onCompletion = onCompletion; - this.stringUtils = new StringUtils(context); - this.providerUtils = MyTracksProviderUtils.Factory.get(context); - } - - @Override - public void run() { - Log.d(TAG, "Sending to Maps: trackId = " + trackId); - doUpload(); - } - - private void doUpload() { - boolean success = true; - try { - progressIndicator.setProgressMessage( - context.getString(R.string.send_google_progress_reading_track)); - - // Get the track meta-data - Track track = providerUtils.getTrack(trackId); - if (track == null) { - Log.w(Constants.TAG, "Cannot get track."); - return; - } - - String originalDescription = track.getDescription(); - track.setDescription("

" + track.getDescription() + "

" - + stringUtils.generateTrackDescription(track, null, null) + "

"); - - mapsClient = new MapsFacade(context, auth); - - // Create a new map if necessary: - boolean isNewMap = mapId.equals(NEW_MAP_ID); - if (isNewMap) { - SharedPreferences preferences = context.getSharedPreferences( - Constants.SETTINGS_NAME, Context.MODE_PRIVATE); - boolean mapPublic = true; - if (preferences != null) { - mapPublic = preferences.getBoolean( - context.getString(R.string.default_map_public_key), true); - } - - String creatingFormat = context.getString(R.string.send_google_progress_creating); - String serviceName = context.getString(SendType.MAPS.getServiceName()); - progressIndicator.setProgressMessage(String.format(creatingFormat, serviceName)); - - StringBuilder mapIdBuilder = new StringBuilder(); - success = mapsClient.createNewMap( - track.getName(), track.getCategory(), originalDescription, mapPublic, mapIdBuilder); - mapId = mapIdBuilder.toString(); - } - - // Upload all of the segments of the track plus start/end markers - if (success) { - success = uploadAllTrackPoints(track, originalDescription); - } - - // Put waypoints. - if (success) { - Cursor c = providerUtils.getWaypointsCursor( - track.getId(), 0, - Constants.MAX_LOADED_WAYPOINTS_POINTS); - if (c != null) { - try { - if (c.getCount() > 1 && c.moveToFirst()) { - // This will skip the 1st waypoint (it carries the stats for the - // last segment). - ArrayList waypoints = new ArrayList(c.getCount() - 1); - while (c.moveToNext()) { - Waypoint wpt = providerUtils.createWaypoint(c); - waypoints.add(wpt); - } - - success = mapsClient.uploadWaypoints(mapId, waypoints); - } - } finally { - c.close(); - } - } else { - success = false; - } - - if (!success) { - Log.w(TAG, "SendToMaps: upload waypoints failed."); - } - } - - Log.d(TAG, "SendToMaps: Done: " + success); - progressIndicator.setProgressValue(100); - } finally { - if (mapsClient != null) { - mapsClient.cleanUp(); - } - - final boolean finalSuccess = success; - context.runOnUiThread(new Runnable() { - public void run() { - if (onCompletion != null) { - onCompletion.onSendCompleted(mapId, finalSuccess); - } - } - }); - } - } - - private boolean uploadAllTrackPoints( - final Track track, String originalDescription) { - SharedPreferences preferences = context.getSharedPreferences( - Constants.SETTINGS_NAME, Context.MODE_PRIVATE); - boolean metricUnits = true; - if (preferences != null) { - metricUnits = - preferences.getBoolean(context.getString(R.string.metric_units_key), - true); - } - - Cursor locationsCursor = - providerUtils.getLocationsCursor(track.getId(), 0, -1, false); - try { - if (locationsCursor == null || !locationsCursor.moveToFirst()) { - Log.w(TAG, "Unable to get any points to upload"); - return false; - } - - totalLocationsRead = 0; - totalLocationsPrepared = 0; - totalLocationsUploaded = 0; - totalLocations = locationsCursor.getCount(); - totalSegmentsUploaded = 0; - - // Limit the number of elevation readings. Ideally we would want around 250. - int elevationSamplingFrequency = - Math.max(1, (int) (totalLocations / 250.0)); - Log.d(TAG, - "Using elevation sampling factor: " + elevationSamplingFrequency - + " on " + totalLocations); - double totalDistance = 0; - - Vector distances = new Vector(); - Vector elevations = new Vector(); - DoubleBuffer elevationBuffer = - new DoubleBuffer(Constants.ELEVATION_SMOOTHING_FACTOR); - - List locations = new ArrayList(MAX_POINTS_PER_UPLOAD); - progressIndicator.setProgressMessage( - context.getString(R.string.send_google_progress_reading_track)); - Location lastLocation = null; - do { - if (totalLocationsRead % 100 == 0) { - updateProgress(); - } - - Location loc = providerUtils.createLocation(locationsCursor); - locations.add(loc); - - if (totalLocationsRead == 0) { - // Put a marker at the first point of the first valid segment: - mapsClient.uploadMarker(mapId, track.getName(), track.getDescription(), loc, true); - } - - // Add to the elevation profile. - if (loc != null && LocationUtils.isValidLocation(loc)) { - // All points go into the smoothing buffer... - elevationBuffer.setNext(metricUnits ? loc.getAltitude() - : loc.getAltitude() * UnitConversions.M_TO_FT); - if (lastLocation != null) { - double dist = lastLocation.distanceTo(loc); - totalDistance += dist; - } - - // ...but only a few points are really used to keep the url short. - if (totalLocationsRead % elevationSamplingFrequency == 0) { - distances.add(totalDistance); - elevations.add(elevationBuffer.getAverage()); - } - } - - // If the location was not valid, it's a segment split, so make sure the - // distance between the previous segment and the new one is not accounted - // for in the next iteration. - lastLocation = loc; - - // Every now and then, upload the accumulated points - if (totalLocationsRead % MAX_POINTS_PER_UPLOAD - == MAX_POINTS_PER_UPLOAD - 1) { - if (!prepareAndUploadPoints(track, locations)) { - return false; - } - } - - totalLocationsRead++; - } while (locationsCursor.moveToNext()); - - // Do a final upload with what's left - if (!prepareAndUploadPoints(track, locations)) { - return false; - } - - // Put an end marker at the last point of the last valid segment: - if (lastLocation != null) { - track.setDescription("

" + originalDescription + "

" - + stringUtils.generateTrackDescription( - track, distances, elevations) - + "

"); - return mapsClient.uploadMarker(mapId, track.getName(), track.getDescription(), - lastLocation, false); - } - - return true; - } finally { - if (locationsCursor != null) { - locationsCursor.close(); - } - } - } - - - private boolean prepareAndUploadPoints(Track track, List locations) { - progressIndicator.setProgressMessage( - context.getString(R.string.send_google_progress_preparing_track)); - updateProgress(); - - int numLocations = locations.size(); - if (numLocations < 2) { - Log.d(TAG, "Not preparing/uploading too few points"); - totalLocationsUploaded += numLocations; - return true; - } - - // Prepare/pre-process the points - ArrayList splitTracks = prepareLocations(track, locations); - - // Start uploading them - String sendingFormat = context.getString(R.string.send_google_progress_sending); - String serviceName = context.getString(SendType.MAPS.getServiceName()); - progressIndicator.setProgressMessage(String.format(sendingFormat, serviceName)); - for (Track splitTrack : splitTracks) { - if (totalSegmentsUploaded > 1) { - splitTrack.setName(splitTrack.getName() + " " - + String.format( - context.getString(R.string.send_google_track_part_label), totalSegmentsUploaded)); - } - totalSegmentsUploaded++; - Log.d(TAG, - "SendToMaps: Prepared feature for upload w/ " - + splitTrack.getLocations().size() + " points."); - - // Transmit tracks via GData feed: - // ------------------------------- - Log.d(TAG, - "SendToMaps: Uploading to map " + mapId + " w/ auth " + auth); - if (!mapsClient.uploadTrackPoints(mapId, splitTrack.getName(), splitTrack.getLocations())) { - Log.e(TAG, "Uploading failed"); - return false; - } - } - - locations.clear(); - totalLocationsUploaded += numLocations; - updateProgress(); - return true; - } - - /** - * Prepares a buffer of locations for transmission to google maps. - * - * @param track the original track with meta data - * @param locations a buffer of locations on the track - * @return an array of tracks each with a sub section of the points in the - * original buffer - */ - private ArrayList prepareLocations( - Track track, Iterable locations) { - ArrayList splitTracks = new ArrayList(); - - // Create segments from each full track: - Track segment = new Track(); - TripStatistics segmentStats = segment.getStatistics(); - TripStatistics trackStats = track.getStatistics(); - segment.setId(track.getId()); - segment.setName(track.getName()); - segment.setDescription(/* track.getDescription() */ ""); - segment.setCategory(track.getCategory()); - segmentStats.setStartTime(trackStats.getStartTime()); - segmentStats.setStopTime(trackStats.getStopTime()); - boolean startNewTrackSegment = false; - for (Location loc : locations) { - if (totalLocationsPrepared % 100 == 0) { - updateProgress(); - } - if (loc.getLatitude() > 90) { - startNewTrackSegment = true; - } - - if (startNewTrackSegment) { - // Close up the last segment. - prepareTrackSegment(segment, splitTracks); - - Log.d(TAG, - "MyTracksSendToMaps: Starting new track segment..."); - startNewTrackSegment = false; - segment = new Track(); - segment.setId(track.getId()); - segment.setName(track.getName()); - segment.setDescription(/* track.getDescription() */ ""); - segment.setCategory(track.getCategory()); - } - - if (loc.getLatitude() <= 90) { - segment.addLocation(loc); - if (segmentStats.getStartTime() < 0) { - segmentStats.setStartTime(loc.getTime()); - } - } - totalLocationsPrepared++; - } - - prepareTrackSegment(segment, splitTracks); - - return splitTracks; - } - - /** - * Prepares a track segment for sending to google maps. - * The main steps are: - * - correcting end time - * - decimating locations - * - splitting into smaller tracks. - * - * The final track pieces will be put in the array list splitTracks. - * - * @param segment the original segment of the track - * @param splitTracks an array of smaller track segments - */ - private void prepareTrackSegment( - Track segment, ArrayList splitTracks) { - TripStatistics segmentStats = segment.getStatistics(); - if (segmentStats.getStopTime() < 0 - && segment.getLocations().size() > 0) { - segmentStats.setStopTime(segment.getLocations().size() - 1); - } - - /* - * Decimate to 2 meter precision. Mapshop doesn't like too many - * points: - */ - LocationUtils.decimate(segment, 2.0); - - /* It the track still has > 500 points, split it in pieces: */ - if (segment.getLocations().size() > 500) { - splitTracks.addAll(LocationUtils.split(segment, 500)); - } else if (segment.getLocations().size() >= 2) { - splitTracks.add(segment); - } - } - - /** - * Sets the current upload progress. - */ - private void updateProgress() { - // The percent of the total that represents the completed part of this - // segment. - int totalPercentage = - (totalLocationsRead + totalLocationsPrepared + totalLocationsUploaded) * 100 - / (totalLocations * 3); - totalPercentage = Math.min(99, totalPercentage); - progressIndicator.setProgressValue(totalPercentage); - } -} diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/backup/PreferenceBackupHelper.java b/MyTracks/src/com/google/android/apps/mytracks/io/backup/PreferenceBackupHelper.java index 4a3cc2738..1b70dc1f5 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/backup/PreferenceBackupHelper.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/backup/PreferenceBackupHelper.java @@ -16,7 +16,7 @@ package com.google.android.apps.mytracks.io.backup; import com.google.android.apps.mytracks.content.ContentTypeIds; -import com.google.android.apps.mytracks.util.ApiFeatures; +import com.google.android.apps.mytracks.util.ApiAdapterFactory; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; @@ -106,7 +106,7 @@ class PreferenceBackupHelper { byte typeId = reader.readByte(); readAndSetPreference(name, typeId, reader, editor); } - ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges(editor); + ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor); } /** diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/docs/DocsHelper.java b/MyTracks/src/com/google/android/apps/mytracks/io/docs/DocsHelper.java deleted file mode 100644 index 02202fa76..000000000 --- a/MyTracks/src/com/google/android/apps/mytracks/io/docs/DocsHelper.java +++ /dev/null @@ -1,369 +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.io.docs; - -import com.google.android.apps.mytracks.Constants; -import com.google.android.apps.mytracks.content.Track; -import com.google.android.apps.mytracks.io.AuthManager; -import com.google.android.apps.mytracks.io.gdata.GDataWrapper; -import com.google.android.apps.mytracks.io.gdata.GDataWrapper.QueryFunction; -import com.google.android.apps.mytracks.stats.TripStatistics; -import com.google.android.apps.mytracks.util.ResourceUtils; -import com.google.android.apps.mytracks.util.StringUtils; -import com.google.android.maps.mytracks.R; -import com.google.wireless.gdata.client.GDataServiceClient; -import com.google.wireless.gdata.client.HttpException; -import com.google.wireless.gdata.data.Entry; -import com.google.wireless.gdata.docs.SpreadsheetsClient; -import com.google.wireless.gdata.docs.SpreadsheetsClient.WorksheetEntry; -import com.google.wireless.gdata.parser.GDataParser; -import com.google.wireless.gdata.parser.ParseException; -import com.google.wireless.gdata2.client.AuthenticationException; - -import android.content.Context; -import android.util.Log; - -import java.io.BufferedReader; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.io.OutputStreamWriter; -import java.net.URL; -import java.net.URLConnection; -import java.util.concurrent.atomic.AtomicReference; - -/** - * This class contains helper methods for interacting with Google Docs and - * Google Spreadsheets. - * - * @author Sandor Dornbush - * @author Matthew Simmons - */ -public class DocsHelper { - private static final String DOCS_FEED_URL = - "https://docs.google.com/feeds/documents/private/full"; - private static final String DOCS_SPREADSHEET_URL = - "https://docs.google.com/feeds/documents/private/full/spreadsheet%3A"; - private static final String DOCS_WORKSHEETS_URL_FORMAT = - "https://spreadsheets.google.com/feeds/worksheets/%s/private/full"; - private static final String DOCS_MY_SPREADSHEETS_FEED_URL = - "https://docs.google.com/feeds/documents/private/full?category=mine,spreadsheet"; - private static final String DOCS_SPREADSHEET_URL_FORMAT = - "https://spreadsheets.google.com/feeds/list/%s/%s/private/full"; - - private static final String CONTENT_TYPE_PARAM = "Content-Type"; - private static final String OPENDOCUMENT_SPREADSHEET_MIME_TYPE = - "application/x-vnd.oasis.opendocument.spreadsheet"; - private static final String ATOM_FEED_MIME_TYPE = "application/atom+xml"; - - /** - * Creates a new MyTracks spreadsheet with the given name. - * - * @param context The context associated with this request. - * @param docListWrapper The GData handle for the Document List service. - * @param name The name for the newly-created spreadsheet. - * @return The spreadsheet ID, if one is created. {@code null} will be - * returned if a GData error didn't occur, but no spreadsheet ID was - * returned. - */ - public String createSpreadsheet(final Context context, - final GDataWrapper docListWrapper, final String name) throws IOException { - final AtomicReference idSaver = new AtomicReference(); - - boolean success = docListWrapper.runQuery(new QueryFunction() { - @Override - public void query(GDataServiceClient client) throws IOException, - GDataWrapper.AuthenticationException { - // Construct and send request - URL url = new URL(DOCS_FEED_URL); - URLConnection conn = url.openConnection(); - conn.addRequestProperty(CONTENT_TYPE_PARAM, - OPENDOCUMENT_SPREADSHEET_MIME_TYPE); - conn.addRequestProperty("Slug", name); - conn.addRequestProperty("Authorization", - "GoogleLogin auth=" + - docListWrapper.getAuthManager().getAuthToken()); - conn.setDoOutput(true); - OutputStream os = conn.getOutputStream(); - ResourceUtils.readBinaryFileToOutputStream( - context, R.raw.mytracks_empty_spreadsheet, os); - - // Get the response - // TODO: The following is a horrible ugly hack. - // Hopefully we can retire it when there is a proper gdata api. - BufferedReader rd = null; - String line; - StringBuilder resultBuilder = new StringBuilder(); - try { - rd = new BufferedReader(new InputStreamReader(conn.getInputStream())); - while ((line = rd.readLine()) != null) { - resultBuilder.append(line); - } - os.close(); - rd.close(); - } catch (FileNotFoundException e) { - // The GData API sometimes throws an error, even though creation of - // the document succeeded. In that case let's just return. The caller - // then needs to check if the doc actually exists. - return; - } finally { - os.close(); - if (rd != null) { - rd.close(); - } - } - - String result = resultBuilder.toString(); - // Try to find the id. - int idTagIndex = result.indexOf(""); - if (idTagIndex == -1) { - return; - } - int idTagCloseIndex = result.indexOf("", idTagIndex); - if (idTagCloseIndex == -1) { - return; - } - int idStringStart = result.indexOf(DOCS_SPREADSHEET_URL, idTagIndex); - if (idStringStart == -1) { - return; - } - - String id = result.substring( - idStringStart + DOCS_SPREADSHEET_URL.length(), idTagCloseIndex); - Log.i(Constants.TAG, "Created new spreadsheet: " + id); - idSaver.set(id); - }}); - - if (!success) { - throw newIOException(docListWrapper, - "Failed to create new spreadsheet."); - } - - return idSaver.get(); - } - - /** - * Retrieve the ID of a spreadsheet with the given name. - * - * @param docListWrapper The GData handle for the Document List service. - * @param title The name of the spreadsheet whose ID is to be retrieved. - * @return The spreadsheet ID, if it can be retrieved. {@code null} will - * be returned if no spreadsheet exists by the given name. - * @throws IOException If an error occurs during the GData request. - */ - public String requestSpreadsheetId(final GDataWrapper docListWrapper, - final String title) throws IOException { - final AtomicReference idSaver = new AtomicReference(); - - boolean result = docListWrapper.runQuery(new QueryFunction() { - @Override - public void query(GDataServiceClient client) - throws IOException, GDataWrapper.ParseException, GDataWrapper.HttpException, - GDataWrapper.AuthenticationException { - GDataParser listParser; - try { - listParser = client.getParserForFeed(Entry.class, - DOCS_MY_SPREADSHEETS_FEED_URL, - docListWrapper.getAuthManager().getAuthToken()); - listParser.init(); - - while (listParser.hasMoreData()) { - Entry entry = listParser.readNextEntry(null); - String entryTitle = entry.getTitle(); - Log.i(Constants.TAG, "Found docs entry: " + entryTitle); - if (entryTitle.equals(title)) { - String entryId = entry.getId(); - int lastSlash = entryId.lastIndexOf('/'); - idSaver.set(entryId.substring(lastSlash + 15)); - break; - } - } - } catch (ParseException e) { - throw new GDataWrapper.ParseException(e); - } catch (HttpException e) { - throw new GDataWrapper.HttpException(e.getStatusCode(), e.getMessage()); - } - } - }); - - if (!result) { - throw newIOException(docListWrapper, - "Failed to retrieve spreadsheet list."); - } - - return idSaver.get(); - } - - /** - * Retrieve the ID of the first worksheet in the named spreadsheet. - * - * @param trixWrapper The GData handle for the spreadsheet service. - * @param spreadsheetId The GData ID for the given spreadsheet. - * @return The worksheet ID, if it can be retrieved. {@code null} will be - * returned if the GData request returns without error, but without an - * ID. - * @throws IOException If an error occurs during the GData request. - */ - public String getWorksheetId(final GDataWrapper trixWrapper, - final String spreadsheetId) throws IOException { - final AtomicReference idSaver = new AtomicReference(); - - boolean result = trixWrapper.runQuery(new QueryFunction() { - @Override - public void query(GDataServiceClient client) - throws GDataWrapper.AuthenticationException, IOException, GDataWrapper.ParseException, GDataWrapper.HttpException { - String uri = String.format(DOCS_WORKSHEETS_URL_FORMAT, spreadsheetId); - GDataParser sheetParser; - try { - sheetParser = ((SpreadsheetsClient) client).getParserForWorksheetsFeed(uri, - trixWrapper.getAuthManager().getAuthToken()); - sheetParser.init(); - if (!sheetParser.hasMoreData()) { - Log.i(Constants.TAG, "Found no worksheets"); - return; - } - - // Grab the first worksheet. - WorksheetEntry worksheetEntry = - (WorksheetEntry) sheetParser.readNextEntry(new WorksheetEntry()); - - int lastSlash = worksheetEntry.getId().lastIndexOf('/'); - idSaver.set(worksheetEntry.getId().substring(lastSlash + 1)); - } catch (ParseException e) { - throw new GDataWrapper.ParseException(e); - } catch (AuthenticationException e) { - throw new GDataWrapper.AuthenticationException(e); - } - } - }); - - if (!result) { - throw newIOException(trixWrapper, "Failed to retrieve worksheet ID."); - } - - return idSaver.get(); - } - - /** - * Add a row to a worksheet containing the stats for a given track. - * - * @param context The context associated with this request. - * @param trixAuth The GData authorization for the spreadsheet service. - * @param spreadsheetId The spreadsheet to be modified. - * @param worksheetId The worksheet to be modified. - * @param track The track whose stats are to be written. - * @param metricUnits True if metric units are to be used. If false, - * imperial units will be used. - * @throws IOException If an error occurs while updating the worksheet. - */ - public void addTrackRow(Context context, AuthManager trixAuth, - String spreadsheetId, String worksheetId, Track track, - boolean metricUnits) throws IOException { - String worksheetUri = String.format(DOCS_SPREADSHEET_URL_FORMAT, - spreadsheetId, worksheetId); - TripStatistics stats = track.getStatistics(); - - String distanceUnit = context.getString(metricUnits ? - R.string.unit_kilometer : R.string.unit_mile); - String speedUnit = context.getString(metricUnits ? - R.string.unit_kilometer_per_hour : R.string.unit_mile_per_hour); - String elevationUnit = context.getString(metricUnits ? - R.string.unit_meter : R.string.unit_feet); - - // Prepare the Post-Text we are going to send. - DocsTagBuilder tagBuilder = new DocsTagBuilder(metricUnits) - .append("name", track.getName()) - .append("description", track.getDescription()) - .append("date", StringUtils.formatDateTime(context, stats.getStartTime())) - .append("totaltime", StringUtils.formatElapsedTime(stats.getTotalTime())) - .append("movingtime", StringUtils.formatElapsedTime(stats.getMovingTime())) - .appendLargeUnits("distance", stats.getTotalDistance() / 1000) - .append("distanceunit", distanceUnit) - .appendLargeUnits("averagespeed", stats.getAverageSpeed() * 3.6) - .appendLargeUnits("averagemovingspeed", - stats.getAverageMovingSpeed() * 3.6) - .appendLargeUnits("maxspeed", stats.getMaxSpeed() * 3.6) - .append("speedunit", speedUnit) - .appendSmallUnits("elevationgain", stats.getTotalElevationGain()) - .appendSmallUnits("minelevation", stats.getMinElevation()) - .appendSmallUnits("maxelevation", stats.getMaxElevation()) - .append("elevationunit", elevationUnit); - - if (track.getMapId().length() > 0) { - tagBuilder.append("map", String.format("%s?msa=0&msid=%s", - Constants.MAPSHOP_BASE_URL, track.getMapId())); - } - - String postText = new StringBuilder() - .append("") - .append(tagBuilder.build()) - .append("") - .toString(); - - Log.i(Constants.TAG, - "Inserting at: " + spreadsheetId + " => " + worksheetUri); - - Log.i(Constants.TAG, postText); - - writeRowData(trixAuth, worksheetUri, postText); - - Log.i(Constants.TAG, "Post finished."); - } - - /** - * Writes spreadsheet row data to the indicated worksheet. - * - * @param trixAuth The GData authorization for the spreadsheet service. - * @param worksheetUri The URI of the worksheet to be altered. - * @param postText The XML tags describing the change to be made. - * @throws IOException Thrown if an error occurs during the write. - */ - protected void writeRowData(AuthManager trixAuth, String worksheetUri, - String postText) throws IOException { - // No need for a wrapper because we know that the authorization was good - // enough to get this far. - URL url = new URL(worksheetUri); - URLConnection conn = url.openConnection(); - conn.addRequestProperty(CONTENT_TYPE_PARAM, ATOM_FEED_MIME_TYPE); - conn.addRequestProperty("Authorization", - "GoogleLogin auth=" + trixAuth.getAuthToken()); - conn.setDoOutput(true); - OutputStreamWriter wr = new OutputStreamWriter(conn.getOutputStream()); - wr.write(postText); - wr.flush(); - - // Get the response. - // TODO: Should we parse the response, rather than simply throwing it away? - BufferedReader rd = - new BufferedReader(new InputStreamReader(conn.getInputStream())); - String line; - while ((line = rd.readLine()) != null) { - // Process line. - Log.i(Constants.TAG, "r: " + line); - } - wr.close(); - rd.close(); - } - - private static IOException newIOException(GDataWrapper wrapper, - String message) { - return new IOException(String.format("%s: %d: %s", message, - wrapper.getErrorType(), wrapper.getErrorMessage())); - } -} diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/docs/DocsTagBuilder.java b/MyTracks/src/com/google/android/apps/mytracks/io/docs/DocsTagBuilder.java deleted file mode 100644 index 17833c28c..000000000 --- a/MyTracks/src/com/google/android/apps/mytracks/io/docs/DocsTagBuilder.java +++ /dev/null @@ -1,118 +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.io.docs; - -import com.google.android.apps.mytracks.util.StringUtils; -import com.google.android.apps.mytracks.util.UnitConversions; - -import java.text.NumberFormat; -import java.util.Locale; - -/** - *

This class builds a string of XML tags used to talk to Docs using GData. - * - *

Sample Usage: - * - * - * String tags = new DocsTagBuilder(false) - * .append("tagName", "tagValue") - * .appendLargeUnits("bigTagName", 1.0) - * .build(); - * - * - *

results in: - * - * - * - * - * - * - * @author Matthew Simmons - */ -class DocsTagBuilder { - - // Google Docs can only parse numbers in the English locale. - private static final NumberFormat LARGE_UNIT_FORMAT = NumberFormat.getNumberInstance( - Locale.ENGLISH); - private static final NumberFormat SMALL_UNIT_FORMAT = NumberFormat.getIntegerInstance( - Locale.ENGLISH); - - static { - LARGE_UNIT_FORMAT.setMaximumFractionDigits(2); - LARGE_UNIT_FORMAT.setMinimumFractionDigits(2); - } - - protected final boolean metricUnits; - protected final StringBuilder stringBuilder; - - /** - * @param metricUnits True if metric units are to be used. If false, - * imperial units will be used. - */ - DocsTagBuilder(boolean metricUnits) { - this.metricUnits = metricUnits; - this.stringBuilder = new StringBuilder(); - } - - /** Appends a tag containing a string value */ - DocsTagBuilder append(String name, String value) { - appendTag(name, value); - return this; - } - - /** - * Appends a tag containing a numeric value. The value will be formatted - * according to the large distance of the specified measurement system (i.e. - * kilometers or miles). - * - * @param name The tag name. - * @param d The value to be formatted, in kilometers. - */ - DocsTagBuilder appendLargeUnits(String name, double d) { - double value = metricUnits ? d : (d * UnitConversions.KM_TO_MI); - appendTag(name, LARGE_UNIT_FORMAT.format(value)); - return this; - } - - /** - * Appends a tag containing a numeric value. The value will be formatted - * according to the small distance of the specified measurement system (i.e. - * meters or feet). - * - * @param name The tag name. - * @param d The value to be formatted, in meters. - */ - DocsTagBuilder appendSmallUnits(String name, double d) { - double value = metricUnits ? d : (d * UnitConversions.M_TO_FT); - appendTag(name, SMALL_UNIT_FORMAT.format(value)); - return this; - } - - /** Returns a string containing all tags which have been added. */ - String build() { - return stringBuilder.toString(); - } - - private void appendTag(String name, String value) { - stringBuilder.append("") - .append(StringUtils.stringAsCData(value)) - .append(""); - } -} diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/docs/SendDocsActivity.java b/MyTracks/src/com/google/android/apps/mytracks/io/docs/SendDocsActivity.java new file mode 100644 index 000000000..61467a6f2 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/io/docs/SendDocsActivity.java @@ -0,0 +1,125 @@ +/* + * Copyright 2012 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.docs; + +import com.google.android.apps.mytracks.io.sendtogoogle.SendRequest; +import com.google.android.apps.mytracks.io.sendtogoogle.UploadResultActivity; +import com.google.android.maps.mytracks.R; + +import android.app.Activity; +import android.app.Dialog; +import android.app.ProgressDialog; +import android.content.DialogInterface; +import android.content.Intent; +import android.os.Bundle; + +/** + * An activity to send a track to Google Docs. + * + * @author jshih@google.com (Jimmy Shih) + */ +public class SendDocsActivity extends Activity { + + private static final int PROGRESS_DIALOG = 1; + + private SendRequest sendRequest; + private SendDocsAsyncTask asyncTask; + private ProgressDialog progressDialog; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + sendRequest = getIntent().getParcelableExtra(SendRequest.SEND_REQUEST_KEY); + + Object retained = getLastNonConfigurationInstance(); + if (retained instanceof SendDocsAsyncTask) { + asyncTask = (SendDocsAsyncTask) retained; + asyncTask.setActivity(this); + } else { + asyncTask = new SendDocsAsyncTask(this, sendRequest.getTrackId(), sendRequest.getAccount()); + asyncTask.execute(); + } + } + + @Override + public Object onRetainNonConfigurationInstance() { + asyncTask.setActivity(null); + return asyncTask; + } + + @Override + protected Dialog onCreateDialog(int id) { + switch (id) { + case PROGRESS_DIALOG: + progressDialog = new ProgressDialog(this); + progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); + progressDialog.setIcon(android.R.drawable.ic_dialog_info); + progressDialog.setTitle(getString( + R.string.send_google_progress_title, getString(R.string.send_google_docs))); + progressDialog.setMax(100); + progressDialog.setProgress(0); + progressDialog.setCancelable(true); + progressDialog.setOnCancelListener(new DialogInterface.OnCancelListener() { + @Override + public void onCancel(DialogInterface dialog) { + asyncTask.cancel(true); + startNextActivity(false); + } + }); + return progressDialog; + default: + return null; + } + } + + /** + * Invokes when the associated AsyncTask completes. + * + * @param success true if success + */ + public void onAsyncTaskCompleted(boolean success) { + startNextActivity(success); + } + + /** + * Shows the progress dialog. + */ + public void showProgressDialog() { + showDialog(PROGRESS_DIALOG); + } + + /** + * Sets the progress dialog value. + */ + public void setProgressDialogValue(int value) { + if (progressDialog != null) { + progressDialog.setProgress(value); + } + } + + /** + * Starts the next activity. + * + * @param success true if sendDocs is success + */ + private void startNextActivity(boolean success) { + sendRequest.setDocsSuccess(success); + Intent intent = new Intent(this, UploadResultActivity.class) + .putExtra(SendRequest.SEND_REQUEST_KEY, sendRequest); + startActivity(intent); + finish(); + } +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/docs/SendDocsAsyncTask.java b/MyTracks/src/com/google/android/apps/mytracks/io/docs/SendDocsAsyncTask.java new file mode 100644 index 000000000..7b4466d01 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/io/docs/SendDocsAsyncTask.java @@ -0,0 +1,389 @@ +/* + * Copyright 2012 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.docs; + +import com.google.android.apps.mytracks.content.MyTracksProviderUtils; +import com.google.android.apps.mytracks.content.Track; +import com.google.android.apps.mytracks.io.gdata.GDataClientFactory; +import com.google.android.apps.mytracks.io.gdata.docs.DocumentsClient; +import com.google.android.apps.mytracks.io.gdata.docs.SpreadsheetsClient; +import com.google.android.apps.mytracks.io.gdata.docs.XmlDocsGDataParserFactory; +import com.google.android.common.gdata.AndroidXmlParserFactory; +import com.google.android.maps.mytracks.R; +import com.google.wireless.gdata.client.GDataClient; +import com.google.wireless.gdata.client.HttpException; +import com.google.wireless.gdata.parser.ParseException; +import com.google.wireless.gdata2.client.AuthenticationException; + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.accounts.AuthenticatorException; +import android.accounts.OperationCanceledException; +import android.content.Context; +import android.os.AsyncTask; +import android.util.Log; + +import java.io.IOException; + +/** + * AsyncTask to send a track to Google Docs. + * + * @author jshih@google.com (Jimmy Shih) + */ +public class SendDocsAsyncTask extends AsyncTask { + private static final int PROGRESS_GET_SPREADSHEET_ID = 0; + private static final int PROGRESS_CREATE_SPREADSHEET = 25; + private static final int PROGRESS_GET_WORKSHEET_ID = 50; + private static final int PROGRESS_ADD_TRACK_INFO = 75; + private static final int PROGRESS_COMPLETE = 100; + + private static final String TAG = SendDocsAsyncTask.class.getSimpleName(); + + private SendDocsActivity activity; + private final long trackId; + private final Account account; + private final Context context; + private final MyTracksProviderUtils myTracksProviderUtils; + private final GDataClient gDataClient; + private final DocumentsClient documentsClient; + private final SpreadsheetsClient spreadsheetsClient; + + /** + * True if can retry sending to Google Docs. + */ + private boolean canRetry; + + /** + * True if the AsyncTask has completed. + */ + private boolean completed; + + /** + * True if the result is success. + */ + private boolean success; + + // The following variables are for per upload states + private String documentsAuthToken; + private String spreadsheetsAuthToken; + private String spreadsheetId; + private String worksheetId; + + public SendDocsAsyncTask(SendDocsActivity activity, long trackId, Account account) { + this.activity = activity; + this.trackId = trackId; + this.account = account; + + context = activity.getApplicationContext(); + myTracksProviderUtils = MyTracksProviderUtils.Factory.get(context); + gDataClient = GDataClientFactory.getGDataClient(context); + documentsClient = new DocumentsClient( + gDataClient, new XmlDocsGDataParserFactory(new AndroidXmlParserFactory())); + spreadsheetsClient = new SpreadsheetsClient( + gDataClient, new XmlDocsGDataParserFactory(new AndroidXmlParserFactory())); + + canRetry = true; + completed = false; + success = false; + } + + /** + * Sets the activity associated with this AyncTask. + * + * @param activity the activity. + */ + public void setActivity(SendDocsActivity activity) { + this.activity = activity; + if (completed && activity != null) { + activity.onAsyncTaskCompleted(success); + } + } + + @Override + protected void onPreExecute() { + activity.showProgressDialog(); + } + + @Override + protected Boolean doInBackground(Void... params) { + return doUpload(); + } + + @Override + protected void onProgressUpdate(Integer... values) { + if (activity != null) { + activity.setProgressDialogValue(values[0]); + } + } + + @Override + protected void onPostExecute(Boolean result) { + closeClient(); + success = result; + completed = true; + if (activity != null) { + activity.onAsyncTaskCompleted(success); + } + } + + @Override + protected void onCancelled() { + closeClient(); + } + + private void closeClient() { + if (gDataClient != null) { + gDataClient.close(); + } + } + + /** + * Uploads a track to Google Docs. + * + * @return true if success. + */ + private boolean doUpload() { + // Reset the per upload states + documentsAuthToken = null; + spreadsheetsAuthToken = null; + spreadsheetId = null; + worksheetId = null; + + try { + documentsAuthToken = AccountManager.get(context).blockingGetAuthToken( + account, documentsClient.getServiceName(), false); + spreadsheetsAuthToken = AccountManager.get(context).blockingGetAuthToken( + account, spreadsheetsClient.getServiceName(), false); + } catch (OperationCanceledException e) { + Log.d(TAG, "Unable to get auth token", e); + return retryUpload(); + } catch (AuthenticatorException e) { + Log.d(TAG, "Unable to get auth token", e); + return retryUpload(); + } catch (IOException e) { + Log.d(TAG, "Unable to get auth token", e); + return retryUpload(); + } + + Track track = myTracksProviderUtils.getTrack(trackId); + if (track == null) { + Log.d(TAG, "Track is null"); + return false; + } + + String title = context.getString(R.string.my_tracks_app_name); + if (track.getCategory() != null && !track.getCategory().equals("")) { + title += "-" + track.getCategory(); + } + + // Get the spreadsheet ID + publishProgress(PROGRESS_GET_SPREADSHEET_ID); + if (!fetchSpreadSheetId(title, false)) { + return retryUpload(); + } + + // Create a new spreadsheet if necessary + publishProgress(PROGRESS_CREATE_SPREADSHEET); + if (spreadsheetId == null) { + if (!createSpreadSheet(title)) { + Log.d(TAG, "Unable to create a new spreadsheet"); + return false; + } + + // The previous creation might have succeeded even though GData + // reported an error. Seems to be a know bug. + // See http://code.google.com/p/gdata-issues/issues/detail?id=929 + // Try to find the created spreadsheet. + if (spreadsheetId == null) { + if (!fetchSpreadSheetId(title, true)) { + Log.d(TAG, "Unable to check if the new spreadsheet is created"); + return false; + } + + if (spreadsheetId == null) { + Log.d(TAG, "Unable to create a new spreadsheet"); + return false; + } + } + } + + // Get the worksheet ID + publishProgress(PROGRESS_GET_WORKSHEET_ID); + if (!fetchWorksheetId()) { + return retryUpload(); + } + if (worksheetId == null) { + Log.d(TAG, "Unable to get a worksheet ID"); + return false; + } + + // Add the track info + publishProgress(PROGRESS_ADD_TRACK_INFO); + if (!addTrackInfo(track)) { + Log.d(TAG, "Unable to add track info"); + return false; + } + + publishProgress(PROGRESS_COMPLETE); + return true; + } + + /** + * Fetches the spreadsheet id. Sets the instance variable + * {@link SendDocsAsyncTask#spreadsheetId}. + * + * @param title the spreadsheet title + * @param waitFirst wait before checking + * @return true if completes. + */ + private boolean fetchSpreadSheetId(String title, boolean waitFirst) { + if (isCancelled()) { + return false; + } + + if (waitFirst) { + try { + Thread.sleep(5000); + } catch (InterruptedException e) { + Log.d(TAG, "Unable to wait", e); + return false; + } + } + + try { + spreadsheetId = SendDocsUtils.getSpreadsheetId(title, documentsClient, documentsAuthToken); + } catch (ParseException e) { + Log.d(TAG, "Unable to fetch spreadsheet ID", e); + return false; + } catch (HttpException e) { + Log.d(TAG, "Unable to fetch spreadsheet ID", e); + return false; + } catch (IOException e) { + Log.d(TAG, "Unable to fetch spreadsheet ID", e); + return false; + } + + if (spreadsheetId == null) { + // Waiting a few seconds and trying again. Maybe the server just had a + // hickup (unfortunately that happens quite a lot...). + try { + Thread.sleep(5000); + } catch (InterruptedException e) { + Log.d(TAG, "Unable to wait", e); + return false; + } + + try { + spreadsheetId = SendDocsUtils.getSpreadsheetId(title, documentsClient, documentsAuthToken); + } catch (ParseException e) { + Log.d(TAG, "Unable to fetch spreadsheet ID", e); + return false; + } catch (HttpException e) { + Log.d(TAG, "Unable to fetch spreadsheet ID", e); + return false; + } catch (IOException e) { + Log.d(TAG, "Unable to fetch spreadsheet ID", e); + return false; + } + } + return true; + } + + /** + * Creates a spreadsheet. If successful, sets the instance variable + * {@link SendDocsAsyncTask#spreadsheetId}. + * + * @param spreadsheetTitle the spreadsheet title + * @return true if completes. + */ + private boolean createSpreadSheet(String spreadsheetTitle) { + if (isCancelled()) { + return false; + } + try { + spreadsheetId = SendDocsUtils.createSpreadsheet(spreadsheetTitle, documentsAuthToken, context); + } catch (IOException e) { + Log.d(TAG, "Unable to create spreadsheet", e); + return false; + } + return true; + } + + /** + * Fetches the worksheet ID. Sets the instance variable + * {@link SendDocsAsyncTask#worksheetId}. + * + * @return true if completes. + */ + private boolean fetchWorksheetId() { + if (isCancelled()) { + return false; + } + try { + worksheetId = SendDocsUtils.getWorksheetId( + spreadsheetId, spreadsheetsClient, spreadsheetsAuthToken); + } catch (IOException e) { + Log.d(TAG, "Unable to fetch worksheet ID", e); + return false; + } catch (AuthenticationException e) { + Log.d(TAG, "Unable to fetch worksheet ID", e); + return false; + } catch (ParseException e) { + Log.d(TAG, "Unable to fetch worksheet ID", e); + return false; + } + return true; + } + + /** + * Adds track info to a worksheet. + * + * @param track the track + * @return true if completes. + */ + private boolean addTrackInfo(Track track) { + if (isCancelled()) { + return false; + } + try { + SendDocsUtils.addTrackInfo(track, spreadsheetId, worksheetId, spreadsheetsAuthToken, context); + } catch (IOException e) { + Log.d(TAG, "Unable to add track info", e); + return false; + } + return true; + } + + /** + * Retries upload. Invalidates the authToken. If can retry, invokes + * {@link SendDocsAsyncTask#doUpload()}. Returns false if cannot retry. + */ + private boolean retryUpload() { + if (isCancelled()) { + return false; + } + + AccountManager.get(context).invalidateAuthToken( + documentsClient.getServiceName(), documentsAuthToken); + AccountManager.get(context).invalidateAuthToken( + spreadsheetsClient.getServiceName(), spreadsheetsAuthToken); + if (canRetry) { + canRetry = false; + return doUpload(); + } + return false; + } +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/docs/SendDocsUtils.java b/MyTracks/src/com/google/android/apps/mytracks/io/docs/SendDocsUtils.java new file mode 100644 index 000000000..adee07e89 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/io/docs/SendDocsUtils.java @@ -0,0 +1,410 @@ +/* + * Copyright 2012 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.docs; + +import com.google.android.apps.mytracks.Constants; +import com.google.android.apps.mytracks.content.Track; +import com.google.android.apps.mytracks.io.gdata.docs.DocumentsClient; +import com.google.android.apps.mytracks.io.gdata.docs.SpreadsheetsClient; +import com.google.android.apps.mytracks.io.gdata.docs.SpreadsheetsClient.WorksheetEntry; +import com.google.android.apps.mytracks.stats.TripStatistics; +import com.google.android.apps.mytracks.util.ResourceUtils; +import com.google.android.apps.mytracks.util.StringUtils; +import com.google.android.apps.mytracks.util.UnitConversions; +import com.google.android.maps.mytracks.R; +import com.google.common.annotations.VisibleForTesting; +import com.google.wireless.gdata.client.HttpException; +import com.google.wireless.gdata.data.Entry; +import com.google.wireless.gdata.parser.GDataParser; +import com.google.wireless.gdata.parser.ParseException; +import com.google.wireless.gdata2.client.AuthenticationException; + +import android.content.Context; +import android.content.SharedPreferences; +import android.util.Log; + +import java.io.BufferedReader; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLEncoder; +import java.text.NumberFormat; +import java.util.Locale; + +/** + * Utilities for sending a track to Google Docs. + * + * @author Sandor Dornbush + * @author Matthew Simmons + */ +public class SendDocsUtils { + private static final String GET_SPREADSHEET_BY_TITLE_URI = + "https://docs.google.com/feeds/documents/private/full?" + + "category=mine,spreadsheet&title=%s&title-exact=true"; + private static final String CREATE_SPREADSHEET_URI = + "https://docs.google.com/feeds/documents/private/full"; + private static final String GET_WORKSHEETS_URI = + "https://spreadsheets.google.com/feeds/worksheets/%s/private/full"; + private static final String GET_WORKSHEET_URI = + "https://spreadsheets.google.com/feeds/list/%s/%s/private/full"; + + private static final String SPREADSHEET_ID_PREFIX = + "https://docs.google.com/feeds/documents/private/full/spreadsheet%3A"; + + private static final String CONTENT_TYPE = "Content-Type"; + private static final String ATOM_FEED_MIME_TYPE = "application/atom+xml"; + private static final String OPENDOCUMENT_SPREADSHEET_MIME_TYPE = + "application/x-vnd.oasis.opendocument.spreadsheet"; + + private static final String AUTHORIZATION = "Authorization"; + private static final String AUTHORIZATION_PREFIX = "GoogleLogin auth="; + + private static final String SLUG = "Slug"; + + // Google Docs can only parse numbers in the English locale. + private static final NumberFormat NUMBER_FORMAT = NumberFormat.getNumberInstance(Locale.ENGLISH); + private static final NumberFormat INTEGER_FORMAT = NumberFormat.getIntegerInstance( + Locale.ENGLISH); + + static { + NUMBER_FORMAT.setMaximumFractionDigits(2); + NUMBER_FORMAT.setMinimumFractionDigits(2); + } + + private static final String TAG = SendDocsUtils.class.getSimpleName(); + + private SendDocsUtils() {} + + /** + * Gets the spreadsheet id of a spreadsheet. Returns null if the spreadsheet + * doesn't exist. + * + * @param title the title of the spreadsheet + * @param documentsClient the documents client + * @param authToken the auth token + * @return spreadsheet id or null if it doesn't exist. + */ + public static String getSpreadsheetId( + String title, DocumentsClient documentsClient, String authToken) + throws IOException, ParseException, HttpException { + GDataParser gDataParser = null; + try { + String uri = String.format(GET_SPREADSHEET_BY_TITLE_URI, URLEncoder.encode(title)); + gDataParser = documentsClient.getParserForFeed(Entry.class, uri, authToken); + gDataParser.init(); + + while (gDataParser.hasMoreData()) { + Entry entry = gDataParser.readNextEntry(null); + String entryTitle = entry.getTitle(); + if (entryTitle.equals(title)) { + return getEntryId(entry); + } + } + return null; + } finally { + if (gDataParser != null) { + gDataParser.close(); + } + } + } + + /** + * Gets the id from an entry. Returns null if not available. + * + * @param entry the entry + */ + @VisibleForTesting + static String getEntryId(Entry entry) { + String entryId = entry.getId(); + if (entryId.startsWith(SPREADSHEET_ID_PREFIX)) { + return entryId.substring(SPREADSHEET_ID_PREFIX.length()); + } + return null; + } + + /** + * Creates a new spreadsheet with the given title. Returns the spreadsheet ID + * if successful. Returns null otherwise. Note that it is possible that a new + * spreadsheet is created, but the returned ID is null. + * + * @param title the title + * @param authToken the auth token + * @param context the context + */ + public static String createSpreadsheet(String title, String authToken, Context context) + throws IOException { + URL url = new URL(CREATE_SPREADSHEET_URI); + URLConnection conn = url.openConnection(); + conn.addRequestProperty(CONTENT_TYPE, OPENDOCUMENT_SPREADSHEET_MIME_TYPE); + conn.addRequestProperty(SLUG, title); + conn.addRequestProperty(AUTHORIZATION, AUTHORIZATION_PREFIX + authToken); + conn.setDoOutput(true); + OutputStream outputStream = conn.getOutputStream(); + ResourceUtils.readBinaryFileToOutputStream( + context, R.raw.mytracks_empty_spreadsheet, outputStream); + + // Get the response + BufferedReader bufferedReader = null; + StringBuilder resultBuilder = new StringBuilder(); + try { + bufferedReader = new BufferedReader(new InputStreamReader(conn.getInputStream())); + String line; + while ((line = bufferedReader.readLine()) != null) { + resultBuilder.append(line); + } + } catch (FileNotFoundException e) { + // The GData API sometimes throws an error, even though creation of + // the document succeeded. In that case let's just return. The caller + // then needs to check if the doc actually exists. + Log.d(TAG, "Unable to read result after creating a spreadsheet", e); + return null; + } finally { + outputStream.close(); + if (bufferedReader != null) { + bufferedReader.close(); + } + } + return getNewSpreadsheetId(resultBuilder.toString()); + } + + /** + * Gets the spreadsheet id from a create spreadsheet result. + * + * @param result the create spreadsheet result + */ + @VisibleForTesting + static String getNewSpreadsheetId(String result) { + int idTagIndex = result.indexOf(""); + if (idTagIndex == -1) { + return null; + } + + int idTagCloseIndex = result.indexOf("", idTagIndex); + if (idTagCloseIndex == -1) { + return null; + } + + int idStringStart = result.indexOf(SPREADSHEET_ID_PREFIX, idTagIndex); + if (idStringStart == -1) { + return null; + } + + return result.substring(idStringStart + SPREADSHEET_ID_PREFIX.length(), idTagCloseIndex); + } + + /** + * Gets the first worksheet ID of a spreadsheet. Returns null if not + * available. + * + * @param spreadsheetId the spreadsheet ID + * @param spreadsheetClient the spreadsheet client + * @param authToken the auth token + */ + public static String getWorksheetId( + String spreadsheetId, SpreadsheetsClient spreadsheetClient, String authToken) + throws IOException, AuthenticationException, ParseException { + GDataParser gDataParser = null; + try { + String uri = String.format(GET_WORKSHEETS_URI, spreadsheetId); + gDataParser = spreadsheetClient.getParserForWorksheetsFeed(uri, authToken); + gDataParser.init(); + if (!gDataParser.hasMoreData()) { + Log.d(TAG, "No worksheet"); + return null; + } + + // Get the first worksheet + WorksheetEntry worksheetEntry = + (WorksheetEntry) gDataParser.readNextEntry(new WorksheetEntry()); + return getWorksheetEntryId(worksheetEntry); + } finally { + if (gDataParser != null) { + gDataParser.close(); + } + } + } + + /** + * Gets the worksheet id from a worksheet entry. Returns null if not available. + * + * @param entry the worksheet entry + */ + @VisibleForTesting + static String getWorksheetEntryId(WorksheetEntry entry) { + String id = entry.getId(); + int lastSlash = id.lastIndexOf('/'); + if (lastSlash == -1) { + Log.d(TAG, "No id"); + return null; + } + return id.substring(lastSlash + 1); + } + + /** + * Adds a track's info as a row in a worksheet. + * + * @param track the track + * @param spreadsheetId the spreadsheet ID + * @param worksheetId the worksheet ID + * @param authToken the auth token + * @param context the context + */ + public static void addTrackInfo( + Track track, String spreadsheetId, String worksheetId, String authToken, Context context) + throws IOException { + String worksheetUri = String.format(GET_WORKSHEET_URI, spreadsheetId, worksheetId); + SharedPreferences prefs = context.getSharedPreferences( + Constants.SETTINGS_NAME, Context.MODE_PRIVATE); + boolean metricUnits = prefs.getBoolean(context.getString(R.string.metric_units_key), true); + + addRow(worksheetUri, getRowContent(track, metricUnits, context), authToken); + } + + /** + * Gets the row content containing the track's info. + * + * @param track the track + * @param metricUnits true to use metric + * @param context the context + */ + @VisibleForTesting + static String getRowContent(Track track, boolean metricUnits, Context context) { + TripStatistics stats = track.getStatistics(); + + String distanceUnit = context.getString( + metricUnits ? R.string.unit_kilometer : R.string.unit_mile); + String speedUnit = context.getString( + metricUnits ? R.string.unit_kilometer_per_hour : R.string.unit_mile_per_hour); + String elevationUnit = context.getString( + metricUnits ? R.string.unit_meter : R.string.unit_feet); + + StringBuilder builder = new StringBuilder().append(""); + appendTag(builder, "name", track.getName()); + appendTag(builder, "description", track.getDescription()); + appendTag(builder, "date", StringUtils.formatDateTime(context, stats.getStartTime())); + appendTag(builder, "totaltime", StringUtils.formatElapsedTime(stats.getTotalTime())); + appendTag(builder, "movingtime", StringUtils.formatElapsedTime(stats.getMovingTime())); + appendTag(builder, "distance", getDistance(stats.getTotalDistance(), metricUnits)); + appendTag(builder, "distanceunit", distanceUnit); + appendTag(builder, "averagespeed", getSpeed(stats.getAverageSpeed(), metricUnits)); + appendTag(builder, "averagemovingspeed", getSpeed(stats.getAverageMovingSpeed(), metricUnits)); + appendTag(builder, "maxspeed", getSpeed(stats.getMaxSpeed(), metricUnits)); + appendTag(builder, "speedunit", speedUnit); + appendTag(builder, "elevationgain", getElevation(stats.getTotalElevationGain(), metricUnits)); + appendTag(builder, "minelevation", getElevation(stats.getMinElevation(), metricUnits)); + appendTag(builder, "maxelevation", getElevation(stats.getMaxElevation(), metricUnits)); + appendTag(builder, "elevationunit", elevationUnit); + + if (track.getMapId().length() > 0) { + appendTag(builder, "map", + String.format("%s?msa=0&msid=%s", Constants.MAPSHOP_BASE_URL, track.getMapId())); + } + + builder.append(""); + return builder.toString(); + } + + /** + * Appends a name-value pair as a gsx tag to a string builder. + * + * @param stringBuilder the string builder + * @param name the name + * @param value the value + */ + @VisibleForTesting + static void appendTag(StringBuilder stringBuilder, String name, String value) { + stringBuilder + .append("") + .append(StringUtils.stringAsCData(value)) + .append(""); + } + + /** + * Gets the distance. Performs unit conversion and formatting. + * + * @param distanceInMeter the distance in meters + * @param metricUnits true to use metric + */ + @VisibleForTesting + static final String getDistance(double distanceInMeter, boolean metricUnits) { + double distanceInKilometer = distanceInMeter / 1000.0; + double distance = metricUnits ? distanceInKilometer + : distanceInKilometer * UnitConversions.KM_TO_MI; + return NUMBER_FORMAT.format(distance); + } + + /** + * Gets the speed. Performs unit conversion and formatting. + * + * @param speedInMeterPerSecond the speed in meters per second + * @param metricUnits true to use metric + */ + @VisibleForTesting + static final String getSpeed(double speedInMeterPerSecond, boolean metricUnits) { + double speedInKilometerPerHour = speedInMeterPerSecond * 3.6; + double speed = metricUnits ? speedInKilometerPerHour + : speedInKilometerPerHour * UnitConversions.KMH_TO_MPH; + return NUMBER_FORMAT.format(speed); + } + + /** + * Gets the elevation. Performs unit conversion and formatting. + * + * @param elevationInMeter the elevation value in meters + * @param metricUnits true to use metric + */ + @VisibleForTesting + static final String getElevation(double elevationInMeter, boolean metricUnits) { + double elevation = metricUnits ? elevationInMeter : elevationInMeter * UnitConversions.M_TO_FT; + return INTEGER_FORMAT.format(elevation); + } + + /** + * Adds a row to a Google Spreadsheet worksheet. + * + * @param worksheetUri the worksheet URI + * @param rowContent the row content + * @param authToken the auth token + */ + private static final void addRow(String worksheetUri, String rowContent, String authToken) + throws IOException { + URL url = new URL(worksheetUri); + URLConnection conn = url.openConnection(); + conn.addRequestProperty(CONTENT_TYPE, ATOM_FEED_MIME_TYPE); + conn.addRequestProperty(AUTHORIZATION, AUTHORIZATION_PREFIX + authToken); + conn.setDoOutput(true); + OutputStreamWriter writer = new OutputStreamWriter(conn.getOutputStream()); + writer.write(rowContent); + writer.flush(); + + // Get the response + BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream())); + while ((reader.readLine()) != null) { + // Just read till the end + } + writer.close(); + reader.close(); + } +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/fusiontables/SendFusionTablesActivity.java b/MyTracks/src/com/google/android/apps/mytracks/io/fusiontables/SendFusionTablesActivity.java new file mode 100644 index 000000000..287f8e955 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/io/fusiontables/SendFusionTablesActivity.java @@ -0,0 +1,149 @@ +/* + * Copyright 2012 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.fusiontables; + +import com.google.android.apps.mytracks.io.docs.SendDocsActivity; +import com.google.android.apps.mytracks.io.sendtogoogle.SendRequest; +import com.google.android.apps.mytracks.io.sendtogoogle.UploadResultActivity; +import com.google.android.maps.mytracks.R; + +import android.app.Activity; +import android.app.Dialog; +import android.app.ProgressDialog; +import android.content.DialogInterface; +import android.content.Intent; +import android.os.Bundle; + +/** + * An activity to send a track to Google Fusion Tables. + *

+ * The activity gets recreated when the screen rotates. To support the activity + * displaying a progress dialog, we do the following: + *

    + *
  • use one instance of an AyncTask to send the track
  • + *
  • save that instance as the last non configuration instance of the activity + *
  • + *
  • when a new activity is created, pass the activity to the AsyncTask so + * that the AsyncTask can update the progress dialog of the activity
  • + *
+ * + * @author jshih@google.com (Jimmy Shih) + */ +public class SendFusionTablesActivity extends Activity { + + private static final int PROGRESS_DIALOG = 1; + + private SendRequest sendRequest; + private SendFusionTablesAsyncTask asyncTask; + private ProgressDialog progressDialog; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + sendRequest = getIntent().getParcelableExtra(SendRequest.SEND_REQUEST_KEY); + + Object retained = getLastNonConfigurationInstance(); + if (retained instanceof SendFusionTablesAsyncTask) { + asyncTask = (SendFusionTablesAsyncTask) retained; + asyncTask.setActivity(this); + } else { + asyncTask = new SendFusionTablesAsyncTask( + this, sendRequest.getTrackId(), sendRequest.getAccount()); + asyncTask.execute(); + } + } + + @Override + public Object onRetainNonConfigurationInstance() { + asyncTask.setActivity(null); + return asyncTask; + } + + @Override + protected Dialog onCreateDialog(int id) { + switch (id) { + case PROGRESS_DIALOG: + progressDialog = new ProgressDialog(this); + progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); + progressDialog.setIcon(android.R.drawable.ic_dialog_info); + progressDialog.setTitle(getString( + R.string.send_google_progress_title, getString(R.string.send_google_fusion_tables))); + progressDialog.setMax(100); + progressDialog.setProgress(0); + progressDialog.setCancelable(true); + progressDialog.setOnCancelListener(new DialogInterface.OnCancelListener() { + @Override + public void onCancel(DialogInterface dialog) { + asyncTask.cancel(true); + startNextActivity(false, true); + } + }); + return progressDialog; + default: + return null; + } + } + + /** + * Invokes when the associated AsyncTask completes. + * + * @param success true if success + */ + public void onAsyncTaskCompleted(boolean success) { + startNextActivity(success, false); + } + + /** + * Shows the progress dialog. + */ + public void showProgressDialog() { + showDialog(PROGRESS_DIALOG); + } + + /** + * Sets the progress dialog value. + */ + public void setProgressDialogValue(int value) { + if (progressDialog != null) { + progressDialog.setProgress(value); + } + } + + /** + * Starts the next activity. + * + * @param success true if sendFusionTables is success + * @param isCancel true if it is a cancel request + */ + private void startNextActivity(boolean success, boolean isCancel) { + sendRequest.setFusionTablesSuccess(success); + + Class next; + if (isCancel) { + next = UploadResultActivity.class; + } else { + if (sendRequest.isSendDocs()) { + next = SendDocsActivity.class; + } else { + next = UploadResultActivity.class; + } + } + Intent intent = new Intent(this, next) + .putExtra(SendRequest.SEND_REQUEST_KEY, sendRequest); + startActivity(intent); + finish(); + } +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/fusiontables/SendFusionTablesAsyncTask.java b/MyTracks/src/com/google/android/apps/mytracks/io/fusiontables/SendFusionTablesAsyncTask.java new file mode 100644 index 000000000..6d5e99f7b --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/io/fusiontables/SendFusionTablesAsyncTask.java @@ -0,0 +1,531 @@ +// Copyright 2012 Google Inc. All Rights Reserved. + +package com.google.android.apps.mytracks.io.fusiontables; + +import com.google.android.apps.mytracks.Constants; +import com.google.android.apps.mytracks.content.MyTracksProviderUtils; +import com.google.android.apps.mytracks.content.Track; +import com.google.android.apps.mytracks.content.Waypoint; +import com.google.android.apps.mytracks.io.sendtogoogle.SendToGoogleUtils; +import com.google.android.apps.mytracks.stats.DoubleBuffer; +import com.google.android.apps.mytracks.util.ApiAdapterFactory; +import com.google.android.apps.mytracks.util.LocationUtils; +import com.google.android.apps.mytracks.util.StringUtils; +import com.google.android.apps.mytracks.util.SystemUtils; +import com.google.android.apps.mytracks.util.UnitConversions; +import com.google.android.maps.mytracks.R; +import com.google.api.client.googleapis.GoogleHeaders; +import com.google.api.client.googleapis.MethodOverride; +import com.google.api.client.http.GenericUrl; +import com.google.api.client.http.HttpRequest; +import com.google.api.client.http.HttpRequestFactory; +import com.google.api.client.http.HttpResponse; +import com.google.api.client.http.HttpTransport; +import com.google.api.client.http.InputStreamContent; +import com.google.api.client.util.Strings; + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.accounts.AuthenticatorException; +import android.accounts.OperationCanceledException; +import android.content.Context; +import android.content.SharedPreferences; +import android.database.Cursor; +import android.location.Location; +import android.os.AsyncTask; +import android.util.Log; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.List; +import java.util.Vector; + +/** + * AsyncTask to send a track to Google Fusion Tables. + * + * @author jshih@google.com (Jimmy Shih) + */ +public class SendFusionTablesAsyncTask extends AsyncTask { + + private static final String APP_NAME_PREFIX = "Google-MyTracks-"; + private static final String SQL_KEY = "sql="; + private static final String CONTENT_TYPE = "application/x-www-form-urlencoded"; + private static final String SERVICE_ID = "fusiontables"; + private static final String FUSION_TABLES_BASE_URL = + "https://www.google.com/fusiontables/api/query"; + private static final int MAX_POINTS_PER_UPLOAD = 2048; + private static final String GDATA_VERSION = "2"; + + private static final int PROGRESS_CREATE_TABLE = 0; + private static final int PROGRESS_UNLIST_TABLE = 5; + private static final int PROGRESS_UPLOAD_DATA_MIN = 10; + private static final int PROGRESS_UPLOAD_DATA_MAX = 90; + private static final int PROGRESS_UPLOAD_WAYPOINTS = 95; + private static final int PROGRESS_COMPLETE = 100; + + // See http://support.google.com/fusiontables/bin/answer.py?hl=en&answer=185991 + private static final String MARKER_TYPE_START = "large_green"; + private static final String MARKER_TYPE_END = "large_red"; + private static final String MARKER_TYPE_WAYPOINT = "large_yellow"; + + private static final String TAG = SendFusionTablesAsyncTask.class.getSimpleName(); + + private SendFusionTablesActivity activity; + + private final Context context; + private final long trackId; + private final Account account; + private final MyTracksProviderUtils myTracksProviderUtils; + private final HttpRequestFactory httpRequestFactory; + + /** + * True if can retry sending to Google Fusion Tables. + */ + private boolean canRetry; + + /** + * True if the AsyncTask has completed. + */ + private boolean completed; + + /** + * True if the result is success. + */ + private boolean success; + + // The following variables are for per upload states + private String authToken; + private String tableId; + int currentSegment; + + public SendFusionTablesAsyncTask( + SendFusionTablesActivity activity, long trackId, Account account) { + this.activity = activity; + this.trackId = trackId; + this.account = account; + + context = activity.getApplicationContext(); + myTracksProviderUtils = MyTracksProviderUtils.Factory.get(context); + HttpTransport transport = ApiAdapterFactory.getApiAdapter().getHttpTransport(); + httpRequestFactory = transport.createRequestFactory(new MethodOverride()); + + canRetry = true; + completed = false; + success = false; + } + + /** + * Sets the activity associated with this AyncTask. + * + * @param activity the activity. + */ + public void setActivity(SendFusionTablesActivity activity) { + this.activity = activity; + if (completed && activity != null) { + activity.onAsyncTaskCompleted(success); + } + } + + @Override + protected void onPreExecute() { + activity.showProgressDialog(); + } + + @Override + protected Boolean doInBackground(Void... params) { + return doUpload(); + } + + @Override + protected void onProgressUpdate(Integer... values) { + if (activity != null) { + activity.setProgressDialogValue(values[0]); + } + } + + @Override + protected void onPostExecute(Boolean result) { + success = result; + completed = true; + if (success) { + Track track = myTracksProviderUtils.getTrack(trackId); + if (track != null) { + track.setTableId(tableId); + myTracksProviderUtils.updateTrack(track); + } else { + Log.d(TAG, "No track"); + } + } + if (activity != null) { + activity.onAsyncTaskCompleted(success); + } + } + + /** + * Uploads a track to Google Fusion Tables. + * + * @return true if success. + */ + private boolean doUpload() { + // Reset the per upload states + authToken = null; + tableId = null; + currentSegment = 1; + + try { + authToken = AccountManager.get(context).blockingGetAuthToken(account, SERVICE_ID, false); + } catch (OperationCanceledException e) { + Log.d(TAG, "Unable to get auth token", e); + return retryUpload(); + } catch (AuthenticatorException e) { + Log.d(TAG, "Unable to get auth token", e); + return retryUpload(); + } catch (IOException e) { + Log.d(TAG, "Unable to get auth token", e); + return retryUpload(); + } + + Track track = myTracksProviderUtils.getTrack(trackId); + if (track == null) { + Log.d(TAG, "Track is null"); + return false; + } + + // Create a new table + publishProgress(PROGRESS_CREATE_TABLE); + if (!createNewTable(track)) { + // Retry upload in case the auth token is invalid + return retryUpload(); + } + + // Unlist table + publishProgress(PROGRESS_UNLIST_TABLE); + if (!unlistTable()) { + return false; + } + + // Upload all the track points plus the start and end markers + publishProgress(PROGRESS_UPLOAD_DATA_MIN); + if (!uploadAllTrackPoints(track)) { + return false; + } + + // Upload all the waypoints + publishProgress(PROGRESS_UPLOAD_WAYPOINTS); + if (!uploadWaypoints()) { + return false; + } + + publishProgress(PROGRESS_COMPLETE); + return true; + } + + /** + * Retries upload. Invalidates the authToken. If can retry, invokes + * {@link SendFusionTablesAsyncTask#doUpload()}. Returns false if cannot + * retry. + */ + private boolean retryUpload() { + if (isCancelled()) { + return false; + } + + AccountManager.get(context).invalidateAuthToken(SERVICE_ID, authToken); + if (canRetry) { + canRetry = false; + return doUpload(); + } + return false; + } + + /** + * Creates a new table. + * + * @param track the track + * @return true if success. + */ + private boolean createNewTable(Track track) { + String query = "CREATE TABLE '" + SendFusionTablesUtils.escapeSqlString(track.getName()) + + "' (name:STRING,description:STRING,geometry:LOCATION,marker:STRING)"; + return sendQuery(query, true); + } + + /** + * Unlists a table. + * + * @return true if success. + */ + private boolean unlistTable() { + String query = "UPDATE TABLE " + tableId + " SET VISIBILITY = UNLISTED"; + return sendQuery(query, false); + } + + /** + * Uploads all the points in a track. + * + * @param track the track + * @return true if success. + */ + private boolean uploadAllTrackPoints(Track track) { + Cursor locationsCursor = null; + try { + SharedPreferences prefs = context.getSharedPreferences( + Constants.SETTINGS_NAME, Context.MODE_PRIVATE); + boolean metricUnits = prefs.getBoolean(context.getString(R.string.metric_units_key), true); + + locationsCursor = myTracksProviderUtils.getLocationsCursor(trackId, 0, -1, false); + if (locationsCursor == null) { + Log.d(TAG, "Location cursor is null"); + return false; + } + int locationsCount = locationsCursor.getCount(); + List locations = new ArrayList(MAX_POINTS_PER_UPLOAD); + Location lastLocation = null; + + // Limit the number of elevation readings. Ideally we would want around + // 250. + int elevationSamplingFrequency = Math.max(1, (int) (locationsCount / 250.0)); + double totalDistance = 0; + DoubleBuffer elevationBuffer = new DoubleBuffer(Constants.ELEVATION_SMOOTHING_FACTOR); + Vector distances = new Vector(); + Vector elevations = new Vector(); + + for (int i = 0; i < locationsCount; i++) { + locationsCursor.moveToPosition(i); + + Location location = myTracksProviderUtils.createLocation(locationsCursor); + locations.add(location); + + int readCount = i + 1; + + if (readCount == 1) { + // Create a start marker + String name = context.getString(R.string.marker_label_start, track.getName()); + if (!createNewPoint(name, "", location, MARKER_TYPE_START)) { + Log.d(TAG, "Unable to create the start marker"); + return false; + } + } + + // Add to the distances and elevations vectors + if (LocationUtils.isValidLocation(location)) { + // All points go into the smoothing buffer + elevationBuffer.setNext(metricUnits ? location.getAltitude() + : location.getAltitude() * UnitConversions.M_TO_FT); + if (lastLocation != null) { + totalDistance += (double) lastLocation.distanceTo(location); + } + if (readCount % elevationSamplingFrequency == 0) { + distances.add(totalDistance); + elevations.add(elevationBuffer.getAverage()); + } + } + + // Upload periodically + if (readCount % MAX_POINTS_PER_UPLOAD == 0) { + if (!prepareAndUploadPoints(track, locations, false)) { + Log.d(TAG, "Unable to upload points"); + return false; + } + updateProgress(readCount, locationsCount); + locations.clear(); + } + lastLocation = location; + } + + // Do a final upload with the remaining locations + if (!prepareAndUploadPoints(track, locations, true)) { + Log.d(TAG, "Unable to upload points"); + return false; + } + + // Create an end marker + if (lastLocation != null) { + StringUtils stringUtils = new StringUtils(context); + track.setDescription("

" + track.getDescription() + "

" + + stringUtils.generateTrackDescription(track, distances, elevations) + "

"); + String name = context.getString(R.string.marker_label_end, track.getName()); + if (!createNewPoint(name, track.getDescription(), lastLocation, MARKER_TYPE_END)) { + Log.d(TAG, "Unable to create the end marker"); + return false; + } + } + + return true; + } finally { + if (locationsCursor != null) { + locationsCursor.close(); + } + } + } + + /** + * Prepares and uploads a list of locations from a track. + * + * @param track the track + * @param locations the locations from the track + * @param lastBatch true if it is the last batch of locations + */ + private boolean prepareAndUploadPoints(Track track, List locations, boolean lastBatch) { + // Prepare locations + ArrayList splitTracks = SendToGoogleUtils.prepareLocations(track, locations); + + // Upload segments + boolean onlyOneSegment = lastBatch && currentSegment == 1 && splitTracks.size() == 1; + for (Track splitTrack : splitTracks) { + if (!onlyOneSegment) { + splitTrack.setName(context.getString( + R.string.send_google_track_part_label, splitTrack.getName(), currentSegment)); + } + if (!createNewLineString(splitTrack)) { + Log.d(TAG, "Upload points failed"); + return false; + } + currentSegment++; + } + return true; + } + + /** + * Uploads all the waypoints. + * + * @return true if success. + */ + private boolean uploadWaypoints() { + Cursor cursor = null; + try { + cursor = myTracksProviderUtils.getWaypointsCursor( + trackId, 0, Constants.MAX_LOADED_WAYPOINTS_POINTS); + if (cursor != null && cursor.moveToFirst()) { + // This will skip the first waypoint (it carries the stats for the + // track). + while (cursor.moveToNext()) { + Waypoint wpt = myTracksProviderUtils.createWaypoint(cursor); + if (!createNewPoint( + wpt.getName(), wpt.getDescription(), wpt.getLocation(), MARKER_TYPE_WAYPOINT)) { + Log.d(TAG, "Upload waypoints failed"); + return false; + } + } + } + return true; + } finally { + if (cursor != null) { + cursor.close(); + } + } + } + + /** + * Creates a new row in Google Fusion Tables representing a marker as a + * point. + * + * @param name the marker name + * @param description the marker description + * @param location the marker location + * @param type the marker type + * @return true if success. + */ + private boolean createNewPoint( + String name, String description, Location location, String type) { + String query = "INSERT INTO " + tableId + " (name,description,geometry,marker) VALUES " + + SendFusionTablesUtils.formatSqlValues( + name, description, SendFusionTablesUtils.getKmlPoint(location), type); + return sendQuery(query, false); + } + + /** + * Creates a new row in Google Fusion Tables representing the track as a + * line segment. + * + * @param track the track + * @return true if success. + */ + private boolean createNewLineString(Track track) { + String query = "INSERT INTO " + tableId + " (name,description,geometry) VALUES " + + SendFusionTablesUtils.formatSqlValues(track.getName(), track.getDescription(), + SendFusionTablesUtils.getKmlLineString(track.getLocations())); + return sendQuery(query, false); + } + + /** + * Sends a query to Google Fusion Tables. + * + * @param query the Fusion Tables SQL query + * @param setTableId true to set the table id + * @return true if success. + */ + private boolean sendQuery(String query, boolean setTableId) { + Log.d(TAG, "SendQuery: " + query); + + if (isCancelled()) { + return false; + } + + GenericUrl url = new GenericUrl(FUSION_TABLES_BASE_URL); + String sql = SQL_KEY + URLEncoder.encode(query); + ByteArrayInputStream inputStream = new ByteArrayInputStream(Strings.toBytesUtf8(sql)); + InputStreamContent inputStreamContent = new InputStreamContent(null, inputStream); + HttpRequest request; + try { + request = httpRequestFactory.buildPostRequest(url, inputStreamContent); + } catch (IOException e) { + Log.d(TAG, "Unable to build request", e); + return false; + } + + GoogleHeaders headers = new GoogleHeaders(); + headers.setApplicationName(APP_NAME_PREFIX + SystemUtils.getMyTracksVersion(context)); + headers.gdataVersion = GDATA_VERSION; + headers.setGoogleLogin(authToken); + headers.setContentType(CONTENT_TYPE); + request.setHeaders(headers); + + HttpResponse response; + try { + response = request.execute(); + } catch (IOException e) { + Log.d(TAG, "Unable to execute request", e); + return false; + } + boolean isSuccess = response.isSuccessStatusCode(); + if (isSuccess) { + InputStream content; + try { + content = response.getContent(); + } catch (IOException e) { + Log.d(TAG, "Unable to get response", e); + return false; + } + if (setTableId) { + tableId = SendFusionTablesUtils.getTableId(content); + if (tableId == null) { + Log.d(TAG, "tableId is null"); + return false; + } + } + } else { + Log.d(TAG, + "sendQuery failed: " + response.getStatusMessage() + ": " + response.getStatusCode()); + return false; + } + return true; + } + + /** + * Updates the progress based on the number of locations uploaded. + * + * @param uploaded the number of uploaded locations + * @param total the number of total locations + */ + private void updateProgress(int uploaded, int total) { + double totalPercentage = uploaded / total; + double scaledPercentage = totalPercentage + * (PROGRESS_UPLOAD_DATA_MAX - PROGRESS_UPLOAD_DATA_MIN) + PROGRESS_UPLOAD_DATA_MIN; + publishProgress((int) scaledPercentage); + } +} + diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/fusiontables/SendFusionTablesUtils.java b/MyTracks/src/com/google/android/apps/mytracks/io/fusiontables/SendFusionTablesUtils.java new file mode 100644 index 000000000..752efcfe2 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/io/fusiontables/SendFusionTablesUtils.java @@ -0,0 +1,195 @@ +/* + * Copyright 2012 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.fusiontables; + +import com.google.android.apps.mytracks.content.Track; +import com.google.android.apps.mytracks.stats.TripStatistics; +import com.google.api.client.util.Strings; +import com.google.common.annotations.VisibleForTesting; + +import android.location.Location; +import android.util.Log; + +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.Locale; + +/** + * Utilities for sending a track to Google Fusion Tables. + * + * @author jshih@google.com (Jimmy Shih) + */ +public class SendFusionTablesUtils { + + private static final String UTF8 = "UTF8"; + private static final String TABLE_ID = "tableid"; + private static final String MAP_URL = "https://www.google.com/fusiontables/embedviz?" + + "viz=MAP&q=select+col0,+col1,+col2,+col3+from+%s+&h=false&lat=%f&lng=%f&z=%d&t=1&l=col2"; + private static final String TAG = SendFusionTablesUtils.class.getSimpleName(); + + private SendFusionTablesUtils() {} + + /** + * Gets the url to visualize a fusion table on a map. + * + * @param track the track + * @return the url. + */ + public static String getMapUrl(Track track) { + if (track == null || track.getStatistics() == null || track.getTableId() == null) { + Log.e(TAG, "Invalid track"); + return null; + } + + // TODO(jshih): Determine the correct bounding box and zoom level that + // will show the entire track. + TripStatistics stats = track.getStatistics(); + double latE6 = stats.getBottom() + (stats.getTop() - stats.getBottom()) / 2; + double lonE6 = stats.getLeft() + (stats.getRight() - stats.getLeft()) / 2; + int z = 15; + + // We explicitly format with Locale.US because we need the latitude and + // longitude to be formatted in a locale-independent manner. Specifically, + // we need the decimal separator to be a period rather than a comma. + return String.format( + Locale.US, MAP_URL, track.getTableId(), latE6 / 1.E6, lonE6 / 1.E6, z); + } + + /** + * Formats an array of values as a SQL VALUES like + * ('value1','value2',...,'value_n'). Escapes single quotes with two single + * quotes. + * + * @param values an array of values to format + * @return the formated SQL VALUES. + */ + public static String formatSqlValues(String... values) { + StringBuilder builder = new StringBuilder("("); + for (int i = 0; i < values.length; i++) { + if (i > 0) { + builder.append(','); + } + builder.append('\''); + builder.append(escapeSqlString(values[i])); + builder.append('\''); + } + builder.append(")"); + return builder.toString(); + } + + /** + * Escapes a SQL string. Escapes single quotes with two single quotes. + * + * @param string the string + * @return the escaped string. + */ + public static String escapeSqlString(String string) { + return string.replaceAll("'", "''"); + } + + /** + * Gets a KML Point value representing a location. + * + * @param location the location + * @return the KML Point value. + */ + public static String getKmlPoint(Location location) { + StringBuilder builder = new StringBuilder(""); + if (location != null) { + appendLocation(location, builder); + } + builder.append(""); + return builder.toString(); + } + + /** + * Gets a KML LineString value representing an array of locations. + * + * @param locations the locations. + * @return the KML LineString value. + */ + public static String getKmlLineString(ArrayList locations) { + StringBuilder builder = new StringBuilder(""); + if (locations != null) { + for (int i = 0; i < locations.size(); i++) { + if (i != 0) { + builder.append(' '); + } + appendLocation(locations.get(i), builder); + } + } + builder.append(""); + return builder.toString(); + } + + /** + * Appends a location to a string builder using "longitude,latitude[,altitude]" format. + * + * @param location the location + * @param builder the string builder + */ + @VisibleForTesting + static void appendLocation(Location location, StringBuilder builder) { + builder.append(location.getLongitude()).append(",").append(location.getLatitude()); + if (location.hasAltitude()) { + builder.append(","); + builder.append(location.getAltitude()); + } + } + + /** + * Gets the table id from an input streawm. + * + * @param inputStream input stream + * @return table id or null if not available. + */ + public static String getTableId(InputStream inputStream) { + if (inputStream == null) { + Log.d(TAG, "inputStream is null"); + return null; + } + byte[] result = new byte[1024]; + int read; + try { + read = inputStream.read(result); + } catch (IOException e) { + Log.d(TAG, "Unable to read result", e); + return null; + } + if (read == -1) { + Log.d(TAG, "no data read"); + return null; + } + String s; + try { + s = new String(result, 0, read, UTF8); + } catch (UnsupportedEncodingException e) { + Log.d(TAG, "Unable to parse result", e); + return null; + } + + String[] lines = s.split(Strings.LINE_SEPARATOR); + if (lines.length > 1 && lines[0].equals(TABLE_ID)) { + // returns the next line + return lines[1]; + } else { + Log.d(TAG, "Response is not valid: " + s); + return null; + } + } +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/gdata/GDataClientFactory.java b/MyTracks/src/com/google/android/apps/mytracks/io/gdata/GDataClientFactory.java index a0a717bb1..6f23261d5 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/gdata/GDataClientFactory.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/gdata/GDataClientFactory.java @@ -36,6 +36,7 @@ public class GDataClientFactory { * @return A GDataClient appropriate for this platform */ public static GDataClient getGDataClient(Context context) { + // TODO This should be moved into ApiAdapter try { // Try to use the official unbundled gdata client implementation. // This should work on Froyo and beyond. diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/gdata/GDataWrapper.java b/MyTracks/src/com/google/android/apps/mytracks/io/gdata/GDataWrapper.java deleted file mode 100644 index 6b3859965..000000000 --- a/MyTracks/src/com/google/android/apps/mytracks/io/gdata/GDataWrapper.java +++ /dev/null @@ -1,309 +0,0 @@ -/* - * Copyright 2008 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ -package com.google.android.apps.mytracks.io.gdata; - -import com.google.android.apps.mytracks.Constants; -import com.google.android.apps.mytracks.io.AuthManager; -import com.google.android.apps.mytracks.io.AuthManager.AuthCallback; - -import android.util.Log; - -import java.io.FileNotFoundException; -import java.io.IOException; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.FutureTask; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.concurrent.atomic.AtomicBoolean; - - -/** - * GDataWrapper provides a wrapper around GData operations that maintains the - * GData client, and provides a method to run GData queries with proper error - * handling. After a query is run, the wrapper can be queried about the error - * that occurred. - * - * @param the GData service client - * @author Sandor Dornbush - */ -public class GDataWrapper { - - public static class AuthenticationException extends Exception { - private Exception exception; - private static final long serialVersionUID = 1L; - public AuthenticationException(Exception caught) { - this.exception = caught; - } - public Exception getException() { - return exception; - } - }; - - public static class ParseException extends Exception { - private Exception exception; - private static final long serialVersionUID = 1L; - public ParseException(Exception caught) { - this.exception = caught; - } - public Exception getException() { - return exception; - } - }; - - public static class ConflictDetectedException extends Exception { - private Exception exception; - private static final long serialVersionUID = 1L; - public ConflictDetectedException(Exception caught) { - this.exception = caught; - } - public Exception getException() { - return exception; - } - }; - - public static class HttpException extends Exception { - private static final long serialVersionUID = 1L; - private int statusCode; - private String statusMessage; - public HttpException(int statusCode, String statusMessage) { - super(); - this.statusCode = statusCode; - this.statusMessage = statusMessage; - } - public int getStatusCode() { - return statusCode; - } - public String getStatusMessage() { - return statusMessage; - } - }; - - /** - * A QueryFunction is passed in when executing a query. The query function of - * the class is called with the GData client as a parameter. The function - * should execute whatever operations it desires on the client without concern - * for whether the client will throw an error. - */ - public interface QueryFunction { - public abstract void query(C client) - throws AuthenticationException, IOException, ParseException, - ConflictDetectedException, HttpException; - } - - /** - * A AuthenticatedFunction is passed in when executing the google - * authenticated service. The authenticated function of the class is called - * with the current authentication token for the service. The function should - * execute whatever operations with the google service without concern for - * whether the client will throw an error. - */ - public interface AuthenticatedFunction { - public abstract void run(String authenticationToken) - throws AuthenticationException, IOException; - } - - // The types of error that may be encountered - // No error occurred. - public static final int ERROR_NO_ERROR = 0; - // There was an authentication error, the auth token may be invalid. - public static final int ERROR_AUTH = 1; - // There was an internal error on the server side. - public static final int ERROR_INTERNAL = 2; - // There was an error connecting to the server. - public static final int ERROR_CONNECTION = 3; - // The item queried did not exit. - public static final int ERROR_NOT_FOUND = 4; - // There was an error parsing or serializing locally. - public static final int ERROR_LOCAL = 5; - // There was a conflict, update the entry and try again. - public static final int ERROR_CONFLICT = 6; - // A query was run after cleaning up the wrapper, so the client was invalid. - public static final int ERROR_CLEANED_UP = 7; - // An unknown error occurred. - public static final int ERROR_UNKNOWN = 100; - - private static final int AUTH_TOKEN_INVALIDATE_REFRESH_NUM_RETRIES = 1; - private static final int AUTH_TOKEN_INVALIDATE_REFRESH_TIMEOUT = 5000; - - private String errorMessage; - private int errorType; - private C gdataServiceClient; - private AuthManager auth; - private boolean retryOnAuthFailure; - - public GDataWrapper() { - errorType = ERROR_NO_ERROR; - errorMessage = null; - auth = null; - retryOnAuthFailure = false; - } - - public void setClient(C gdataServiceClient) { - this.gdataServiceClient = gdataServiceClient; - } - - public boolean runAuthenticatedFunction( - final AuthenticatedFunction function) { - return runCommon(function, null); - } - - public boolean runQuery(final QueryFunction query) { - return runCommon(null, query); - } - - /** - * Runs an arbitrary piece of code. - */ - private boolean runCommon(final AuthenticatedFunction function, - final QueryFunction query) { - for (int i = 0; i <= AUTH_TOKEN_INVALIDATE_REFRESH_NUM_RETRIES; i++) { - runOne(function, query); - if (errorType == ERROR_NO_ERROR) { - return true; - } - - Log.d(Constants.TAG, "GData error encountered: " + errorMessage); - if (errorType == ERROR_AUTH && auth != null) { - if (!retryOnAuthFailure || !invalidateAndRefreshAuthToken()) { - return false; - } - } - - Log.d(Constants.TAG, "retrying function/query"); - } - return false; - } - - /** - * Execute a given function or query. If one is executed, errorType and - * errorMessage will contain the result/status of the function/query. - */ - private void runOne(final AuthenticatedFunction function, - final QueryFunction query) { - try { - if (function != null) { - function.run(this.auth.getAuthToken()); - } else if (query != null) { - query.query(gdataServiceClient); - } else { - throw new IllegalArgumentException( - "invalid invocation of runOne; one of function/query " + - "must be non-null"); - } - - errorType = ERROR_NO_ERROR; - errorMessage = null; - - } catch (AuthenticationException e) { - Log.e(Constants.TAG, "AuthenticationException", e); - errorType = ERROR_AUTH; - errorMessage = e.getMessage(); - } catch (HttpException e) { - Log.e(Constants.TAG, - "HttpException, code " + e.getStatusCode() + " message " + e.getMessage(), e); - errorMessage = e.getMessage(); - if (e.getStatusCode() == 401) { - errorType = ERROR_AUTH; - } else { - errorType = ERROR_CONNECTION; - } - } catch (FileNotFoundException e) { - Log.e(Constants.TAG, "Exception", e); - errorType = ERROR_AUTH; - errorMessage = e.getMessage(); - } catch (IOException e) { - Log.e(Constants.TAG, "Exception", e); - errorMessage = e.getMessage(); - if (errorMessage != null && errorMessage.contains("503")) { - errorType = ERROR_INTERNAL; - } else { - errorType = ERROR_CONNECTION; - } - } catch (ParseException e) { - Log.e(Constants.TAG, "Exception", e); - errorType = ERROR_LOCAL; - errorMessage = e.getMessage(); - } catch (ConflictDetectedException e) { - Log.e(Constants.TAG, "Exception", e); - errorType = ERROR_CONFLICT; - errorMessage = e.getMessage(); - } - } - - /** - * Invalidates and refreshes the auth token. Blocks until the refresh has - * completed or until we deem the refresh as having timed out. - * - * @return true If the invalidate/refresh succeeds, false if it fails or - * times out. - */ - private boolean invalidateAndRefreshAuthToken() { - Log.d(Constants.TAG, "Retrying due to auth failure"); - // This FutureTask doesn't do anything -- it exists simply to be - // blocked upon using get(). - final FutureTask whenFinishedFuture = new FutureTask(new Runnable() { - public void run() {} - }, null); - - final AtomicBoolean finalSuccess = new AtomicBoolean(false); - auth.invalidateAndRefresh(new AuthCallback() { - @Override - public void onAuthResult(boolean success) { - finalSuccess.set(success); - whenFinishedFuture.run(); - } - }); - - try { - Log.d(Constants.TAG, "waiting for invalidate"); - whenFinishedFuture.get(AUTH_TOKEN_INVALIDATE_REFRESH_TIMEOUT, - TimeUnit.MILLISECONDS); - boolean success = finalSuccess.get(); - Log.d(Constants.TAG, "invalidate finished, success = " + success); - return success; - } catch (InterruptedException e) { - Log.e(Constants.TAG, "Failed to invalidate", e); - } catch (ExecutionException e) { - Log.e(Constants.TAG, "Failed to invalidate", e); - } catch (TimeoutException e) { - Log.e(Constants.TAG, "Invalidate didn't complete in time", e); - } finally { - whenFinishedFuture.cancel(false); - } - - return false; - } - - public int getErrorType() { - return errorType; - } - - public String getErrorMessage() { - return errorMessage; - } - - public void setAuthManager(AuthManager auth) { - this.auth = auth; - } - - public AuthManager getAuthManager() { - return auth; - } - - public void setRetryOnAuthFailure(boolean retry) { - retryOnAuthFailure = retry; - } -} diff --git a/MyTracks/src/com/google/wireless/gdata/docs/DocumentsClient.java b/MyTracks/src/com/google/android/apps/mytracks/io/gdata/docs/DocumentsClient.java similarity index 96% rename from MyTracks/src/com/google/wireless/gdata/docs/DocumentsClient.java rename to MyTracks/src/com/google/android/apps/mytracks/io/gdata/docs/DocumentsClient.java index 2b22dd3bc..fbdb98522 100644 --- a/MyTracks/src/com/google/wireless/gdata/docs/DocumentsClient.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/gdata/docs/DocumentsClient.java @@ -13,7 +13,7 @@ * License for the specific language governing permissions and limitations under * the License. */ -package com.google.wireless.gdata.docs; +package com.google.android.apps.mytracks.io.gdata.docs; import com.google.wireless.gdata.client.GDataClient; import com.google.wireless.gdata.client.GDataParserFactory; diff --git a/MyTracks/src/com/google/wireless/gdata/docs/SpreadsheetsClient.java b/MyTracks/src/com/google/android/apps/mytracks/io/gdata/docs/SpreadsheetsClient.java similarity index 99% rename from MyTracks/src/com/google/wireless/gdata/docs/SpreadsheetsClient.java rename to MyTracks/src/com/google/android/apps/mytracks/io/gdata/docs/SpreadsheetsClient.java index 803c5bf70..78f8c295a 100755 --- a/MyTracks/src/com/google/wireless/gdata/docs/SpreadsheetsClient.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/gdata/docs/SpreadsheetsClient.java @@ -13,7 +13,7 @@ * License for the specific language governing permissions and limitations under * the License. */ -package com.google.wireless.gdata.docs; +package com.google.android.apps.mytracks.io.gdata.docs; import com.google.wireless.gdata.client.GDataClient; import com.google.wireless.gdata.client.GDataParserFactory; diff --git a/MyTracks/src/com/google/wireless/gdata/docs/XmlDocsGDataParserFactory.java b/MyTracks/src/com/google/android/apps/mytracks/io/gdata/docs/XmlDocsGDataParserFactory.java similarity index 97% rename from MyTracks/src/com/google/wireless/gdata/docs/XmlDocsGDataParserFactory.java rename to MyTracks/src/com/google/android/apps/mytracks/io/gdata/docs/XmlDocsGDataParserFactory.java index f9337f934..a6e5e4985 100644 --- a/MyTracks/src/com/google/wireless/gdata/docs/XmlDocsGDataParserFactory.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/gdata/docs/XmlDocsGDataParserFactory.java @@ -13,7 +13,7 @@ * License for the specific language governing permissions and limitations under * the License. */ -package com.google.wireless.gdata.docs; +package com.google.android.apps.mytracks.io.gdata.docs; import com.google.wireless.gdata.client.GDataParserFactory; import com.google.wireless.gdata.data.Entry; diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/maps/MapFeatureEntry.java b/MyTracks/src/com/google/android/apps/mytracks/io/gdata/maps/MapFeatureEntry.java similarity index 87% rename from MyTracks/src/com/google/android/apps/mytracks/io/maps/MapFeatureEntry.java rename to MyTracks/src/com/google/android/apps/mytracks/io/gdata/maps/MapFeatureEntry.java index b7d118b5e..67081f4f1 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/maps/MapFeatureEntry.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/gdata/maps/MapFeatureEntry.java @@ -1,5 +1,5 @@ // Copyright 2010 Google Inc. All Rights Reserved. -package com.google.android.apps.mytracks.io.maps; +package com.google.android.apps.mytracks.io.gdata.maps; import com.google.wireless.gdata.data.Entry; @@ -9,7 +9,7 @@ import java.util.Map; /** * GData entry for a map feature. */ -class MapFeatureEntry extends Entry { +public class MapFeatureEntry extends Entry { private String mPrivacy = null; private Map mAttributes = new HashMap(); diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/maps/MapsClient.java b/MyTracks/src/com/google/android/apps/mytracks/io/gdata/maps/MapsClient.java similarity index 96% rename from MyTracks/src/com/google/android/apps/mytracks/io/maps/MapsClient.java rename to MyTracks/src/com/google/android/apps/mytracks/io/gdata/maps/MapsClient.java index dd03e5d18..3a87ac21c 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/maps/MapsClient.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/gdata/maps/MapsClient.java @@ -1,5 +1,5 @@ // Copyright 2010 Google Inc. All Rights Reserved. -package com.google.android.apps.mytracks.io.maps; +package com.google.android.apps.mytracks.io.gdata.maps; import com.google.wireless.gdata.client.GDataClient; import com.google.wireless.gdata.client.GDataParserFactory; @@ -10,7 +10,7 @@ import android.util.Log; /** * Client to talk to Google Maps via GData. */ -class MapsClient extends GDataServiceClient { +public class MapsClient extends GDataServiceClient { private static final boolean DEBUG = false; public static final boolean LOG_COMMUNICATION = false; diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/maps/MapsConstants.java b/MyTracks/src/com/google/android/apps/mytracks/io/gdata/maps/MapsConstants.java similarity index 79% rename from MyTracks/src/com/google/android/apps/mytracks/io/maps/MapsConstants.java rename to MyTracks/src/com/google/android/apps/mytracks/io/gdata/maps/MapsConstants.java index 380745644..b227bab8a 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/maps/MapsConstants.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/gdata/maps/MapsConstants.java @@ -1,11 +1,10 @@ // Copyright 2009 Google Inc. All Rights Reserved. -package com.google.android.apps.mytracks.io.maps; +package com.google.android.apps.mytracks.io.gdata.maps; /** * Constants for Google Maps. */ public class MapsConstants { - static final String TAG = "MapsApi"; static final String MAPSHOP_BASE_URL = "https://maps.google.com/maps/ms"; public static final String SERVICE_NAME = "local"; diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/maps/MapsFeature.java b/MyTracks/src/com/google/android/apps/mytracks/io/gdata/maps/MapsFeature.java similarity index 97% rename from MyTracks/src/com/google/android/apps/mytracks/io/maps/MapsFeature.java rename to MyTracks/src/com/google/android/apps/mytracks/io/gdata/maps/MapsFeature.java index 2dc71ac9d..6668ca7de 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/maps/MapsFeature.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/gdata/maps/MapsFeature.java @@ -1,5 +1,5 @@ // Copyright 2009 Google Inc. All Rights Reserved. -package com.google.android.apps.mytracks.io.maps; +package com.google.android.apps.mytracks.io.gdata.maps; import com.google.android.maps.GeoPoint; @@ -12,7 +12,7 @@ import java.util.Vector; * in a {@link MapsFeatureMetadata} object so that it can be more efficiently * transmitted to other activities. */ -class MapsFeature { +public class MapsFeature { private static final long serialVersionUID = 8439035544430497236L; @@ -73,7 +73,7 @@ class MapsFeature { * Generates a new local id for this feature based on the current time and * a random number. */ - void generateAndroidId() { + public void generateAndroidId() { long time = System.currentTimeMillis(); int rand = random.nextInt(10000); androidId = time + "." + rand; @@ -84,7 +84,7 @@ class MapsFeature { * * @return The local id for this feature */ - String getAndroidId() { + public String getAndroidId() { return androidId; } diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/maps/MapsFeatureMetadata.java b/MyTracks/src/com/google/android/apps/mytracks/io/gdata/maps/MapsFeatureMetadata.java similarity index 96% rename from MyTracks/src/com/google/android/apps/mytracks/io/maps/MapsFeatureMetadata.java rename to MyTracks/src/com/google/android/apps/mytracks/io/gdata/maps/MapsFeatureMetadata.java index 74bd82676..5de3e73d0 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/maps/MapsFeatureMetadata.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/gdata/maps/MapsFeatureMetadata.java @@ -1,5 +1,6 @@ // Copyright 2009 Google Inc. All Rights Reserved. -package com.google.android.apps.mytracks.io.maps; +package com.google.android.apps.mytracks.io.gdata.maps; + /** * Metadata about a maps feature. diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/maps/MapsGDataConverter.java b/MyTracks/src/com/google/android/apps/mytracks/io/gdata/maps/MapsGDataConverter.java similarity index 96% rename from MyTracks/src/com/google/android/apps/mytracks/io/maps/MapsGDataConverter.java rename to MyTracks/src/com/google/android/apps/mytracks/io/gdata/maps/MapsGDataConverter.java index 2092f0e91..0d22af712 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/maps/MapsGDataConverter.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/gdata/maps/MapsGDataConverter.java @@ -1,5 +1,5 @@ // Copyright 2009 Google Inc. All Rights Reserved. -package com.google.android.apps.mytracks.io.maps; +package com.google.android.apps.mytracks.io.gdata.maps; import com.google.wireless.gdata.data.Entry; import com.google.wireless.gdata.data.StringUtils; @@ -17,7 +17,7 @@ import org.xmlpull.v1.XmlSerializer; /** * Converter from GData objects to Maps objects. */ -class MapsGDataConverter { +public class MapsGDataConverter { private final XmlSerializer xmlSerializer; @@ -34,11 +34,7 @@ class MapsGDataConverter { metadata.setSearchable(false); } metadata.setTitle(entry.getTitle()); - - String desc = entry.getContent(); - if (desc.length() > 12) { - metadata.setDescription(desc.substring(9, desc.length() - 3)); - } + metadata.setDescription(entry.getSummary()); String editUri = entry.getEditUri(); if (editUri != null) { @@ -56,7 +52,7 @@ class MapsGDataConverter { MapFeatureEntry entry = new MapFeatureEntry(); entry.setEditUri(metadata.getGDataEditUri()); entry.setTitle(metadata.getTitle()); - entry.setContent(metadata.getDescription()); + entry.setSummary(metadata.getDescription()); entry.setPrivacy(metadata.getSearchable() ? "public" : "unlisted"); entry.setAuthor("android"); entry.setEmail("nobody@google.com"); diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/maps/MapsMapMetadata.java b/MyTracks/src/com/google/android/apps/mytracks/io/gdata/maps/MapsMapMetadata.java similarity index 91% rename from MyTracks/src/com/google/android/apps/mytracks/io/maps/MapsMapMetadata.java rename to MyTracks/src/com/google/android/apps/mytracks/io/gdata/maps/MapsMapMetadata.java index 01f13697d..7c7f2272a 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/maps/MapsMapMetadata.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/gdata/maps/MapsMapMetadata.java @@ -1,10 +1,10 @@ // Copyright 2009 Google Inc. All Rights Reserved. -package com.google.android.apps.mytracks.io.maps; +package com.google.android.apps.mytracks.io.gdata.maps; /** * Metadata about a Google Maps map. */ -class MapsMapMetadata { +public class MapsMapMetadata { private String title; private String description; diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/maps/XmlMapsGDataParser.java b/MyTracks/src/com/google/android/apps/mytracks/io/gdata/maps/XmlMapsGDataParser.java similarity index 99% rename from MyTracks/src/com/google/android/apps/mytracks/io/maps/XmlMapsGDataParser.java rename to MyTracks/src/com/google/android/apps/mytracks/io/gdata/maps/XmlMapsGDataParser.java index ff6f7f9f8..5204ec061 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/maps/XmlMapsGDataParser.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/gdata/maps/XmlMapsGDataParser.java @@ -1,5 +1,5 @@ // Copyright 2009 Google Inc. All Rights Reserved. -package com.google.android.apps.mytracks.io.maps; +package com.google.android.apps.mytracks.io.gdata.maps; import com.google.wireless.gdata.data.Entry; import com.google.wireless.gdata.data.Feed; diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/maps/XmlMapsGDataParserFactory.java b/MyTracks/src/com/google/android/apps/mytracks/io/gdata/maps/XmlMapsGDataParserFactory.java similarity index 95% rename from MyTracks/src/com/google/android/apps/mytracks/io/maps/XmlMapsGDataParserFactory.java rename to MyTracks/src/com/google/android/apps/mytracks/io/gdata/maps/XmlMapsGDataParserFactory.java index 7a2508464..bbf447a13 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/maps/XmlMapsGDataParserFactory.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/gdata/maps/XmlMapsGDataParserFactory.java @@ -1,5 +1,5 @@ // Copyright 2010 Google Inc. All Rights Reserved. -package com.google.android.apps.mytracks.io.maps; +package com.google.android.apps.mytracks.io.gdata.maps; import com.google.wireless.gdata.client.GDataParserFactory; import com.google.wireless.gdata.data.Entry; @@ -21,7 +21,7 @@ import org.xmlpull.v1.XmlPullParserException; /** * Factory of Xml parsers for gdata maps data. */ -class XmlMapsGDataParserFactory implements GDataParserFactory { +public class XmlMapsGDataParserFactory implements GDataParserFactory { private XmlParserFactory xmlFactory; public XmlMapsGDataParserFactory(XmlParserFactory xmlFactory) { diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/maps/XmlMapsGDataSerializer.java b/MyTracks/src/com/google/android/apps/mytracks/io/gdata/maps/XmlMapsGDataSerializer.java similarity index 99% rename from MyTracks/src/com/google/android/apps/mytracks/io/maps/XmlMapsGDataSerializer.java rename to MyTracks/src/com/google/android/apps/mytracks/io/gdata/maps/XmlMapsGDataSerializer.java index 2dc4c4d87..e533b2512 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/maps/XmlMapsGDataSerializer.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/gdata/maps/XmlMapsGDataSerializer.java @@ -1,5 +1,5 @@ // Copyright 2010 Google Inc. All Rights Reserved. -package com.google.android.apps.mytracks.io.maps; +package com.google.android.apps.mytracks.io.gdata.maps; import com.google.wireless.gdata.data.StringUtils; import com.google.wireless.gdata.parser.ParseException; diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/maps/ChooseMapActivity.java b/MyTracks/src/com/google/android/apps/mytracks/io/maps/ChooseMapActivity.java new file mode 100644 index 000000000..6e87f217e --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/io/maps/ChooseMapActivity.java @@ -0,0 +1,236 @@ +/* + * Copyright 2012 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.maps; + +import com.google.android.apps.mytracks.io.gdata.maps.MapsMapMetadata; +import com.google.android.apps.mytracks.io.sendtogoogle.SendRequest; +import com.google.android.maps.mytracks.R; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.ProgressDialog; +import android.content.DialogInterface; +import android.content.DialogInterface.OnCancelListener; +import android.content.DialogInterface.OnClickListener; +import android.content.Intent; +import android.graphics.Color; +import android.os.Bundle; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.ListView; +import android.widget.TextView; + +import java.util.ArrayList; + +/** + * An activity to choose a Google Map. + * + * @author jshih@google.com (Jimmy Shih) + */ +public class ChooseMapActivity extends Activity { + + private static final int PROGRESS_DIALOG = 1; + private static final int ERROR_DIALOG = 2; + + private SendRequest sendRequest; + private ChooseMapAsyncTask asyncTask; + private ProgressDialog progressDialog; + private ArrayAdapter arrayAdapter; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + sendRequest = getIntent().getParcelableExtra(SendRequest.SEND_REQUEST_KEY); + setContentView(R.layout.choose_map); + + arrayAdapter = new ArrayAdapter(this, R.layout.choose_map_item, new ArrayList< + ListItem>()) { + @Override + public View getView(int position, View convertView, ViewGroup parent) { + if (convertView == null) { + convertView = getLayoutInflater().inflate(R.layout.choose_map_item, parent, false); + } + MapsMapMetadata mapData = getItem(position).getMapData(); + + TextView title = (TextView) convertView.findViewById(R.id.choose_map_list_item_title); + title.setText(mapData.getTitle()); + + TextView description = (TextView) convertView.findViewById( + R.id.choose_map_list_item_description); + String descriptionText = mapData.getDescription(); + if (descriptionText == null || descriptionText.equals("")) { + description.setVisibility(View.GONE); + } else { + description.setVisibility(View.VISIBLE); + description.setText(descriptionText); + } + + TextView searchStatus = (TextView) convertView.findViewById( + R.id.choose_map_list_item_search_status); + searchStatus.setTextColor(mapData.getSearchable() ? Color.RED : Color.GREEN); + searchStatus.setText(mapData.getSearchable() ? R.string.maps_list_public_label + : R.string.maps_list_unlisted_label); + return convertView; + } + }; + + ListView list = (ListView) findViewById(R.id.choose_map_list_view); + list.setEmptyView(findViewById(R.id.choose_map_empty_view)); + list.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + startNextActivity(arrayAdapter.getItem(position).getMapId()); + } + }); + list.setAdapter(arrayAdapter); + + Object retained = getLastNonConfigurationInstance(); + if (retained instanceof ChooseMapAsyncTask) { + asyncTask = (ChooseMapAsyncTask) retained; + asyncTask.setActivity(this); + } else { + asyncTask = new ChooseMapAsyncTask(this, sendRequest.getAccount()); + asyncTask.execute(); + } + } + + @Override + public Object onRetainNonConfigurationInstance() { + asyncTask.setActivity(null); + return asyncTask; + } + + @Override + protected Dialog onCreateDialog(int id) { + switch (id) { + case PROGRESS_DIALOG: + progressDialog = new ProgressDialog(this); + progressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER); + progressDialog.setMessage(getString(R.string.maps_list_loading)); + progressDialog.setCancelable(true); + progressDialog.setOnCancelListener(new DialogInterface.OnCancelListener() { + @Override + public void onCancel(DialogInterface dialog) { + asyncTask.cancel(true); + finish(); + } + }); + progressDialog.setIcon(android.R.drawable.ic_dialog_info); + progressDialog.setTitle(R.string.generic_progress_title); + return progressDialog; + case ERROR_DIALOG: + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setIcon(android.R.drawable.ic_dialog_alert); + builder.setTitle(R.string.generic_error_title); + builder.setMessage(R.string.maps_list_error); + builder.setPositiveButton(R.string.generic_ok, new OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int arg1) { + finish(); + } + }); + builder.setOnCancelListener(new OnCancelListener() { + @Override + public void onCancel(DialogInterface dialog) { + finish(); + } + }); + return builder.create(); + default: + return null; + } + } + + /** + * Invokes when the associated AsyncTask completes. + * + * @param success true if success + * @param mapIds an array of map ids + * @param mapData an array of map data + */ + public void onAsyncTaskCompleted( + boolean success, ArrayList mapIds, ArrayList mapData) { + removeDialog(PROGRESS_DIALOG); + if (success) { + arrayAdapter.clear(); + // To prevent displaying the emptyView message momentarily before the + // arrayAdapter is set, don't set the emptyView message in the xml layout. + // Instead, set it only when needed. + if (mapIds.size() == 0) { + TextView emptyView = (TextView) findViewById(R.id.choose_map_empty_view); + emptyView.setText(R.string.maps_list_no_maps); + } else { + for (int i = 0; i < mapIds.size(); i++) { + arrayAdapter.add(new ListItem(mapIds.get(i), mapData.get(i))); + } + } + } else { + showDialog(ERROR_DIALOG); + } + } + + /** + * Shows the progress dialog. + */ + public void showProgressDialog() { + showDialog(PROGRESS_DIALOG); + } + + /** + * Starts the next activity, {@link SendMapsActivity}. + * + * @param mapId the chosen map id + */ + private void startNextActivity(String mapId) { + sendRequest.setMapId(mapId); + Intent intent = new Intent(this, SendMapsActivity.class) + .putExtra(SendRequest.SEND_REQUEST_KEY, sendRequest); + startActivity(intent); + finish(); + } + + /** + * A class containing {@link ChooseMapActivity} list item. + * + * @author jshih@google.com (Jimmy Shih) + */ + private class ListItem { + private String mapId; + private MapsMapMetadata mapData; + + private ListItem(String mapId, MapsMapMetadata mapData) { + this.mapId = mapId; + this.mapData = mapData; + } + + /** + * Gets the map id. + */ + public String getMapId() { + return mapId; + } + + /** + * Gets the map data. + */ + public MapsMapMetadata getMapData() { + return mapData; + } + } +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/maps/ChooseMapAsyncTask.java b/MyTracks/src/com/google/android/apps/mytracks/io/maps/ChooseMapAsyncTask.java new file mode 100644 index 000000000..96c0064b8 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/io/maps/ChooseMapAsyncTask.java @@ -0,0 +1,208 @@ +/* + * Copyright 2012 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.maps; + +import com.google.android.apps.mytracks.io.gdata.GDataClientFactory; +import com.google.android.apps.mytracks.io.gdata.maps.MapFeatureEntry; +import com.google.android.apps.mytracks.io.gdata.maps.MapsClient; +import com.google.android.apps.mytracks.io.gdata.maps.MapsConstants; +import com.google.android.apps.mytracks.io.gdata.maps.MapsGDataConverter; +import com.google.android.apps.mytracks.io.gdata.maps.MapsMapMetadata; +import com.google.android.apps.mytracks.io.gdata.maps.XmlMapsGDataParserFactory; +import com.google.android.common.gdata.AndroidXmlParserFactory; +import com.google.wireless.gdata.client.GDataClient; +import com.google.wireless.gdata.client.HttpException; +import com.google.wireless.gdata.parser.GDataParser; +import com.google.wireless.gdata.parser.ParseException; + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.accounts.AuthenticatorException; +import android.accounts.OperationCanceledException; +import android.content.Context; +import android.os.AsyncTask; +import android.util.Log; + +import java.io.IOException; +import java.util.ArrayList; + +/** + * AsyncTask for {@link ChooseMapActivity} to get all the maps from Google Maps. + * + * @author jshih@google.com (Jimmy Shih) + */ +public class ChooseMapAsyncTask extends AsyncTask { + private static final String TAG = ChooseMapAsyncTask.class.getSimpleName(); + + private ChooseMapActivity activity; + private final Account account; + private final Context context; + private final GDataClient gDataClient; + private final MapsClient mapsClient; + + /** + * True if can retry sending to Google Fusion Tables. + */ + private boolean canRetry; + + /** + * True if the AsyncTask has completed. + */ + private boolean completed; + + /** + * True if the result is success. + */ + private boolean success; + + // The following variables are for per request states + private String authToken; + private ArrayList mapIds; + private ArrayList mapData; + + public ChooseMapAsyncTask(ChooseMapActivity activity, Account account) { + this.activity = activity; + this.account = account; + + context = activity.getApplicationContext(); + gDataClient = GDataClientFactory.getGDataClient(context); + mapsClient = new MapsClient( + gDataClient, new XmlMapsGDataParserFactory(new AndroidXmlParserFactory())); + + canRetry = true; + completed = false; + success = false; + } + + /** + * Sets the activity associated with this AyncTask. + * + * @param activity the activity. + */ + public void setActivity(ChooseMapActivity activity) { + this.activity = activity; + if (completed && activity != null) { + activity.onAsyncTaskCompleted(success, mapIds, mapData); + } + } + + @Override + protected void onPreExecute() { + activity.showProgressDialog(); + } + + @Override + protected Boolean doInBackground(Void... params) { + return getMaps(); + } + + @Override + protected void onCancelled() { + closeClient(); + } + + @Override + protected void onPostExecute(Boolean result) { + closeClient(); + success = result; + completed = true; + if (activity != null) { + activity.onAsyncTaskCompleted(success, mapIds, mapData); + } + } + + /** + * Closes the gdata client. + */ + private void closeClient() { + if (gDataClient != null) { + gDataClient.close(); + } + } + + /** + * Gets all the maps from Google Maps. + * + * @return true if success. + */ + private boolean getMaps() { + // Reset the per request states + authToken = null; + mapIds = new ArrayList(); + mapData = new ArrayList(); + + try { + authToken = AccountManager.get(context).blockingGetAuthToken( + account, MapsConstants.SERVICE_NAME, false); + } catch (OperationCanceledException e) { + Log.d(TAG, "Unable to get auth token", e); + return retryUpload(); + } catch (AuthenticatorException e) { + Log.d(TAG, "Unable to get auth token", e); + return retryUpload(); + } catch (IOException e) { + Log.d(TAG, "Unable to get auth token", e); + return retryUpload(); + } + + if (isCancelled()) { + return false; + } + GDataParser gDataParser = null; + try { + gDataParser = mapsClient.getParserForFeed( + MapFeatureEntry.class, MapsClient.getMapsFeed(), authToken); + gDataParser.init(); + while (gDataParser.hasMoreData()) { + MapFeatureEntry entry = (MapFeatureEntry) gDataParser.readNextEntry(null); + mapIds.add(MapsGDataConverter.getMapidForEntry(entry)); + mapData.add(MapsGDataConverter.getMapMetadataForEntry(entry)); + } + } catch (ParseException e) { + Log.d(TAG, "Unable to get maps", e); + return retryUpload(); + } catch (IOException e) { + Log.d(TAG, "Unable to get maps", e); + return retryUpload(); + } catch (HttpException e) { + Log.d(TAG, "Unable to get maps", e); + return retryUpload(); + } finally { + if (gDataParser != null) { + gDataParser.close(); + } + } + + return true; + } + + /** + * Retries upload. Invalidates the authToken. If can retry, invokes + * {@link ChooseMapAsyncTask#getMaps()}. Returns false if cannot retry. + */ + private boolean retryUpload() { + if (isCancelled()) { + return false; + } + + AccountManager.get(context).invalidateAuthToken(MapsConstants.SERVICE_NAME, authToken); + if (canRetry) { + canRetry = false; + return getMaps(); + } + return false; + } +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/maps/GeoRect.java b/MyTracks/src/com/google/android/apps/mytracks/io/maps/GeoRect.java deleted file mode 100644 index 6eca581c5..000000000 --- a/MyTracks/src/com/google/android/apps/mytracks/io/maps/GeoRect.java +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright 2009 Google Inc. All Rights Reserved. -package com.google.android.apps.mytracks.io.maps; - -import com.google.android.maps.GeoPoint; - -/** - * A rectangle in geographical space. - */ -class GeoRect { - public int top; - public int left; - public int bottom; - public int right; - - public GeoRect() { - top = 0; - left = 0; - bottom = 0; - right = 0; - } - - public GeoRect(GeoPoint center, int latSpan, int longSpan) { - top = center.getLatitudeE6() - latSpan / 2; - left = center.getLongitudeE6() - longSpan / 2; - bottom = center.getLatitudeE6() + latSpan / 2; - right = center.getLongitudeE6() + longSpan / 2; - } - - public GeoPoint getCenter() { - return new GeoPoint(top / 2 + bottom / 2, left / 2 + right / 2); - } - - public int getLatSpan() { - return bottom - top; - } - - public int getLongSpan() { - return right - left; - } - - public boolean contains(GeoPoint geoPoint) { - if (geoPoint.getLatitudeE6() >= top - && geoPoint.getLatitudeE6() <= bottom - && geoPoint.getLongitudeE6() >= left - && geoPoint.getLongitudeE6() <= right) { - return true; - } - return false; - } -} diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/maps/MapsFacade.java b/MyTracks/src/com/google/android/apps/mytracks/io/maps/MapsFacade.java deleted file mode 100644 index 97bb07031..000000000 --- a/MyTracks/src/com/google/android/apps/mytracks/io/maps/MapsFacade.java +++ /dev/null @@ -1,379 +0,0 @@ -// Copyright 2011 Google Inc. All Rights Reserved. -package com.google.android.apps.mytracks.io.maps; - -import com.google.android.apps.mytracks.content.Waypoint; -import com.google.android.apps.mytracks.io.AuthManager; -import com.google.android.apps.mytracks.util.StringUtils; -import com.google.android.maps.GeoPoint; -import com.google.android.maps.mytracks.R; -import com.google.wireless.gdata.client.HttpException; -import com.google.wireless.gdata.data.Entry; -import com.google.wireless.gdata.parser.GDataParser; -import com.google.wireless.gdata.parser.ParseException; - -import android.content.Context; -import android.location.Location; -import android.text.TextUtils; -import android.util.Log; - -import java.io.IOException; -import java.util.Collection; - -import org.xmlpull.v1.XmlPullParserException; - -/** - * Single interface which abstracts all access to the Google Maps service. - * - * @author Rodrigo Damazio - */ -public class MapsFacade { - - /** - * Interface for receiving data back from getMapsList. - * All calls to the interface will happen before getMapsList returns. - */ - public interface MapsListCallback { - void onReceivedMapListing(String mapId, String title, String description, - boolean isPublic); - } - - private static final String END_ICON_URL = - "http://maps.google.com/mapfiles/ms/micons/red-dot.png"; - private static final String START_ICON_URL = - "http://maps.google.com/mapfiles/ms/micons/green-dot.png"; - - private final Context context; - private final MapsGDataWrapper wrapper; - private final MapsGDataConverter gdataConverter; - private final String authToken; - - public MapsFacade(Context context, AuthManager auth) { - this.context = context; - this.authToken = auth.getAuthToken(); - - wrapper = new MapsGDataWrapper(context, auth); - wrapper.setRetryOnAuthFailure(true); - - try { - gdataConverter = new MapsGDataConverter(); - } catch (XmlPullParserException e) { - throw new IllegalStateException("Unable to create maps data converter", e); - } - } - - public static String buildMapUrl(String mapId) { - return MapsClient.buildMapUrl(mapId); - } - - /** - * Returns a list of all maps for the current user. - * - * @param callback callback to call for each map returned - * @return true on success, false otherwise - */ - public boolean getMapsList(final MapsListCallback callback) { - return wrapper.runQuery(new MapsGDataWrapper.QueryFunction() { - @Override - public void query(MapsClient client) throws IOException, Exception { - GDataParser listParser = client.getParserForFeed( - MapFeatureEntry.class, MapsClient.getMapsFeed(), authToken); - listParser.init(); - while (listParser.hasMoreData()) { - MapFeatureEntry entry = - (MapFeatureEntry) listParser.readNextEntry(null); - MapsMapMetadata metadata = - MapsGDataConverter.getMapMetadataForEntry(entry); - String mapId = MapsGDataConverter.getMapidForEntry(entry); - - callback.onReceivedMapListing( - mapId, metadata.getTitle(), metadata.getDescription(), metadata.getSearchable()); - } - listParser.close(); - listParser = null; - } - }); - } - - /** - * Creates a new map for the current user. - * - * @param title title of the map - * @param category category of the map - * @param description description for the map - * @param isPublic whether the map should be public - * @param mapIdBuilder builder to append the map ID to - * @return true on success, false otherwise - */ - public boolean createNewMap( - final String title, final String category, final String description, - final boolean isPublic, final StringBuilder mapIdBuilder) { - if (mapIdBuilder.length() > 0) { - throw new IllegalArgumentException("mapIdBuilder should be empty"); - } - - return wrapper.runQuery(new MapsGDataWrapper.QueryFunction() { - @Override - public void query(MapsClient client) throws IOException, Exception { - Log.d(MapsConstants.TAG, "Creating a new map."); - String mapFeed = MapsClient.getMapsFeed(); - Log.d(MapsConstants.TAG, "Map feed is " + mapFeed); - MapsMapMetadata metaData = new MapsMapMetadata(); - metaData.setTitle(title); - metaData.setDescription(description + " - " - + category + " - " + StringUtils.getCreatedByMyTracks(context, false)); - metaData.setSearchable(isPublic); - Entry entry = MapsGDataConverter.getMapEntryForMetadata(metaData); - Log.d(MapsConstants.TAG, "Title: " + entry.getTitle()); - Entry map = client.createEntry(mapFeed, authToken, entry); - - String mapId = MapsClient.getMapIdFromMapEntryId(map.getId()); - mapIdBuilder.append(mapId); - Log.d(MapsConstants.TAG, "New map id is: " + mapId); - } - }); - } - - /** - * Uploads a single start or end marker to the given map. - * - * @param mapId ID of the map to upload to - * @param trackName name of the track being started/ended - * @param trackDescription description of the track being started/ended - * @param loc the location of the marker - * @param isStart true to add a start marker, false to add an end marker - * @return true on success, false otherwise - */ - public boolean uploadMarker(final String mapId, final String trackName, - final String trackDescription, final Location loc, final boolean isStart) { - return wrapper.runQuery(new MapsGDataWrapper.QueryFunction() { - @Override - public void query(MapsClient client) - throws IOException, Exception { - String featureFeed = MapsClient.getFeaturesFeed(mapId); - GeoPoint geoPoint = getGeoPoint(loc); - insertMarker(client, featureFeed, trackName, trackDescription, geoPoint, isStart); - } - }); - } - - /** - * Inserts a place mark. Second try if 1st try fails. Will throw exception on - * 2nd failure. - */ - private void insertMarker(MapsClient client, - String featureFeed, - String trackName, String trackDescription, - GeoPoint geoPoint, - boolean isStart) throws IOException, Exception { - MapsFeature feature = - buildMapsPlacemarkFeature(trackName, trackDescription, geoPoint, isStart); - Entry entry = gdataConverter.getEntryForFeature(feature); - Log.d(MapsConstants.TAG, "SendToMaps: Creating placemark " - + entry.getTitle()); - try { - client.createEntry(featureFeed, authToken, entry); - Log.d(MapsConstants.TAG, "SendToMaps: createEntry success!"); - } catch (IOException e) { - Log.w(MapsConstants.TAG, - "SendToMaps: createEntry 1st try failed. Trying again."); - // Retry once (often IOException is thrown on a timeout): - client.createEntry(featureFeed, authToken, entry); - Log.d(MapsConstants.TAG, - "SendToMaps: createEntry success on 2nd try!"); - } - } - - /** - * Builds a placemark MapsFeature from a track. - * - * @param trackName the track - * @param trackDescription the track description - * @param geoPoint the geo point - * @param isStart true if it's the start of the track, or false for end - * @return a MapsFeature - */ - private MapsFeature buildMapsPlacemarkFeature( - String trackName, String trackDescription, - GeoPoint geoPoint, boolean isStart) { - String iconUrl; - if (isStart) { - iconUrl = START_ICON_URL; - } else { - iconUrl = END_ICON_URL; - } - String title = trackName + " " - + (isStart ? context.getString(R.string.marker_label_start) - : context.getString(R.string.marker_label_end)); - String description = isStart ? "" : trackDescription; - return buildMapsPlacemarkFeature(title, description, iconUrl, geoPoint); - } - - /** - * Builds a MapsFeature from a waypoint. - * - * @param title the title - * @param description the description - * @param iconUrl the icon url - * @param geoPoint the waypoint - * @return a MapsFeature - */ - private static MapsFeature buildMapsPlacemarkFeature( - String title, String description, String iconUrl, GeoPoint geoPoint) { - MapsFeature mapsFeature = new MapsFeature(); - mapsFeature.generateAndroidId(); - mapsFeature.setType(MapsFeature.MARKER); - mapsFeature.setIconUrl(iconUrl); - mapsFeature.setDescription(description); - mapsFeature.addPoint(geoPoint); - if (TextUtils.isEmpty(title)) { - // Features must have a name (otherwise GData upload may fail): - mapsFeature.setTitle("-"); - } else { - mapsFeature.setTitle(title); - } - mapsFeature.setDescription(description.replaceAll("\n", "
")); - return mapsFeature; - } - - /** - * Uploads a series of waypoints to the given map. - * - * @param mapId ID of the map to upload to - * @param waypoints the waypoints to upload - * @return true on success, false otherwise - */ - public boolean uploadWaypoints( - final String mapId, final Iterable waypoints) { - return wrapper.runQuery(new MapsGDataWrapper.QueryFunction() { - public void query(MapsClient client) { - // TODO(rdamazio): Stream through the waypoints in chunks. - // I am leaving the number of waypoints very high which should not be a - // problem because we don't try to load them into objects all at the - // same time. - String featureFeed = MapsClient.getFeaturesFeed(mapId); - - try { - for (Waypoint waypoint : waypoints) { - MapsFeature feature = buildMapsPlacemarkFeature( - waypoint.getName(), waypoint.getDescription(), waypoint.getIcon(), - getGeoPoint(waypoint.getLocation())); - Entry entry = gdataConverter.getEntryForFeature(feature); - - Log.d(MapsConstants.TAG, - "SendToMaps: Creating waypoint."); - try { - client.createEntry(featureFeed, authToken, entry); - Log.d(MapsConstants.TAG, - "SendToMaps: createEntry success!"); - } catch (IOException e) { - Log.w(MapsConstants.TAG, - "SendToMaps: createEntry 1st try failed. Retrying."); - - // Retry once (often IOException is thrown on a timeout): - client.createEntry(featureFeed, authToken, entry); - Log.d(MapsConstants.TAG, - "SendToMaps: createEntry success on 2nd try!"); - } - } - } catch (ParseException e) { - Log.w(MapsConstants.TAG, "ParseException caught.", e); - } catch (HttpException e) { - Log.w(MapsConstants.TAG, "HttpException caught.", e); - } catch (IOException e) { - Log.w(MapsConstants.TAG, "IOException caught.", e); - } - } - }); - } - - /** - * Uploads a series of points to the given map. - * - * @param mapId ID of the map to upload to - * @param trackName the name of the track - * @param locations the locations to upload - * @return true on success, false otherwise - */ - public boolean uploadTrackPoints( - final String mapId, final String trackName, final Collection locations) { - return wrapper.runQuery(new MapsGDataWrapper.QueryFunction() { - @Override - public void query(MapsClient client) - throws IOException, Exception { - String featureFeed = MapsClient.getFeaturesFeed(mapId); - Log.d(MapsConstants.TAG, "Feature feed url: " + featureFeed); - uploadTrackPoints(client, featureFeed, trackName, locations); - } - }); - } - - private boolean uploadTrackPoints( - MapsClient client, - String featureFeed, - String trackName, Collection locations) - throws IOException, Exception { - Entry entry = null; - int numLocations = locations.size(); - if (numLocations < 2) { - // Need at least two points for a polyline: - Log.w(MapsConstants.TAG, "Not uploading too few points"); - return true; - } - - // Put the line: - entry = gdataConverter.getEntryForFeature( - buildMapsLineFeature(trackName, locations)); - Log.d(MapsConstants.TAG, - "SendToMaps: Creating line " + entry.getTitle()); - try { - client.createEntry(featureFeed, authToken, entry); - Log.d(MapsConstants.TAG, "SendToMaps: createEntry success!"); - } catch (IOException e) { - Log.w(MapsConstants.TAG, - "SendToMaps: createEntry 1st try failed. Trying again."); - // Retry once (often IOException is thrown on a timeout): - client.createEntry(featureFeed, authToken, entry); - Log.d(MapsConstants.TAG, - "SendToMaps: createEntry success on 2nd try!"); - } - return true; - } - - /** - * Builds a MapsFeature from a track. - * - * @param trackName the track name - * @param locations locations on the track - * @return a MapsFeature - */ - private static MapsFeature buildMapsLineFeature(String trackName, - Iterable locations) { - MapsFeature mapsFeature = new MapsFeature(); - mapsFeature.generateAndroidId(); - mapsFeature.setType(MapsFeature.LINE); - if (TextUtils.isEmpty(trackName)) { - // Features must have a name (otherwise GData upload may fail): - mapsFeature.setTitle("-"); - } else { - mapsFeature.setTitle(trackName); - } - mapsFeature.setColor(0x80FF0000); - for (Location loc : locations) { - mapsFeature.addPoint(getGeoPoint(loc)); - } - return mapsFeature; - } - - /** - * Cleans up after a series of uploads. - * This closes the connection to Maps and resets retry counters. - */ - public void cleanUp() { - wrapper.cleanUp(); - } - - private static GeoPoint getGeoPoint(Location location) { - return new GeoPoint((int) (location.getLatitude() * 1E6), - (int) (location.getLongitude() * 1E6)); - } -} diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/maps/MapsGDataWrapper.java b/MyTracks/src/com/google/android/apps/mytracks/io/maps/MapsGDataWrapper.java deleted file mode 100644 index 1d2631a9f..000000000 --- a/MyTracks/src/com/google/android/apps/mytracks/io/maps/MapsGDataWrapper.java +++ /dev/null @@ -1,171 +0,0 @@ -// Copyright 2010 Google Inc. All Rights Reserved. -package com.google.android.apps.mytracks.io.maps; - -import com.google.android.apps.mytracks.io.AuthManager; -import com.google.android.apps.mytracks.io.AuthManager.AuthCallback; -import com.google.android.apps.mytracks.io.gdata.GDataClientFactory; -import com.google.android.common.gdata.AndroidXmlParserFactory; -import com.google.wireless.gdata.client.GDataClient; -import com.google.wireless.gdata.client.HttpException; -import com.google.wireless.gdata.parser.xml.SimplePullParser.ParseException; -import com.google.wireless.gdata2.ConflictDetectedException; -import com.google.wireless.gdata2.client.AuthenticationException; - -import android.content.Context; -import android.util.Log; - -import java.io.IOException; - -/** - * MapsGDataWrapper provides a wrapper around GData operations that maintains - * the GData client, and provides a method to run gdata queries with proper - * error handling. After a query is run, the wrapper can be queried about the - * error that occurred. - */ -class MapsGDataWrapper { - /** - * A QueryFunction is passed in when executing a query. The query function - * of the class is called with the GData client as a parameter. The - * function should execute whatever operations it desires on the client - * without concern for whether the client will throw an error. - */ - interface QueryFunction { - public abstract void query(MapsClient client) - throws AuthenticationException, IOException, ParseException, - ConflictDetectedException, Exception; - } - - // The types of error that may be encountered - /** No error occurred. */ - public static final int ERROR_NO_ERROR = 0; - /** There was an authentication error, the auth token may be invalid. */ - public static final int ERROR_AUTH = 1; - /** There was an internal error on the server side. */ - public static final int ERROR_INTERNAL = 2; - /** There was an error connecting to the server. */ - public static final int ERROR_CONNECTION = 3; - /** The item queried did not exit. */ - public static final int ERROR_NOT_FOUND = 4; - /** There was an error parsing or serializing locally. */ - public static final int ERROR_LOCAL = 5; - /** There was a conflict, update the entry and try again. */ - public static final int ERROR_CONFLICT = 6; - /** - * A query was run after cleaning up the wrapper, so the client was invalid. - */ - public static final int ERROR_CLEANED_UP = 7; - /** An unknown error occurred. */ - public static final int ERROR_UNKNOWN = 100; - - private final GDataClient androidGdataClient; - private final AuthManager auth; - private final MapsClient client; - - private String errorMessage; - private int errorType; - private boolean retryOnAuthFailure; - private int retriesPending; - private boolean cleanupCalled; - - public MapsGDataWrapper(Context context, AuthManager auth) { - androidGdataClient = GDataClientFactory.getGDataClient(context); - this.auth = auth; - client = - new MapsClient(androidGdataClient, new XmlMapsGDataParserFactory( - new AndroidXmlParserFactory())); - errorType = ERROR_NO_ERROR; - errorMessage = null; - retryOnAuthFailure = false; - retriesPending = 0; - cleanupCalled = false; - } - - public boolean runQuery(final QueryFunction query) { - if (client == null) { - errorType = ERROR_CLEANED_UP; - errorMessage = "GData Wrapper has already been cleaned up!"; - return false; - } - try { - query.query(client); - errorType = ERROR_NO_ERROR; - errorMessage = null; - return true; - } catch (AuthenticationException e) { - Log.e(MapsConstants.TAG, "Exception", e); - errorType = ERROR_AUTH; - errorMessage = e.getMessage(); - } catch (HttpException e) { - Log.e(MapsConstants.TAG, "HttpException", e); - errorMessage = e.getMessage(); - if (errorMessage != null && errorMessage.contains("401")) { - errorType = ERROR_AUTH; - } else { - errorType = ERROR_CONNECTION; - } - } catch (IOException e) { - Log.e(MapsConstants.TAG, "Exception", e); - errorMessage = e.getMessage(); - if (errorMessage != null && errorMessage.contains("503")) { - errorType = ERROR_INTERNAL; - } else { - errorType = ERROR_CONNECTION; - } - } catch (ParseException e) { - Log.e(MapsConstants.TAG, "Exception", e); - errorType = ERROR_LOCAL; - errorMessage = e.getMessage(); - } catch (ConflictDetectedException e) { - Log.e(MapsConstants.TAG, "Exception", e); - errorType = ERROR_CONFLICT; - errorMessage = e.getMessage(); - } catch (Exception e) { - Log.e(MapsConstants.TAG, "Exception", e); - errorType = ERROR_UNKNOWN; - errorMessage = e.getMessage(); - e.printStackTrace(); - } - Log.d(MapsConstants.TAG, "GData error encountered: " + errorMessage); - if (errorType == ERROR_AUTH && auth != null) { - AuthCallback whenFinished = null; - if (retryOnAuthFailure) { - retriesPending++; - whenFinished = new AuthCallback() { - @Override - public void onAuthResult(boolean success) { - retriesPending--; - retryOnAuthFailure = false; - runQuery(query); - if (cleanupCalled && retriesPending == 0) { - cleanUp(); - } - } - }; - } - auth.invalidateAndRefresh(whenFinished); - } - return false; - } - - public int getErrorType() { - return errorType; - } - - public String getErrorMessage() { - return errorMessage; - } - - // cleanUp must be called when done using this wrapper to close the client. - // Note that the cleanup will be delayed if auth failure retries were - // requested and there is a pending retry. - public void cleanUp() { - if (retriesPending == 0 && !cleanupCalled) { - androidGdataClient.close(); - } - cleanupCalled = true; - } - - public void setRetryOnAuthFailure(boolean retry) { - retryOnAuthFailure = retry; - } -} diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/maps/SendMapsActivity.java b/MyTracks/src/com/google/android/apps/mytracks/io/maps/SendMapsActivity.java new file mode 100644 index 000000000..7b3d83de9 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/io/maps/SendMapsActivity.java @@ -0,0 +1,142 @@ +/* + * Copyright 2012 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.maps; + +import com.google.android.apps.mytracks.io.docs.SendDocsActivity; +import com.google.android.apps.mytracks.io.fusiontables.SendFusionTablesActivity; +import com.google.android.apps.mytracks.io.sendtogoogle.SendRequest; +import com.google.android.apps.mytracks.io.sendtogoogle.UploadResultActivity; +import com.google.android.maps.mytracks.R; + +import android.app.Activity; +import android.app.Dialog; +import android.app.ProgressDialog; +import android.content.DialogInterface; +import android.content.Intent; +import android.os.Bundle; + +/** + * An activity to send a track to Google Maps. + * + * @author Jimmy Shih + */ +public class SendMapsActivity extends Activity { + + private static final int PROGRESS_DIALOG = 1; + + private SendRequest sendRequest; + private SendMapsAsyncTask asyncTask; + private ProgressDialog progressDialog; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + sendRequest = getIntent().getParcelableExtra(SendRequest.SEND_REQUEST_KEY); + + Object retained = getLastNonConfigurationInstance(); + if (retained instanceof SendMapsAsyncTask) { + asyncTask = (SendMapsAsyncTask) retained; + asyncTask.setActivity(this); + } else { + asyncTask = new SendMapsAsyncTask( + this, sendRequest.getTrackId(), sendRequest.getAccount(), sendRequest.getMapId()); + asyncTask.execute(); + } + } + + @Override + public Object onRetainNonConfigurationInstance() { + asyncTask.setActivity(null); + return asyncTask; + } + + @Override + protected Dialog onCreateDialog(int id) { + switch (id) { + case PROGRESS_DIALOG: + progressDialog = new ProgressDialog(this); + progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); + progressDialog.setIcon(android.R.drawable.ic_dialog_info); + progressDialog.setTitle( + getString(R.string.send_google_progress_title, getString(R.string.send_google_maps))); + progressDialog.setMax(100); + progressDialog.setProgress(0); + progressDialog.setCancelable(true); + progressDialog.setOnCancelListener(new DialogInterface.OnCancelListener() { + @Override + public void onCancel(DialogInterface dialog) { + asyncTask.cancel(true); + startNextActivity(false, true); + } + }); + return progressDialog; + default: + return null; + } + } + + /** + * Invokes when the associated AsyncTask completes. + * + * @param success true if success + */ + public void onAsyncTaskCompleted(boolean success) { + startNextActivity(success, false); + } + + /** + * Shows the progress dialog. + */ + public void showProgressDialog() { + showDialog(PROGRESS_DIALOG); + } + + /** + * Sets the progress dialog value. + */ + public void setProgressDialogValue(int value) { + if (progressDialog != null) { + progressDialog.setProgress(value); + } + } + + /** + * Starts the next activity. + * + * @param success true if sendMaps is success + * @param isCancel true if it is a cancel request + */ + private void startNextActivity(boolean success, boolean isCancel) { + sendRequest.setMapsSuccess(success); + + Class next; + if (isCancel) { + next = UploadResultActivity.class; + } else { + if (sendRequest.isSendFusionTables()) { + next = SendFusionTablesActivity.class; + } else if (sendRequest.isSendDocs()) { + next = SendDocsActivity.class; + } else { + next = UploadResultActivity.class; + } + } + Intent intent = new Intent(this, next) + .putExtra(SendRequest.SEND_REQUEST_KEY, sendRequest); + startActivity(intent); + finish(); + } +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/maps/SendMapsAsyncTask.java b/MyTracks/src/com/google/android/apps/mytracks/io/maps/SendMapsAsyncTask.java new file mode 100644 index 000000000..09b20e3f8 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/io/maps/SendMapsAsyncTask.java @@ -0,0 +1,558 @@ +/* + * Copyright 2012 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.maps; + +import com.google.android.apps.mytracks.Constants; +import com.google.android.apps.mytracks.content.MyTracksProviderUtils; +import com.google.android.apps.mytracks.content.Track; +import com.google.android.apps.mytracks.content.Waypoint; +import com.google.android.apps.mytracks.io.gdata.GDataClientFactory; +import com.google.android.apps.mytracks.io.gdata.maps.MapsClient; +import com.google.android.apps.mytracks.io.gdata.maps.MapsConstants; +import com.google.android.apps.mytracks.io.gdata.maps.MapsGDataConverter; +import com.google.android.apps.mytracks.io.gdata.maps.XmlMapsGDataParserFactory; +import com.google.android.apps.mytracks.io.sendtogoogle.SendToGoogleUtils; +import com.google.android.apps.mytracks.stats.DoubleBuffer; +import com.google.android.apps.mytracks.util.LocationUtils; +import com.google.android.apps.mytracks.util.StringUtils; +import com.google.android.apps.mytracks.util.UnitConversions; +import com.google.android.common.gdata.AndroidXmlParserFactory; +import com.google.android.maps.mytracks.R; +import com.google.wireless.gdata.client.GDataClient; +import com.google.wireless.gdata.client.HttpException; +import com.google.wireless.gdata.parser.ParseException; + +import android.accounts.Account; +import android.accounts.AccountManager; +import android.accounts.AuthenticatorException; +import android.accounts.OperationCanceledException; +import android.content.Context; +import android.content.SharedPreferences; +import android.database.Cursor; +import android.location.Location; +import android.os.AsyncTask; +import android.util.Log; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Vector; + +import org.xmlpull.v1.XmlPullParserException; + +/** + * AsyncTask to send a track to Google Maps. + *

+ * IMPORTANT: While this code is Apache-licensed, please notice that usage of + * the Google Maps servers through this API is only allowed for the My Tracks + * application. Other applications looking to upload maps data should look into + * using the Google Fusion Tables API. + * + * @author Jimmy Shih + */ +public class SendMapsAsyncTask extends AsyncTask { + private static final String START_ICON_URL = + "http://maps.google.com/mapfiles/ms/micons/green-dot.png"; + private static final String END_ICON_URL = + "http://maps.google.com/mapfiles/ms/micons/red-dot.png"; + private static final int MAX_POINTS_PER_UPLOAD = 500; + + private static final int PROGRESS_FETCH_MAP_ID = 5; + private static final int PROGRESS_UPLOAD_DATA_MIN = 10; + private static final int PROGRESS_UPLOAD_DATA_MAX = 90; + private static final int PROGRESS_UPLOAD_WAYPOINTS = 95; + private static final int PROGRESS_COMPLETE = 100; + + private static final String TAG = SendMapsAsyncTask.class.getSimpleName(); + + private SendMapsActivity activity; + private final long trackId; + private final Account account; + private final String chooseMapId; + private final Context context; + private final MyTracksProviderUtils myTracksProviderUtils; + private final GDataClient gDataClient; + private final MapsClient mapsClient; + + /** + * True if can retry sending to Google Maps. + */ + private boolean canRetry; + + /** + * True if the AsyncTask has completed. + */ + private boolean completed; + + /** + * True if the result is success. + */ + private boolean success; + + // The following variables are for per upload states + private MapsGDataConverter mapsGDataConverter; + private String authToken; + private String mapId; + int currentSegment; + + public SendMapsAsyncTask( + SendMapsActivity activity, long trackId, Account account, String chooseMapId) { + this.activity = activity; + this.trackId = trackId; + this.account = account; + this.chooseMapId = chooseMapId; + + context = activity.getApplicationContext(); + myTracksProviderUtils = MyTracksProviderUtils.Factory.get(context); + gDataClient = GDataClientFactory.getGDataClient(context); + mapsClient = new MapsClient( + gDataClient, new XmlMapsGDataParserFactory(new AndroidXmlParserFactory())); + + canRetry = true; + completed = false; + success = false; + } + + /** + * Sets the activity associated with this AyncTask. + * + * @param activity the activity. + */ + public void setActivity(SendMapsActivity activity) { + this.activity = activity; + if (completed && activity != null) { + activity.onAsyncTaskCompleted(success); + } + } + + @Override + protected void onPreExecute() { + activity.showProgressDialog(); + } + + @Override + protected Boolean doInBackground(Void... params) { + return doUpload(); + } + + @Override + protected void onProgressUpdate(Integer... values) { + if (activity != null) { + activity.setProgressDialogValue(values[0]); + } + } + + @Override + protected void onPostExecute(Boolean result) { + closeClient(); + success = result; + completed = true; + if (success) { + Track track = myTracksProviderUtils.getTrack(trackId); + if (track != null) { + track.setMapId(mapId); + myTracksProviderUtils.updateTrack(track); + } else { + Log.d(TAG, "No track"); + } + } + if (activity != null) { + activity.onAsyncTaskCompleted(success); + } + } + + @Override + protected void onCancelled() { + closeClient(); + } + + /** + * Closes the gdata client. + */ + private void closeClient() { + if (gDataClient != null) { + gDataClient.close(); + } + } + + /** + * Uploads a track to Google Maps. + * + * @return true if success. + */ + private boolean doUpload() { + // Reset the per upload states + mapsGDataConverter = null; + authToken = null; + mapId = null; + currentSegment = 1; + + // Create a maps gdata converter + try { + mapsGDataConverter = new MapsGDataConverter(); + } catch (XmlPullParserException e) { + Log.d(TAG, "Unable to create a maps gdata converter", e); + return false; + } + + // Get auth token + try { + authToken = AccountManager.get(context).blockingGetAuthToken( + account, MapsConstants.SERVICE_NAME, false); + } catch (OperationCanceledException e) { + Log.d(TAG, "Unable to get auth token", e); + return retryUpload(); + } catch (AuthenticatorException e) { + Log.d(TAG, "Unable to get auth token", e); + return retryUpload(); + } catch (IOException e) { + Log.d(TAG, "Unable to get auth token", e); + return retryUpload(); + } + + // Get the track + Track track = myTracksProviderUtils.getTrack(trackId); + if (track == null) { + Log.d(TAG, "Track is null"); + return false; + } + + // Fetch the mapId, create a new map if necessary + publishProgress(PROGRESS_FETCH_MAP_ID); + if (!fetchSendMapId(track)) { + Log.d("TAG", "Unable to upload all track points"); + return retryUpload(); + } + + // Upload all the track points plus the start and end markers + publishProgress(PROGRESS_UPLOAD_DATA_MIN); + if (!uploadAllTrackPoints(track)) { + Log.d("TAG", "Unable to upload all track points"); + return retryUpload(); + } + + // Upload all the waypoints + publishProgress(PROGRESS_UPLOAD_WAYPOINTS); + if (!uploadWaypoints()) { + Log.d("TAG", "Unable to upload waypoints"); + return false; + } + + publishProgress(PROGRESS_COMPLETE); + return true; + } + + /** + * Retries upload. Invalidates the authToken. If can retry, invokes + * {@link SendMapsAsyncTask#doUpload()}. Returns false if cannot retry. + */ + private boolean retryUpload() { + if (isCancelled()) { + return false; + } + + AccountManager.get(context).invalidateAuthToken(MapsConstants.SERVICE_NAME, authToken); + if (canRetry) { + canRetry = false; + return doUpload(); + } + return false; + } + + /** + * Fetches the {@link SendMapsAsyncTask#mapId} instance variable for + * sending a track to Google Maps. + * + * @param track the Track + * @return true if able to fetch the mapId variable. + */ + private boolean fetchSendMapId(Track track) { + if (isCancelled()) { + return false; + } + + if (chooseMapId != null) { + mapId = chooseMapId; + return true; + } else { + SharedPreferences sharedPreferences = context.getSharedPreferences( + Constants.SETTINGS_NAME, Context.MODE_PRIVATE); + boolean mapPublic = sharedPreferences.getBoolean( + context.getString(R.string.default_map_public_key), true); + try { + String description = track.getCategory() + "\n" + track.getDescription() + "\n" + + StringUtils.getCreatedByMyTracks(context, false); + mapId = SendMapsUtils.createNewMap( + track.getName(), description, mapPublic, mapsClient, authToken); + } catch (ParseException e) { + Log.d(TAG, "Unable to create a new map", e); + return false; + } catch (HttpException e) { + Log.d(TAG, "Unable to create a new map", e); + return false; + } catch (IOException e) { + Log.d(TAG, "Unable to create a new map", e); + return false; + } + return mapId != null; + } + } + + /** + * Uploads all the points in a track. + * + * @param track the track + * @return true if success. + */ + private boolean uploadAllTrackPoints(Track track) { + Cursor locationsCursor = null; + try { + SharedPreferences prefs = context.getSharedPreferences( + Constants.SETTINGS_NAME, Context.MODE_PRIVATE); + boolean metricUnits = prefs.getBoolean(context.getString(R.string.metric_units_key), true); + + locationsCursor = myTracksProviderUtils.getLocationsCursor(trackId, 0, -1, false); + if (locationsCursor == null) { + Log.d(TAG, "Location cursor is null"); + return false; + } + int locationsCount = locationsCursor.getCount(); + List locations = new ArrayList(MAX_POINTS_PER_UPLOAD); + Location lastLocation = null; + + // Limit the number of elevation readings to 250. + int elevationSamplingFrequency = Math.max(1, (int) (locationsCount / 250.0)); + double totalDistance = 0; + DoubleBuffer elevationBuffer = new DoubleBuffer(Constants.ELEVATION_SMOOTHING_FACTOR); + Vector distances = new Vector(); + Vector elevations = new Vector(); + + for (int i = 0; i < locationsCount; i++) { + locationsCursor.moveToPosition(i); + + Location location = myTracksProviderUtils.createLocation(locationsCursor); + locations.add(location); + + int readCount = i + 1; + + if (readCount == 1) { + // Create a start marker + if (!uploadMarker(context.getString(R.string.marker_label_start, track.getName()), "", + START_ICON_URL, location)) { + Log.d(TAG, "Unable to create a start marker"); + return false; + } + } + + // Add to the distances and elevations vectors + if (LocationUtils.isValidLocation(location)) { + // All points go into the smoothing buffer + elevationBuffer.setNext(metricUnits ? location.getAltitude() + : location.getAltitude() * UnitConversions.M_TO_FT); + if (lastLocation != null) { + totalDistance += (double) lastLocation.distanceTo(location); + } + if (readCount % elevationSamplingFrequency == 0) { + distances.add(totalDistance); + elevations.add(elevationBuffer.getAverage()); + } + } + + // Upload periodically + if (readCount % MAX_POINTS_PER_UPLOAD == 0) { + if (!prepareAndUploadPoints(track, locations, false)) { + Log.d(TAG, "Unable to upload points"); + return false; + } + updateProgress(readCount, locationsCount); + locations.clear(); + } + lastLocation = location; + } + + // Do a final upload with the remaining locations + if (!prepareAndUploadPoints(track, locations, true)) { + Log.d(TAG, "Unable to upload points"); + return false; + } + + // Create an end marker + if (lastLocation != null) { + StringUtils stringUtils = new StringUtils(context); + track.setDescription("

" + track.getDescription() + "

" + + stringUtils.generateTrackDescription(track, distances, elevations) + "

"); + if (!uploadMarker(context.getString(R.string.marker_label_end, track.getName()), + track.getDescription(), END_ICON_URL, lastLocation)) { + Log.d(TAG, "Unable to create an end marker"); + return false; + } + } + return true; + } finally { + if (locationsCursor != null) { + locationsCursor.close(); + } + } + } + + /** + * Prepares and uploads a list of locations from a track. + * + * @param track the track + * @param locations the locations from the track + * @param lastBatch true if it is the last batch of locations + * @return true if success. + */ + private boolean prepareAndUploadPoints(Track track, List locations, boolean lastBatch) { + // Prepare locations + ArrayList splitTracks = SendToGoogleUtils.prepareLocations(track, locations); + + // Upload segments + boolean onlyOneSegment = lastBatch && currentSegment == 1 && splitTracks.size() == 1; + for (Track segment : splitTracks) { + if (!onlyOneSegment) { + segment.setName(context.getString( + R.string.send_google_track_part_label, segment.getName(), currentSegment)); + } + if (!uploadSegment(segment.getName(), segment.getLocations())) { + Log.d(TAG, "Unable to upload segment"); + return false; + } + currentSegment++; + } + return true; + } + + /** + * Uploads a marker. + * + * @param title marker title + * @param description marker description + * @param iconUrl marker marker icon + * @param location marker location + * @return true if success. + */ + private boolean uploadMarker( + String title, String description, String iconUrl, Location location) { + if (isCancelled()) { + return false; + } + try { + if (!SendMapsUtils.uploadMarker(mapId, title, description, iconUrl, location, mapsClient, + authToken, mapsGDataConverter)) { + Log.d(TAG, "Unable to upload marker"); + return false; + } + } catch (ParseException e) { + Log.d(TAG, "Unable to upload marker", e); + return false; + } catch (HttpException e) { + Log.d(TAG, "Unable to upload marker", e); + return false; + } catch (IOException e) { + Log.d(TAG, "Unable to upload marker", e); + return false; + } + return true; + } + + /** + * Uploads a segment + * + * @param title segment title + * @param locations segment locations + * @return true if success + */ + private boolean uploadSegment(String title, ArrayList locations) { + if (isCancelled()) { + return false; + } + try { + if (!SendMapsUtils.uploadSegment( + mapId, title, locations, mapsClient, authToken, mapsGDataConverter)) { + Log.d(TAG, "Unable to upload track points"); + return false; + } + } catch (ParseException e) { + Log.d(TAG, "Unable to upload track points", e); + return false; + } catch (HttpException e) { + Log.d(TAG, "Unable to upload track points", e); + return false; + } catch (IOException e) { + Log.d(TAG, "Unable to upload track points", e); + return false; + } + return true; + } + + /** + * Uploads all the waypoints. + * + * @return true if success. + */ + private boolean uploadWaypoints() { + Cursor cursor = null; + try { + cursor = myTracksProviderUtils.getWaypointsCursor( + trackId, 0, Constants.MAX_LOADED_WAYPOINTS_POINTS); + if (cursor != null && cursor.moveToFirst()) { + // This will skip the first waypoint (it carries the stats for the + // track). + while (cursor.moveToNext()) { + if (isCancelled()) { + return false; + } + Waypoint waypoint = myTracksProviderUtils.createWaypoint(cursor); + try { + if (!SendMapsUtils.uploadWaypoint( + mapId, waypoint, mapsClient, authToken, mapsGDataConverter)) { + Log.d(TAG, "Unable to upload waypoint"); + return false; + } + } catch (ParseException e) { + Log.d(TAG, "Unable to upload waypoint", e); + return false; + } catch (HttpException e) { + Log.d(TAG, "Unable to upload waypoint", e); + return false; + } catch (IOException e) { + Log.d(TAG, "Unable to upload waypoint", e); + return false; + } + } + } + return true; + } finally { + if (cursor != null) { + cursor.close(); + } + } + } + + /** + * Updates the progress based on the number of locations uploaded. + * + * @param uploaded the number of uploaded locations + * @param total the number of total locations + */ + private void updateProgress(int uploaded, int total) { + double totalPercentage = uploaded / total; + double scaledPercentage = totalPercentage + * (PROGRESS_UPLOAD_DATA_MAX - PROGRESS_UPLOAD_DATA_MIN) + PROGRESS_UPLOAD_DATA_MIN; + publishProgress((int) scaledPercentage); + } +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/maps/SendMapsUtils.java b/MyTracks/src/com/google/android/apps/mytracks/io/maps/SendMapsUtils.java new file mode 100644 index 000000000..db769c366 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/io/maps/SendMapsUtils.java @@ -0,0 +1,227 @@ +/* + * Copyright 2012 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.maps; + +import com.google.android.apps.mytracks.content.Track; +import com.google.android.apps.mytracks.content.Waypoint; +import com.google.android.apps.mytracks.io.gdata.maps.MapsClient; +import com.google.android.apps.mytracks.io.gdata.maps.MapsFeature; +import com.google.android.apps.mytracks.io.gdata.maps.MapsGDataConverter; +import com.google.android.apps.mytracks.io.gdata.maps.MapsMapMetadata; +import com.google.android.maps.GeoPoint; +import com.google.common.annotations.VisibleForTesting; +import com.google.wireless.gdata.client.HttpException; +import com.google.wireless.gdata.data.Entry; +import com.google.wireless.gdata.parser.ParseException; + +import android.location.Location; +import android.text.TextUtils; +import android.util.Log; + +import java.io.IOException; +import java.util.ArrayList; + +/** + * Utilities for sending a track to Google Maps. + * + * @author jshih@google.com (Jimmy Shih) + */ +public class SendMapsUtils { + + private static final String EMPTY_TITLE = "-"; + private static final int LINE_COLOR = 0x80FF0000; + private static final String TAG = SendMapsUtils.class.getSimpleName(); + + private SendMapsUtils() {} + + /** + * Gets the Google Maps url for a track. + * + * @param track the track + * @return the url if available. + */ + public static String getMapUrl(Track track) { + if (track == null || track.getMapId() == null) { + Log.e(TAG, "Invalid track"); + return null; + } + + return MapsClient.buildMapUrl(track.getMapId()); + } + + /** + * Creates a new Google Map. + * + * @param title title of the map + * @param description description of the map + * @param isPublic true if the map can be public + * @param mapsClient the maps client + * @param authToken the auth token + * @return map id of the created map if successful. + */ + public static String createNewMap( + String title, String description, boolean isPublic, MapsClient mapsClient, String authToken) + throws ParseException, HttpException, IOException { + String mapFeed = MapsClient.getMapsFeed(); + MapsMapMetadata metaData = new MapsMapMetadata(); + metaData.setTitle(title); + metaData.setDescription(description); + metaData.setSearchable(isPublic); + Entry entry = MapsGDataConverter.getMapEntryForMetadata(metaData); + Entry result = mapsClient.createEntry(mapFeed, authToken, entry); + if (result == null) { + Log.d(TAG, "No result when creating a new map"); + return null; + } + return MapsClient.getMapIdFromMapEntryId(result.getId()); + } + + /** + * Uploads a start/end marker to Google Maps. + * + * @param mapId the map id + * @param title the marker title + * @param description the marker description + * @param iconUrl the marker icon URL + * @param location the marker location + * @param mapsClient the maps client + * @param authToken the auth token + * @param mapsGDataConverter the maps gdata converter + * @return true if success. + */ + public static boolean uploadMarker(String mapId, String title, String description, String iconUrl, + Location location, MapsClient mapsClient, String authToken, + MapsGDataConverter mapsGDataConverter) throws ParseException, HttpException, IOException { + String featuresFeed = MapsClient.getFeaturesFeed(mapId); + MapsFeature mapsFeature = buildMapsMarkerFeature( + title, description, iconUrl, getGeoPoint(location)); + Entry entry = mapsGDataConverter.getEntryForFeature(mapsFeature); + try { + mapsClient.createEntry(featuresFeed, authToken, entry); + } catch (IOException e) { + // Retry once (often IOException is thrown on a timeout) + Log.d(TAG, "Retry upload marker", e); + mapsClient.createEntry(featuresFeed, authToken, entry); + } + return true; + } + + /** + * Uploads a waypoint as a marker feature to Google Maps. + * + * @param mapId the map id + * @param waypoint the waypoint + * @param mapsClient the maps client + * @param authToken the auth token + * @param mapsGDataConverter the maps gdata converter + * @return true if success. + */ + public static boolean uploadWaypoint(String mapId, Waypoint waypoint, MapsClient mapsClient, + String authToken, MapsGDataConverter mapsGDataConverter) + throws ParseException, HttpException, IOException { + String featuresFeed = MapsClient.getFeaturesFeed(mapId); + MapsFeature feature = buildMapsMarkerFeature(waypoint.getName(), waypoint.getDescription(), + waypoint.getIcon(), getGeoPoint(waypoint.getLocation())); + Entry entry = mapsGDataConverter.getEntryForFeature(feature); + try { + mapsClient.createEntry(featuresFeed, authToken, entry); + } catch (IOException e) { + // Retry once (often IOException is thrown on a timeout) + Log.d(TAG, "Retry upload waypoint", e); + mapsClient.createEntry(featuresFeed, authToken, entry); + } + return true; + } + + /** + * Uploads a segment as a line feature to Google Maps. + * + * @param mapId the map id + * @param title the segment title + * @param locations the segment locations + * @param mapsClient the maps client + * @param authToken the auth token + * @param mapsGDataConverter the maps gdata converter + * @return true if success. + */ + public static boolean uploadSegment(String mapId, String title, ArrayList locations, + MapsClient mapsClient, String authToken, MapsGDataConverter mapsGDataConverter) + throws ParseException, HttpException, IOException { + String featuresFeed = MapsClient.getFeaturesFeed(mapId); + Entry entry = mapsGDataConverter.getEntryForFeature(buildMapsLineFeature(title, locations)); + try { + mapsClient.createEntry(featuresFeed, authToken, entry); + } catch (IOException e) { + // Retry once (often IOException is thrown on a timeout) + Log.d(TAG, "Retry upload track points", e); + mapsClient.createEntry(featuresFeed, authToken, entry); + } + return true; + } + + /** + * Builds a map marker feature. + * + * @param title feature title + * @param description the feature description + * @param iconUrl the feature icon URL + * @param geoPoint the marker + */ + @VisibleForTesting + static MapsFeature buildMapsMarkerFeature( + String title, String description, String iconUrl, GeoPoint geoPoint) { + MapsFeature mapsFeature = new MapsFeature(); + mapsFeature.setType(MapsFeature.MARKER); + mapsFeature.generateAndroidId(); + // Feature must have a name (otherwise GData upload may fail) + mapsFeature.setTitle(TextUtils.isEmpty(title) ? EMPTY_TITLE : title); + mapsFeature.setDescription(description.replaceAll("\n", "
")); + mapsFeature.setIconUrl(iconUrl); + mapsFeature.addPoint(geoPoint); + return mapsFeature; + } + + /** + * Builds a maps line feature from a set of locations. + * + * @param title the feature title + * @param locations set of locations + */ + @VisibleForTesting + static MapsFeature buildMapsLineFeature(String title, ArrayList locations) { + MapsFeature mapsFeature = new MapsFeature(); + mapsFeature.setType(MapsFeature.LINE); + mapsFeature.generateAndroidId(); + // Feature must have a name (otherwise GData upload may fail) + mapsFeature.setTitle(TextUtils.isEmpty(title) ? EMPTY_TITLE : title); + mapsFeature.setColor(LINE_COLOR); + for (Location location : locations) { + mapsFeature.addPoint(getGeoPoint(location)); + } + return mapsFeature; + } + + /** + * Gets a {@link GeoPoint} from a {@link Location}. + * + * @param location the location + */ + @VisibleForTesting + static GeoPoint getGeoPoint(Location location) { + return new GeoPoint( + (int) (location.getLatitude() * 1E6), (int) (location.getLongitude() * 1E6)); + } +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/AccountChooserActivity.java b/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/AccountChooserActivity.java index a6e22bafe..0af149c90 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/AccountChooserActivity.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/AccountChooserActivity.java @@ -16,7 +16,11 @@ package com.google.android.apps.mytracks.io.sendtogoogle; import com.google.android.apps.mytracks.Constants; -import com.google.android.apps.mytracks.util.ApiFeatures; +import com.google.android.apps.mytracks.io.docs.SendDocsActivity; +import com.google.android.apps.mytracks.io.fusiontables.SendFusionTablesActivity; +import com.google.android.apps.mytracks.io.maps.ChooseMapActivity; +import com.google.android.apps.mytracks.io.maps.SendMapsActivity; +import com.google.android.apps.mytracks.util.ApiAdapterFactory; import com.google.android.maps.mytracks.R; import android.accounts.Account; @@ -32,46 +36,34 @@ import android.content.SharedPreferences.Editor; import android.os.Bundle; /** - * A chooser to select an account. To be called with - * {@link Activity#startActivityForResult(Intent, int)}. Returns - * {@link Activity#RESULT_CANCELED} if the user cancels the activity. Otherwise, - * returns {@link Activity#RESULT_OK} with an intent containing the selected - * account. The selected account is stored as an {@link Account} in the - * {@link AccountChooserActivity#ACCOUNT} item of the intent. The selected - * account can be null. + * A chooser to select an account. * * @author jshih@google.com (Jimmy Shih) */ public class AccountChooserActivity extends Activity { - /** - * Key for storing an {@link Account} in an {@link Intent}. - */ - public static final String ACCOUNT = "account"; - private static final int NO_ACCOUNT_DIALOG = 1; private static final int CHOOSE_ACCOUNT_DIALOG = 2; + private SendRequest sendRequest; private Account[] accounts; private int selectedAccountIndex; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + sendRequest = getIntent().getParcelableExtra(SendRequest.SEND_REQUEST_KEY); accounts = AccountManager.get(this).getAccountsByType(Constants.ACCOUNT_TYPE); if (accounts.length == 1) { - Intent intent = new Intent(); - intent.putExtra(ACCOUNT, accounts[0]); - setResult(RESULT_OK, intent); - finish(); + startNextActivity(accounts[0]); return; } SharedPreferences prefs = getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE); String preferredAccount = prefs.getString(getString(R.string.preferred_account_key), ""); - selectedAccountIndex = -1; + selectedAccountIndex = 0; for (int i = 0; i < accounts.length; i++) { if (accounts[i].name.equals(preferredAccount)) { selectedAccountIndex = i; @@ -102,15 +94,11 @@ public class AccountChooserActivity extends Activity { builder.setOnCancelListener(new DialogInterface.OnCancelListener() { @Override public void onCancel(DialogInterface dialog) { - setResult(RESULT_CANCELED); finish(); } }); builder.setPositiveButton(R.string.generic_ok, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { - Intent intent = new Intent(); - intent.putExtra(ACCOUNT, (Account) null); - setResult(RESULT_OK, intent); finish(); } }); @@ -134,31 +122,23 @@ public class AccountChooserActivity extends Activity { builder.setOnCancelListener(new DialogInterface.OnCancelListener() { @Override public void onCancel(DialogInterface dialog) { - setResult(RESULT_CANCELED); finish(); } }); builder.setNegativeButton(R.string.generic_cancel, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { - setResult(RESULT_CANCELED); finish(); } }); builder.setPositiveButton(R.string.generic_ok, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { - Account account = null; - if (selectedAccountIndex != -1) { - account = accounts[selectedAccountIndex]; - SharedPreferences prefs = getSharedPreferences( - Constants.SETTINGS_NAME, Context.MODE_PRIVATE); - Editor editor = prefs.edit(); - editor.putString(getString(R.string.preferred_account_key), account.name); - ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges(editor); - } - Intent intent = new Intent(); - intent.putExtra(ACCOUNT, account); - setResult(RESULT_OK, intent); - finish(); + Account account = accounts[selectedAccountIndex]; + SharedPreferences prefs = getSharedPreferences( + Constants.SETTINGS_NAME, Context.MODE_PRIVATE); + Editor editor = prefs.edit(); + editor.putString(getString(R.string.preferred_account_key), account.name); + ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor); + startNextActivity(account); } }); return builder.create(); @@ -166,4 +146,38 @@ public class AccountChooserActivity extends Activity { return null; } } -} + + /** + * Starts the next activity. If + *

+ * sendMaps and newMap -> {@link SendMapsActivity} + *

+ * sendMaps and !newMap -> {@link ChooseMapActivity} + *

+ * !sendMaps && sendFusionTables -> {@link SendFusionTablesActivity} + *

+ * !sendMaps && !sendFusionTables && sendDocs -> {@link SendDocsActivity} + *

+ * !sendMaps && !sendFusionTables && !sendDocs -> {@link UploadResultActivity} + * + * @param account the chosen account + */ + private void startNextActivity(Account account) { + sendRequest.setAccount(account); + + Class next; + if (sendRequest.isSendMaps()) { + next = sendRequest.isNewMap() ? SendMapsActivity.class : ChooseMapActivity.class; + } else if (sendRequest.isSendFusionTables()) { + next = SendFusionTablesActivity.class; + } else if (sendRequest.isSendDocs()) { + next = SendDocsActivity.class; + } else { + next = UploadResultActivity.class; + } + Intent intent = new Intent(this, next) + .putExtra(SendRequest.SEND_REQUEST_KEY, sendRequest); + startActivity(intent); + finish(); + } +} \ No newline at end of file diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/ResultDialogFactory.java b/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/ResultDialogFactory.java deleted file mode 100644 index ec03581a5..000000000 --- a/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/ResultDialogFactory.java +++ /dev/null @@ -1,109 +0,0 @@ -/* - * 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 com.google.android.maps.mytracks.R; - -import android.app.Activity; -import android.app.AlertDialog; -import android.app.Dialog; -import android.content.DialogInterface; -import android.view.View; -import android.widget.ListView; - -import java.util.List; - -/** - * Creates the dialog used to display the results of sending track data to - * Google. The dialog lists the services to which data was uploaded, along - * with an indicator of success/failure. If possible, a button is displayed - * offering to share the uploaded track. - * - * Implementation note: This class is a factory, rather than a {@link Dialog} - * subclass, because alert dialogs have to be created with - * {@link AlertDialog.Builder}. Attempts to subclass {@link AlertDialog} - * directly have been unsuccessful, as {@link AlertDialog.Builder} uses a - * private subclass to implement most of the interesting behavior. - * - * @author Matthew Simmons - */ -public class ResultDialogFactory { - private ResultDialogFactory() {} - - /** - * Create a send-to-Google result dialog. The caller is responsible for - * showing it. - * - * @param activity the activity associated with the dialog - * @param results the results to be displayed in the dialog - * @param onOkClickListener the listener to invoke if the OK button is - * clicked - * @param onShareClickListener the listener to invoke if the Share button is - * clicked. If no share listener is provided, the Share button will not - * be displayed. - * @return the created dialog - */ - public static AlertDialog makeDialog(Activity activity, List results, - DialogInterface.OnClickListener onOkClickListener, - DialogInterface.OnClickListener onShareClickListener, - DialogInterface.OnCancelListener onCancelListener) { - boolean success = true; - for (SendResult result : results) { - if (!result.isSuccess()) { - success = false; - break; - } - } - - AlertDialog.Builder builder = new AlertDialog.Builder(activity) - .setView(makeDialogContent(activity, results, success)); - - if (success) { - builder.setTitle(R.string.generic_success_title); - builder.setIcon(android.R.drawable.ic_dialog_info); - } else { - builder.setTitle(R.string.generic_error_title); - builder.setIcon(android.R.drawable.ic_dialog_alert); - } - - builder.setPositiveButton(activity.getString(R.string.generic_ok), onOkClickListener); - if (onShareClickListener != null) { - builder.setNegativeButton(activity.getString(R.string.send_google_result_share_url), - onShareClickListener); - } - - builder.setOnCancelListener(onCancelListener); - - return builder.create(); - } - - private static View makeDialogContent(Activity activity, List results, - boolean success) { - ResultListAdapter resultListAdapter = new ResultListAdapter(activity, - R.layout.send_to_google_result_list_item, results); - - View content = activity.getLayoutInflater().inflate(R.layout.send_to_google_result, null); - ListView resultList = (ListView) content.findViewById(R.id.send_to_google_result_list); - resultList.setAdapter(resultListAdapter); - - content.findViewById(R.id.send_to_google_result_comment) - .setVisibility(success ? View.VISIBLE : View.GONE); - content.findViewById(R.id.send_to_google_result_error) - .setVisibility(success ? View.GONE : View.VISIBLE); - - return content; - } -} diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/ResultListAdapter.java b/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/ResultListAdapter.java deleted file mode 100644 index 68653067b..000000000 --- a/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/ResultListAdapter.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * 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 com.google.android.maps.mytracks.R; - -import android.content.Context; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ArrayAdapter; -import android.widget.ImageView; -import android.widget.ListAdapter; -import android.widget.TextView; - -import java.util.List; - -/** - * This class implements the {@link ListAdapter} used by the send-to-Google - * result dialog created by {@link ResultDialogFactory}. It generates views - * for each entry in an array of {@link SendResult} instances. - * @author Matthew Simmons - */ -class ResultListAdapter extends ArrayAdapter { - public ResultListAdapter(Context context, int textViewResourceId, List results) { - super(context, textViewResourceId, results); - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - View view = convertView; - if (view == null) { - LayoutInflater inflater - = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); - view = inflater.inflate(R.layout.send_to_google_result_list_item, null); - } - - SendResult result = getItem(position); - setImage(view, result.isSuccess() ? R.drawable.success : R.drawable.failure); - setName(view, result.getType().getServiceName()); - setUrl(view, result.getType().getServiceUrl()); - return view; - } - - @Override - public boolean areAllItemsEnabled() { - // We don't want the displayed items to be clickable - return false; - } - - // The following protected methods exist to be overridden for testing - // purposes. Doing so insulates the test class from the details of the - // layout. - - protected void setImage(View content, int drawableId) { - ImageView imageView = (ImageView) content.findViewById(R.id.send_to_google_result_icon); - imageView.setImageDrawable(getContext().getResources().getDrawable(drawableId)); - } - - protected void setName(View content, int nameId) { - setTextViewText(content, R.id.send_to_google_result_name, nameId); - } - - protected void setUrl(View content, int urlId) { - setTextViewText(content, R.id.send_to_google_result_url, urlId); - } - - private void setTextViewText(View content, int viewId, int textId) { - TextView textView = (TextView) content.findViewById(viewId); - textView.setText(getContext().getString(textId)); - } -}; diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/SendActivity.java b/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/SendActivity.java deleted file mode 100644 index dad6abe82..000000000 --- a/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/SendActivity.java +++ /dev/null @@ -1,814 +0,0 @@ -/* - * 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.apps.analytics.GoogleAnalyticsTracker; -import com.google.android.apps.mytracks.Constants; -import com.google.android.apps.mytracks.MapsList; -import com.google.android.apps.mytracks.ProgressIndicator; -import com.google.android.apps.mytracks.content.MyTracksProviderUtils; -import com.google.android.apps.mytracks.content.Track; -import com.google.android.apps.mytracks.io.AuthManager; -import com.google.android.apps.mytracks.io.AuthManager.AuthCallback; -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.SendToMaps; -import com.google.android.apps.mytracks.io.maps.MapsConstants; -import com.google.android.apps.mytracks.io.maps.MapsFacade; -import com.google.android.apps.mytracks.util.SystemUtils; -import com.google.android.maps.mytracks.R; - -import android.accounts.Account; -import android.app.Activity; -import android.app.Dialog; -import android.app.ProgressDialog; -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.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 { - - // Items in the intent that starts the activity. - public static final String TRACK_ID = "trackId"; - public static final String SHARE_URL = "shareUrl"; - public static final String SEND_MAPS = "sendMaps"; - public static final String SEND_FUSION_TABLES = "sendFusionTables"; - public static final String SEND_DOCS = "sendDocs"; - public static final String CREATE_MAP = "createMap"; - - // Keys for saved state variables. - private static final String STATE_ACCOUNT = "account"; - private static final String STATE_STATE = "state"; - 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_TABLE_ID = "tableId"; - private static final String STATE_MAP_ID = "mapId"; - - /** States for the state machine that defines the upload process. */ - private enum SendState { - CHOOSE_ACCOUNT, - 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 PROGRESS_DIALOG = 1; - /* @VisibleForTesting */ - static final int DONE_DIALOG = 2; - - // UI - private ProgressDialog progressDialog; - - // Set in Activity.onCreate - private MyTracksProviderUtils providerUtils; - private SharedPreferences sharedPreferences; - private GoogleAnalyticsTracker tracker; - - // Send request information. Set by the intent that starts the activity. - private long sendTrackId; - private boolean shareRequest; - private boolean sendToMaps; - private boolean sendToMapsNewMap; - private boolean sendToFusionTables; - private boolean sendToDocs; - - // Authentication information. - private Account account; - private HashMap authMap = new HashMap(); - - // Current sending state. - private SendState currentState; - - // Send result information. Used by the results dialog. - private boolean sendToMapsSuccess; - private boolean sendToFusionTablesSuccess; - private boolean sendToDocsSuccess; - private String sendToMapsMapId; - private String sendToFusionTablesTableId; - - 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, Context.MODE_PRIVATE); - - tracker = GoogleAnalyticsTracker.getInstance(); - // Start the tracker in manual dispatch mode... - tracker.start(getString(R.string.my_tracks_analytics_id), - getApplicationContext()); - tracker.setProductVersion("android-mytracks", - SystemUtils.getMyTracksVersion(this)); - - resetState(); - - if (savedInstanceState != null) { - restoreInstanceState(savedInstanceState); - } - - // If we had the instance restored after it was done, reset it. - if (currentState == SendState.DONE) { - resetState(); - } - - // Only consider the intent if we're not restoring from a previous state. - if (currentState == SendState.CHOOSE_ACCOUNT) { - if (!handleIntent()) { - finish(); - return; - } - } - - // Execute the state machine, at the start or restored state. - Log.w(TAG, "Starting at state " + currentState); - executeStateMachine(currentState); - } - - private boolean handleIntent() { - Intent intent = getIntent(); - sendTrackId = intent.getLongExtra(TRACK_ID, -1L); - if (sendTrackId == -1L) { - return false; - } - sendToMaps = intent.getBooleanExtra(SEND_MAPS, false); - sendToFusionTables = intent.getBooleanExtra(SEND_FUSION_TABLES, false); - sendToDocs = intent.getBooleanExtra(SEND_DOCS, false); - if (!sendToMaps && !sendToFusionTables && !sendToDocs) { - return false; - } - sendToMapsNewMap = intent.getBooleanExtra(CREATE_MAP, true); - return true; - } - - @Override - protected Dialog onCreateDialog(int id) { - switch (id) { - case PROGRESS_DIALOG: - return createProgressDialog(); - case DONE_DIALOG: - return createDoneDialog(); - default: - return null; - } - } - - private void restoreInstanceState(Bundle savedInstanceState) { - account = savedInstanceState.getParcelable(STATE_ACCOUNT); - - currentState = SendState.values()[savedInstanceState.getInt(STATE_STATE)]; - - sendToMapsSuccess = savedInstanceState.getBoolean(STATE_MAPS_SUCCESS); - sendToFusionTablesSuccess = savedInstanceState.getBoolean(STATE_FUSION_SUCCESS); - sendToDocsSuccess = savedInstanceState.getBoolean(STATE_DOCS_SUCCESS); - sendToMapsMapId = savedInstanceState.getString(STATE_MAP_ID); - sendToFusionTablesTableId = savedInstanceState.getString(STATE_TABLE_ID); - } - - @Override - protected void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - outState.putParcelable(STATE_ACCOUNT, account); - - outState.putInt(STATE_STATE, currentState.ordinal()); - - outState.putBoolean(STATE_MAPS_SUCCESS, sendToMapsSuccess); - outState.putBoolean(STATE_FUSION_SUCCESS, sendToFusionTablesSuccess); - outState.putBoolean(STATE_DOCS_SUCCESS, sendToDocsSuccess); - outState.putString(STATE_MAP_ID, sendToMapsMapId); - outState.putString(STATE_TABLE_ID, sendToFusionTablesTableId); - } - - @Override - protected void onStop() { - Log.d(TAG, "SendActivity.onStop, state=" + 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) { - 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 CHOOSE_ACCOUNT: - return chooseAccount(); - 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 chooseAccount() { - Intent intent = new Intent(this, AccountChooserActivity.class); - startActivityForResult(intent, Constants.CHOOSE_ACCOUNT); - 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 (sendToMaps) { - return SendState.AUTHENTICATE_MAPS; - } else if (sendToFusionTables) { - return SendState.AUTHENTICATE_FUSION_TABLES; - } else if (sendToDocs) { - 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.generic_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(getAuthenticatingProgressMessage(SendType.MAPS)); - authenticate(Constants.AUTHENTICATE_TO_MAPS, MapsConstants.SERVICE_NAME); - // AUTHENTICATE_TO_MAPS callback calls sendToGoogleMaps - return SendState.NOT_READY; - } - - private SendState pickMap() { - if (!sendToMapsNewMap) { - // Ask the user to choose a map to upload into - Intent listIntent = new Intent(this, MapsList.class); - listIntent.putExtra(MapsList.EXTRA_ACCOUNT_NAME, account.name); - listIntent.putExtra(MapsList.EXTRA_ACCOUNT_TYPE, account.type); - 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"); - - SendToMaps.OnSendCompletedListener onCompletion = new SendToMaps.OnSendCompletedListener() { - @Override - public void onSendCompleted(String mapId, boolean success) { - sendToMapsSuccess = success; - if (sendToMapsSuccess) { - sendToMapsMapId = 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 (sendToMapsMapId == null) { - sendToMapsMapId = SendToMaps.NEW_MAP_ID; - } - - final SendToMaps sender = new SendToMaps(this, sendToMapsMapId, - getAuthManager(MapsConstants.SERVICE_NAME), sendTrackId, this /*progressIndicator*/, - onCompletion); - - new Thread(sender, "SendToMaps").start(); - - return SendState.NOT_READY; - } - - private SendState onSendToGoogleMapsDone() { - if (sendToFusionTables) { - return SendState.AUTHENTICATE_FUSION_TABLES; - } else if (sendToDocs) { - return SendState.AUTHENTICATE_DOCS; - } else { - return SendState.SHOW_RESULTS; - } - } - - private SendState authenticateToFusionTables() { - progressDialog.setProgress(0); - progressDialog.setMessage(getAuthenticatingProgressMessage(SendType.FUSION_TABLES)); - 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) { - 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, getAuthManager(SendToFusionTables.SERVICE_ID), sendTrackId, - this /* progressIndicator */, onCompletion); - - new Thread(sender, "SendToFusionTables").start(); - - return SendState.NOT_READY; - } - - private SendState onSendToFusionTablesDone() { - if (sendToDocs) { - return SendState.AUTHENTICATE_DOCS; - } else { - return SendState.SHOW_RESULTS; - } - } - - private SendState authenticateToGoogleDocs() { - setProgressValue(0); - setProgressMessage(getAuthenticatingProgressMessage(SendType.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(getAuthenticatingProgressMessage(SendType.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); - String format = getString(R.string.send_google_progress_sending); - String serviceName = getString(SendType.DOCS.getServiceName()); - setProgressMessage(String.format(format, serviceName)); - final SendToDocs sender = new SendToDocs(this, - getAuthManager(SendToDocs.GDATA_SERVICE_NAME_TRIX), - getAuthManager(SendToDocs.GDATA_SERVICE_NAME_DOCLIST), - this); - Runnable onCompletion = new Runnable() { - public void run() { - setProgressValue(100); - 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); - // 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 results = makeSendToGoogleResults(); - final boolean canShare = sendToFusionTablesTableId != null || sendToMapsMapId != 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 && shareRequest) - ? doShareListener : finishListener; - DialogInterface.OnClickListener onShareListener = (canShare && !shareRequest) - ? doShareListener : null; - - return ResultDialogFactory.makeDialog( - this, results, onOkListener, onShareListener, finishOnCancelListener); - } - - private SendState onAllDone() { - Log.d(TAG, "All sending done."); - removeDialog(PROGRESS_DIALOG); - removeDialog(DONE_DIALOG); - progressDialog = null; - finish(); - return SendState.DONE; - } - - private SendState shareLink() { - String url = null; - if (sendToMaps && sendToMapsSuccess) { - // Prefer a link to Maps - url = MapsFacade.buildMapUrl(sendToMapsMapId); - } else if (sendToFusionTables && sendToFusionTablesSuccess) { - // Otherwise try using the link to fusion tables - url = getFusionTablesUrl(sendTrackId); - } - - if (url != null) { - shareLinkToMap(url); - } else { - Log.w(TAG, "Failed to share link"); - } - - return SendState.FINISH; - } - - /** - * Shares a link to a 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_track_url_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_track_subject).toString()); - shareIntent.putExtra(Intent.EXTRA_TEXT, msg); - startActivity(Intent.createChooser(shareIntent, - getResources().getText(R.string.share_track_picker_title).toString())); - } - - protected String getFusionTablesUrl(long trackId) { - Track track = providerUtils.getTrack(trackId); - return SendToFusionTables.getMapVisualizationUrl(track); - } - - /** - * Creates a list of {@link SendResult} instances based on the set of - * services selected in {@link SendDialog} and the results as known to - * this class. - */ - private List makeSendToGoogleResults() { - List results = new ArrayList(); - if (sendToMaps) { - results.add(new SendResult(SendType.MAPS, sendToMapsSuccess)); - } - if (sendToFusionTables) { - results.add(new SendResult(SendType.FUSION_TABLES, - sendToFusionTablesSuccess)); - } - if (sendToDocs) { - 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) { - AuthManager authManager = getAuthManager(service); - authManager.doLogin(new AuthCallback() { - @Override - public void onAuthResult(boolean success) { - Log.i(TAG, "Login success for " + service + ": " + success); - if (!success) { - executeStateMachine(SendState.SHOW_RESULTS); - return; - } - - onLoginSuccess(requestCode); - } - }, account); - } - - @Override - public void onActivityResult(int requestCode, int resultCode, - final Intent results) { - SendState nextState = null; - switch (requestCode) { - 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")) { - sendToMapsMapId = results.getStringExtra("mapid"); - } - nextState = SendState.SEND_TO_MAPS; - } else { - nextState = SendState.FINISH; - } - break; - } - case Constants.CHOOSE_ACCOUNT: { - if (resultCode == RESULT_CANCELED) { - nextState = SendState.FINISH; - break; - } - account = results.getParcelableExtra(AccountChooserActivity.ACCOUNT); - if (account == null) { - nextState = SendState.FINISH; - break; - } - nextState = SendState.START; - break; - } - default: { - Log.e(TAG, "Unrequested result: " + requestCode); - return; - } - } - - if (nextState != null) { - executeStateMachine(nextState); - } - } - - private void onLoginSuccess(int requestCode) { - SendState nextState; - switch (requestCode) { - case Constants.AUTHENTICATE_TO_MAPS: - // Authenticated with Google Maps - nextState = SendState.PICK_MAP; - break; - case Constants.AUTHENTICATE_TO_FUSION_TABLES: - // Authenticated with Google Fusion Tables - nextState = SendState.SEND_TO_FUSION_TABLES; - break; - case Constants.AUTHENTICATE_TO_DOCLIST: - // Authenticated with Google Docs - nextState = SendState.AUTHENTICATE_TRIX; - break; - case Constants.AUTHENTICATE_TO_TRIX: - // Authenticated with Trix - nextState = SendState.SEND_TO_DOCS; - break; - - default: { - Log.e(TAG, "Unrequested login code: " + requestCode); - return; - } - } - - executeStateMachine(nextState); - } - - private AuthManager getAuthManager(String service) { - AuthManager authManager = authMap.get(service); - if (authManager == null) { - authManager = AuthManagerFactory.getAuthManager(this, - Constants.GET_LOGIN, - null, - true, - service); - authMap.put(service, authManager); - } - return authManager; - } - /** - * Resets status information for sending to Maps/Fusion Tables/Docs. - */ - private void resetState() { - account = null; - authMap.clear(); - currentState = SendState.CHOOSE_ACCOUNT; - sendToMapsSuccess = false; - sendToFusionTablesSuccess = false; - sendToDocsSuccess = false; - sendToMapsMapId = null; - sendToFusionTablesTableId = null; - } - - /** - * Gets a progress message indicating My Tracks is authenticating to a - * service. - * - * @param type the type of service - */ - private String getAuthenticatingProgressMessage(SendType type) { - String format = getString(R.string.send_google_progress_authenticating); - String serviceName = getString(type.getServiceName()); - return String.format(format, serviceName); - } - - @Override - public void setProgressMessage(final String message) { - runOnUiThread(new Runnable() { - public void run() { - if (progressDialog != null) { - progressDialog.setMessage(message); - } - } - }); - } - - @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(""); - } -} diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/SendRequest.java b/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/SendRequest.java new file mode 100644 index 000000000..a5be3b24d --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/SendRequest.java @@ -0,0 +1,294 @@ +/* + * Copyright 2012 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 android.accounts.Account; +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Send request states for sending a track to Google Maps, Google Fusion Tables, + * and Google Docs. + * + * @author Jimmy Shih + */ +public class SendRequest implements Parcelable { + + public static final String SEND_REQUEST_KEY = "sendRequest"; + + private long trackId = -1L; + private boolean showMaps = false; + private boolean showFusionTables = false; + private boolean showDocs = false; + private boolean sendMaps = false; + private boolean sendFusionTables = false; + private boolean sendDocs = false; + private boolean newMap = false; + private Account account = null; + private String mapId = null; + private boolean mapsSuccess = false; + private boolean docsSuccess = false; + private boolean fusionTablesSuccess = false; + + /** + * Creates a new send request. + * + * @param trackId the track id + * @param showMaps true to show the Google Maps option + * @param showFusionTables true to show the Google Fusion Tables option + * @param showDocs true to show the Google Docs option + */ + public SendRequest(long trackId, boolean showMaps, boolean showFusionTables, boolean showDocs) { + this.trackId = trackId; + this.showMaps = showMaps; + this.showFusionTables = showFusionTables; + this.showDocs = showDocs; + } + + /** + * Get the track id. + */ + public long getTrackId() { + return trackId; + } + + /** + * True if showing the send to Google Maps option. + */ + public boolean isShowMaps() { + return showMaps; + } + + /** + * True if showing the send to Google Fusion Tables option. + */ + public boolean isShowFusionTables() { + return showFusionTables; + } + + /** + * True if showing the send to Google Docs option. + */ + public boolean isShowDocs() { + return showDocs; + } + + /** + * True if showing all the send options. + */ + public boolean isShowAll() { + return showMaps && showFusionTables && showDocs; + } + + /** + * True if the user has selected the send to Google Maps option. + */ + public boolean isSendMaps() { + return sendMaps; + } + + /** + * Sets the send to Google Maps option. + * + * @param sendMaps true if the user has selected the send to Google Maps + * option + */ + public void setSendMaps(boolean sendMaps) { + this.sendMaps = sendMaps; + } + + /** + * True if the user has selected the send to Google Fusion Tables option. + */ + public boolean isSendFusionTables() { + return sendFusionTables; + } + + /** + * Sets the send to Google Fusion Tables option. + * + * @param sendFusionTables true if the user has selected the send to Google + * Fusion Tables option + */ + public void setSendFusionTables(boolean sendFusionTables) { + this.sendFusionTables = sendFusionTables; + } + + /** + * True if the user has selected the send to Google Docs option. + */ + public boolean isSendDocs() { + return sendDocs; + } + + /** + * Sets the send to Google Docs option. + * + * @param sendDocs true if the user has selected the send to Google Docs + * option + */ + public void setSendDocs(boolean sendDocs) { + this.sendDocs = sendDocs; + } + + /** + * True if the user has selected to create a new Google Maps. + */ + public boolean isNewMap() { + return newMap; + } + + /** + * Sets the new map option. + * + * @param newMap true if the user has selected to create a new Google Maps. + */ + public void setNewMap(boolean newMap) { + this.newMap = newMap; + } + + /** + * Gets the account. + */ + public Account getAccount() { + return account; + } + + /** + * Sets the account. + * + * @param account the account + */ + public void setAccount(Account account) { + this.account = account; + } + + /** + * Gets the selected map id if the user has selected to send a track to an + * existing Google Maps. + */ + public String getMapId() { + return mapId; + } + + /** + * Sets the map id. + * + * @param mapId the map id + */ + public void setMapId(String mapId) { + this.mapId = mapId; + } + + /** + * True if sending to Google Maps is success. + */ + public boolean isMapsSuccess() { + return mapsSuccess; + } + + /** + * Sets the Google Maps result. + * + * @param mapsSuccess true if sending to Google Maps is success + */ + public void setMapsSuccess(boolean mapsSuccess) { + this.mapsSuccess = mapsSuccess; + } + + /** + * True if sending to Google Fusion Tables is success. + */ + public boolean isFusionTablesSuccess() { + return fusionTablesSuccess; + } + + /** + * Sets the Google Fusion Tables result. + * + * @param fusionTablesSuccess true if sending to Google Fusion Tables is + * success + */ + public void setFusionTablesSuccess(boolean fusionTablesSuccess) { + this.fusionTablesSuccess = fusionTablesSuccess; + } + + /** + * True if sending to Google Docs is success. + */ + public boolean isDocsSuccess() { + return docsSuccess; + } + + /** + * Sets the Google Docs result. + * + * @param docsSuccess true if sending to Google Docs is success + */ + public void setDocsSuccess(boolean docsSuccess) { + this.docsSuccess = docsSuccess; + } + + private SendRequest(Parcel in) { + trackId = in.readLong(); + showMaps = in.readByte() == 1; + showFusionTables = in.readByte() == 1; + showDocs = in.readByte() == 1; + sendMaps = in.readByte() == 1; + sendFusionTables = in.readByte() == 1; + sendDocs = in.readByte() == 1; + newMap = in.readByte() == 1; + account = in.readParcelable(null); + mapId = in.readString(); + mapsSuccess = in.readByte() == 1; + fusionTablesSuccess = in.readByte() == 1; + docsSuccess = in.readByte() == 1; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeLong(trackId); + out.writeByte((byte) (showMaps ? 1 : 0)); + out.writeByte((byte) (showFusionTables ? 1 : 0)); + out.writeByte((byte) (showDocs ? 1 : 0)); + out.writeByte((byte) (sendMaps ? 1 : 0)); + out.writeByte((byte) (sendFusionTables ? 1 : 0)); + out.writeByte((byte) (sendDocs ? 1 : 0)); + out.writeByte((byte) (newMap ? 1 : 0)); + out.writeParcelable(account, 0); + out.writeString(mapId); + out.writeByte((byte) (mapsSuccess ? 1 : 0)); + out.writeByte((byte) (fusionTablesSuccess ? 1 : 0)); + out.writeByte((byte) (docsSuccess ? 1 : 0)); + } + + public static final Parcelable.Creator CREATOR = new Parcelable.Creator< + SendRequest>() { + public SendRequest createFromParcel(Parcel in) { + return new SendRequest(in); + } + + public SendRequest[] newArray(int size) { + return new SendRequest[size]; + } + }; +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/SendResult.java b/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/SendResult.java deleted file mode 100644 index 34ad4b13d..000000000 --- a/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/SendResult.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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; - -/** - * This class represents the the result of the uploading of track data to a - * single service. - * - * @author Matthew Simmons - */ -public class SendResult { - private final SendType type; - private final boolean success; - - /** - * @param type the service to which track data was uploaded - * @param success true if the uploading succeeded - */ - public SendResult(SendType type, boolean success) { - this.type = type; - this.success = success; - } - - /** Returns the service to which the track data was uploaded */ - public SendType getType() { - return type; - } - - /** Returns true if the uploading succeeded */ - public boolean isSuccess() { - return success; - } -} diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/SendToGoogleUtils.java b/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/SendToGoogleUtils.java new file mode 100644 index 000000000..c3a29ce1d --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/SendToGoogleUtils.java @@ -0,0 +1,132 @@ +/* + * Copyright 2012 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 com.google.android.apps.mytracks.content.Track; +import com.google.android.apps.mytracks.stats.TripStatistics; +import com.google.android.apps.mytracks.util.LocationUtils; + +import android.location.Location; +import android.util.Log; + +import java.util.ArrayList; +import java.util.List; + +/** + * Commons utilities for sending a track to Google. + * + * @author Jimmy Shih + */ +public class SendToGoogleUtils { + + private static final String TAG = SendToGoogleUtils.class.getSimpleName(); + + private SendToGoogleUtils() {} + + /** + * Prepares a list of locations to send to Google Maps or Google Fusion + * Tables. Splits the locations into segments if necessary. + * + * @param track the track + * @param locations the list of locations + * @return an array of split segments. + */ + public static ArrayList prepareLocations(Track track, List locations) { + ArrayList splitTracks = new ArrayList(); + + // Create a new segment + Track segment = new Track(); + segment.setId(track.getId()); + segment.setName(track.getName()); + segment.setDescription(""); + segment.setCategory(track.getCategory()); + + TripStatistics segmentStats = segment.getStatistics(); + TripStatistics trackStats = track.getStatistics(); + segmentStats.setStartTime(trackStats.getStartTime()); + segmentStats.setStopTime(trackStats.getStopTime()); + boolean startNewTrackSegment = false; + for (Location loc : locations) { + // Latitude is greater than 90 if the location is invalid. Do not add to + // the segment. + if (loc.getLatitude() > 90) { + startNewTrackSegment = true; + } + + if (startNewTrackSegment) { + // Close the last segment + prepareTrackSegment(segment, splitTracks); + + startNewTrackSegment = false; + segment = new Track(); + segment.setId(track.getId()); + segment.setName(track.getName()); + segment.setDescription(""); + segment.setCategory(track.getCategory()); + segmentStats = segment.getStatistics(); + } + + if (loc.getLatitude() <= 90) { + segment.addLocation(loc); + + // For a new segment, sets its start time using the first available + // location time. + if (segmentStats.getStartTime() < 0) { + segmentStats.setStartTime(loc.getTime()); + } + } + } + + prepareTrackSegment(segment, splitTracks); + + return splitTracks; + } + + /** + * Prepares a track segment for sending to Google Maps or Google Fusion + * Tables. The main steps are: + *

    + *
  • make sure the segment has at least 2 points
  • + *
  • set the segment stop time if necessary
  • + *
  • decimate locations precision
  • + *
+ * The prepared track will be added to the splitTracks. + * + * @param segment the track segment + * @param splitTracks an array of track segments + */ + private static void prepareTrackSegment(Track segment, ArrayList splitTracks) { + // Make sure the segment has at least 2 points + if (segment.getLocations().size() < 2) { + Log.d(TAG, "segment has less than 2 points"); + return; + } + + // For a new segment, sets it stop time + TripStatistics segmentStats = segment.getStatistics(); + if (segmentStats.getStopTime() < 0) { + Location lastLocation = segment.getLocations().get(segment.getLocations().size() - 1); + segmentStats.setStopTime(lastLocation.getTime()); + } + + // Decimate to 2 meter precision. Google Maps and Google Fusion Tables do + // not like the locations to be too precise. + LocationUtils.decimate(segment, 2.0); + + splitTracks.add(segment); + } +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/SendType.java b/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/SendType.java deleted file mode 100644 index 6bb672d46..000000000 --- a/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/SendType.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * 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 com.google.android.maps.mytracks.R; - -import android.os.Parcel; -import android.os.Parcelable; - -/** - * Enumerates the services to which we can upload track data. - * - * @author Matthew Simmons - */ -public enum SendType implements Parcelable { - MAPS(R.string.send_google_maps, - R.string.send_google_maps_url), - FUSION_TABLES(R.string.send_google_fusion_tables, - R.string.send_google_fusion_tables_url), - DOCS(R.string.send_google_docs, - R.string.send_google_docs_url); - - private int serviceName; - private int serviceUrl; - - private SendType(int serviceName, int serviceUrl) { - this.serviceName = serviceName; - this.serviceUrl = serviceUrl; - } - - /** Returns the resource ID for the printable (short) name of the service */ - public int getServiceName() { - return serviceName; - } - - /** Returns the resource ID for the service's URL */ - public int getServiceUrl() { - return serviceUrl; - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeInt(ordinal()); - } - - public static final Creator CREATOR = new Creator() { - - @Override - public SendType createFromParcel(Parcel source) { - return SendType.values()[source.readInt()]; - } - - @Override - public SendType[] newArray(int size) { - return new SendType[size]; - } - }; -} diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/UploadResultActivity.java b/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/UploadResultActivity.java new file mode 100644 index 000000000..1dda1c770 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/UploadResultActivity.java @@ -0,0 +1,194 @@ +/* + * Copyright 2012 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 com.google.android.apps.mytracks.Constants; +import com.google.android.apps.mytracks.content.MyTracksProviderUtils; +import com.google.android.apps.mytracks.content.Track; +import com.google.android.apps.mytracks.io.fusiontables.SendFusionTablesUtils; +import com.google.android.apps.mytracks.io.maps.SendMapsUtils; +import com.google.android.maps.mytracks.R; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.view.View; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +/** + * A dialog to show the result of uploading to Google services. + * + * @author jshih@google.com (Jimmy Shih) + */ +public class UploadResultActivity extends Activity { + + private static final String TEXT_PLAIN_TYPE = "text/plain"; + private static final int RESULT_DIALOG = 1; + + private SendRequest sendRequest; + private Track track; + private String shareUrl; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + sendRequest = getIntent().getParcelableExtra(SendRequest.SEND_REQUEST_KEY); + track = null; + shareUrl = null; + + if (sendRequest.isSendMaps() && sendRequest.isMapsSuccess()) { + shareUrl = SendMapsUtils.getMapUrl(getTrack()); + } + if (shareUrl == null && sendRequest.isSendFusionTables() + && sendRequest.isFusionTablesSuccess()) { + shareUrl = SendFusionTablesUtils.getMapUrl(getTrack()); + } + } + + private Track getTrack() { + if (track == null) { + track = MyTracksProviderUtils.Factory.get(this).getTrack(sendRequest.getTrackId()); + } + return track; + } + + @Override + protected void onResume() { + super.onResume(); + showDialog(RESULT_DIALOG); + } + + @Override + protected Dialog onCreateDialog(int id) { + switch (id) { + case RESULT_DIALOG: + AlertDialog.Builder builder = new AlertDialog.Builder(this); + View view = getLayoutInflater().inflate(R.layout.upload_result, null); + builder.setView(view); + + LinearLayout mapsResult = (LinearLayout) view.findViewById(R.id.upload_result_maps_result); + LinearLayout fusionTablesResult = (LinearLayout) view.findViewById( + R.id.upload_result_fusion_tables_result); + LinearLayout docsResult = (LinearLayout) view.findViewById(R.id.upload_result_docs_result); + + ImageView mapsResultIcon = (ImageView) view.findViewById( + R.id.upload_result_maps_result_icon); + ImageView fusionTablesResultIcon = (ImageView) view.findViewById( + R.id.upload_result_fusion_tables_result_icon); + ImageView docsResultIcon = (ImageView) view.findViewById( + R.id.upload_result_docs_result_icon); + + TextView successFooter = (TextView) view.findViewById(R.id.upload_result_success_footer); + TextView errorFooter = (TextView) view.findViewById(R.id.upload_result_error_footer); + + boolean hasError = false; + if (!sendRequest.isSendMaps()) { + mapsResult.setVisibility(View.GONE); + } else { + if (!sendRequest.isMapsSuccess()) { + mapsResultIcon.setImageResource(R.drawable.failure); + hasError = true; + } + } + + if (!sendRequest.isSendFusionTables()) { + fusionTablesResult.setVisibility(View.GONE); + } else { + if (!sendRequest.isFusionTablesSuccess()) { + fusionTablesResultIcon.setImageResource(R.drawable.failure); + hasError = true; + } + } + + if (!sendRequest.isSendDocs()) { + docsResult.setVisibility(View.GONE); + } else { + if (!sendRequest.isDocsSuccess()) { + docsResultIcon.setImageResource(R.drawable.failure); + hasError = true; + } + } + + if (hasError) { + builder.setTitle(R.string.generic_error_title); + builder.setIcon(android.R.drawable.ic_dialog_alert); + successFooter.setVisibility(View.GONE); + } else { + builder.setTitle(R.string.generic_success_title); + builder.setIcon(android.R.drawable.ic_dialog_info); + errorFooter.setVisibility(View.GONE); + } + + builder.setCancelable(true); + builder.setOnCancelListener(new DialogInterface.OnCancelListener() { + @Override + public void onCancel(DialogInterface dialog) { + finish(); + } + }); + builder.setPositiveButton( + getString(R.string.generic_ok), new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + if (!sendRequest.isShowAll() && shareUrl != null) { + startShareUrlActivity(shareUrl); + } + finish(); + } + }); + + // Add a Share URL button if showing all the options and a shareUrl + // exists + if (sendRequest.isShowAll() && shareUrl != null) { + builder.setNegativeButton(getString(R.string.send_google_result_share_url), + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + startShareUrlActivity(shareUrl); + finish(); + } + }); + } + return builder.create(); + default: + return null; + } + } + + /** + * Starts an activity to share the url. + * + * @param url the url + */ + private void startShareUrlActivity(String url) { + SharedPreferences prefs = getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE); + boolean shareUrlOnly = prefs.getBoolean(getString(R.string.share_url_only_key), false); + + Intent intent = new Intent(Intent.ACTION_SEND); + intent.setType(TEXT_PLAIN_TYPE); + intent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.share_track_subject)); + intent.putExtra(Intent.EXTRA_TEXT, + shareUrlOnly ? url : getString(R.string.share_track_url_body_format, url)); + startActivity(Intent.createChooser(intent, getString(R.string.share_track_picker_title))); + } +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/UploadServiceChooserActivity.java b/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/UploadServiceChooserActivity.java index b6f3fc8d1..26d0d77ef 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/UploadServiceChooserActivity.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/UploadServiceChooserActivity.java @@ -15,8 +15,10 @@ */ package com.google.android.apps.mytracks.io.sendtogoogle; +import com.google.android.apps.analytics.GoogleAnalyticsTracker; import com.google.android.apps.mytracks.Constants; -import com.google.android.apps.mytracks.util.ApiFeatures; +import com.google.android.apps.mytracks.util.ApiAdapterFactory; +import com.google.android.apps.mytracks.util.SystemUtils; import com.google.android.maps.mytracks.R; import android.app.Activity; @@ -43,20 +45,9 @@ import android.widget.TableRow; */ public class UploadServiceChooserActivity extends Activity { - /** - * The track id. - */ - public static final String TRACK_ID = "trackId"; - - /** - * The send type. Null to send to all Google services. - */ - public static final String SEND_TYPE = "sendType"; - private static final int SERVICE_PICKER_DIALOG = 1; - private long trackId; - private SendType sendType; + private SendRequest sendRequest; private TableRow mapsTableRow; private TableRow fusionTablesTableRow; @@ -76,10 +67,7 @@ public class UploadServiceChooserActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - - Intent intent = getIntent(); - trackId = intent.getLongExtra(TRACK_ID, -1L); - sendType = intent.getParcelableExtra(SEND_TYPE); + sendRequest = getIntent().getParcelableExtra(SendRequest.SEND_REQUEST_KEY); } @Override @@ -136,23 +124,15 @@ public class UploadServiceChooserActivity extends Activity { send.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { saveState(); - Intent intent = new Intent(UploadServiceChooserActivity.this, SendActivity.class); - intent.putExtra(SendActivity.TRACK_ID, trackId); - intent.putExtra(SendActivity.SHARE_URL, sendType != null); - intent.putExtra(SendActivity.SEND_MAPS, sendMaps()); - intent.putExtra(SendActivity.SEND_FUSION_TABLES, sendFusionTables()); - intent.putExtra(SendActivity.SEND_DOCS, sendDocs()); - intent.putExtra(SendActivity.CREATE_MAP, !existingMapRadioButton.isChecked()); - startActivity(intent); - finish(); + startNextActivity(); } }); // Setup initial state initState(); - // Update state based on sendType - updateStateBySendType(); + // Update state based on sendRequest + updateStateBySendRequest(); // Update state based on current user selection updateStateBySelection(); @@ -180,21 +160,21 @@ public class UploadServiceChooserActivity extends Activity { } /** - * Updates the UI state based on the sendType. + * Updates the UI state based on sendRequest. */ - private void updateStateBySendType() { - if (sendType == SendType.MAPS) { - mapsCheckBox.setChecked(true); - } else if (sendType == SendType.FUSION_TABLES) { - fusionTablesCheckBox.setChecked(true); - } else if (sendType == SendType.DOCS) { - docsCheckBox.setChecked(true); - } else { - // sendType == null + private void updateStateBySendRequest() { + if (!sendRequest.isShowAll()) { + if (sendRequest.isShowMaps()) { + mapsCheckBox.setChecked(true); + } else if (sendRequest.isShowFusionTables()) { + fusionTablesCheckBox.setChecked(true); + } else if (sendRequest.isShowDocs()) { + docsCheckBox.setChecked(true); + } } - mapsTableRow.setVisibility(showMaps() ? View.VISIBLE : View.GONE); - fusionTablesTableRow.setVisibility(showFusionTables() ? View.VISIBLE : View.GONE); - docsTableRow.setVisibility(showDocs() ? View.VISIBLE : View.GONE); + mapsTableRow.setVisibility(sendRequest.isShowMaps() ? View.VISIBLE : View.GONE); + fusionTablesTableRow.setVisibility(sendRequest.isShowFusionTables() ? View.VISIBLE : View.GONE); + docsTableRow.setVisibility(sendRequest.isShowDocs() ? View.VISIBLE : View.GONE); } /** @@ -213,53 +193,67 @@ public class UploadServiceChooserActivity extends Activity { Editor editor = prefs.edit(); editor.putBoolean( getString(R.string.pick_existing_map_key), existingMapRadioButton.isChecked()); - if (sendType == null) { + if (sendRequest.isShowAll()) { editor.putBoolean(getString(R.string.send_to_maps_key), sendMaps()); editor.putBoolean(getString(R.string.send_to_fusion_tables_key), sendFusionTables()); editor.putBoolean(getString(R.string.send_to_docs_key), sendDocs()); } - ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges(editor); - } - - /** - * Returns true to show the Google Maps option. - */ - private boolean showMaps() { - return sendType == null || sendType == SendType.MAPS; - } - - /** - * Returns true to show the Google Fusion Tables option. - */ - private boolean showFusionTables() { - return sendType == null || sendType == SendType.FUSION_TABLES; - } - - /** - * Returns true to show the Google Docs option. - */ - private boolean showDocs() { - return sendType == null || sendType == SendType.DOCS; + ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor); } /** * Returns true to send to Google Maps. */ private boolean sendMaps() { - return showMaps() && mapsCheckBox.isChecked(); + return sendRequest.isShowMaps() && mapsCheckBox.isChecked(); } /** * Returns true to send to Google Fusion Tables. */ private boolean sendFusionTables() { - return showFusionTables() && fusionTablesCheckBox.isChecked(); + return sendRequest.isShowFusionTables() && fusionTablesCheckBox.isChecked(); } /** * Returns true to send to Google Docs. */ private boolean sendDocs() { - return showDocs() && docsCheckBox.isChecked(); + return sendRequest.isShowDocs() && docsCheckBox.isChecked(); + } + + /** + * Starts the next activity, {@link AccountChooserActivity}. + */ + private void startNextActivity() { + sendStats(); + sendRequest.setSendMaps(sendMaps()); + sendRequest.setSendFusionTables(sendFusionTables()); + sendRequest.setSendDocs(sendDocs()); + sendRequest.setNewMap(!existingMapRadioButton.isChecked()); + Intent intent = new Intent(this, AccountChooserActivity.class) + .putExtra(SendRequest.SEND_REQUEST_KEY, sendRequest); + startActivity(intent); + finish(); + } + + /** + * Sends stats to Google Analytics. + */ + private void sendStats() { + GoogleAnalyticsTracker tracker = GoogleAnalyticsTracker.getInstance(); + tracker.start(getString(R.string.my_tracks_analytics_id), getApplicationContext()); + tracker.setProductVersion("android-mytracks", SystemUtils.getMyTracksVersion(this)); + if (sendRequest.isSendMaps()) { + tracker.trackPageView("/send/maps"); + } + if (sendRequest.isSendFusionTables()) { + tracker.trackPageView("/send/fusion_tables"); + } + if (sendRequest.isSendDocs()) { + tracker.trackPageView("/send/docs"); + } + tracker.dispatch(); + tracker.stop(); } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/PreferenceManager.java b/MyTracks/src/com/google/android/apps/mytracks/services/PreferenceManager.java index 501fadf39..9e9c90a35 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/PreferenceManager.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/PreferenceManager.java @@ -16,7 +16,7 @@ package com.google.android.apps.mytracks.services; import com.google.android.apps.mytracks.Constants; -import com.google.android.apps.mytracks.util.ApiFeatures; +import com.google.android.apps.mytracks.util.ApiAdapterFactory; import com.google.android.maps.mytracks.R; import android.content.Context; @@ -174,19 +174,19 @@ public class PreferenceManager implements OnSharedPreferenceChangeListener { public void setAutoResumeTrackCurrentRetry(int retryAttempts) { Editor editor = sharedPreferences.edit(); editor.putInt(autoResumeTrackCurrentRetryKey, retryAttempts); - ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges(editor); + ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor); } public void setRecordingTrack(long id) { Editor editor = sharedPreferences.edit(); editor.putLong(recordingTrackKey, id); - ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges(editor); + ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor); } public void setSelectedTrack(long id) { Editor editor = sharedPreferences.edit(); editor.putLong(selectedTrackKey, id); - ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges(editor); + ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor); } public void shutdown() { diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/TrackRecordingService.java b/MyTracks/src/com/google/android/apps/mytracks/services/TrackRecordingService.java index ee1e021cc..8b81a44df 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/TrackRecordingService.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/TrackRecordingService.java @@ -36,14 +36,12 @@ import com.google.android.apps.mytracks.services.tasks.SplitTask; import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerFactory; import com.google.android.apps.mytracks.stats.TripStatistics; import com.google.android.apps.mytracks.stats.TripStatisticsBuilder; -import com.google.android.apps.mytracks.util.ApiFeatures; -import com.google.android.apps.mytracks.util.ApiLevelAdapter; import com.google.android.apps.mytracks.util.LocationUtils; import com.google.android.apps.mytracks.util.StringUtils; import com.google.android.maps.mytracks.R; +import com.google.common.annotations.VisibleForTesting; import android.app.Notification; -import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; import android.content.ContentValues; @@ -80,7 +78,6 @@ public class TrackRecordingService extends Service { static final int MAX_AUTO_RESUME_TRACK_RETRY_ATTEMPTS = 3; - private NotificationManager notificationManager; private LocationManager locationManager; private WakeLock wakeLock; @@ -243,8 +240,6 @@ public class TrackRecordingService extends Service { super.onCreate(); Log.d(TAG, "TrackRecordingService.onCreate"); providerUtils = MyTracksProviderUtils.Factory.get(this); - notificationManager = - (NotificationManager) getSystemService(NOTIFICATION_SERVICE); locationManager = (LocationManager) getSystemService(LOCATION_SERVICE); setUpTaskExecutors(); @@ -369,7 +364,6 @@ public class TrackRecordingService extends Service { // Make sure we have no indirect references to this service. locationManager = null; - notificationManager = null; providerUtils = null; binder.detachFromService(); binder = null; @@ -485,8 +479,6 @@ public class TrackRecordingService extends Service { * Shows the notification message and icon in the notification bar. */ private void showNotification() { - final ApiLevelAdapter apiLevelAdapter = - ApiFeatures.getInstance().getApiAdapter(); if (isRecording) { Notification notification = new Notification( R.drawable.arrow_320, null /* tickerText */, @@ -497,16 +489,24 @@ public class TrackRecordingService extends Service { notification.setLatestEventInfo(this, getString(R.string.my_tracks_app_name), getString(R.string.track_record_notification), contentIntent); notification.flags += Notification.FLAG_NO_CLEAR; - apiLevelAdapter.startForeground(this, notificationManager, 1, - notification); + startForegroundService(notification); } else { - apiLevelAdapter.stopForeground(this, notificationManager, 1); + stopForegroundService(); } } + @VisibleForTesting + protected void startForegroundService(Notification notification) { + startForeground(1, notification); + } + + @VisibleForTesting + protected void stopForegroundService() { + stopForeground(true); + } + private void setUpTaskExecutors() { - announcementExecutor = new PeriodicTaskExecutor( - this, new StatusAnnouncerFactory(ApiFeatures.getInstance())); + announcementExecutor = new PeriodicTaskExecutor(this, new StatusAnnouncerFactory()); splitExecutor = new PeriodicTaskExecutor(this, new SplitTask.Factory()); } diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/sensors/BluetoothConnectionManager.java b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/BluetoothConnectionManager.java index 4a3bc512b..558bfe423 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/sensors/BluetoothConnectionManager.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/BluetoothConnectionManager.java @@ -18,7 +18,7 @@ package com.google.android.apps.mytracks.services.sensors; import com.google.android.apps.mytracks.Constants; import com.google.android.apps.mytracks.content.Sensor; -import com.google.android.apps.mytracks.util.ApiFeatures; +import com.google.android.apps.mytracks.util.ApiAdapterFactory; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; @@ -31,8 +31,6 @@ import android.util.Log; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; import java.util.UUID; /** @@ -44,8 +42,9 @@ import java.util.UUID; * @author Sandor Dornbush */ public class BluetoothConnectionManager { - // Unique UUID for this application - private static final UUID SPP_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB"); + + // Unique Bluetooth UUID for My Tracks + public static final UUID SPP_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB"); private MessageParser parser; @@ -254,41 +253,13 @@ public class BluetoothConnectionManager { // Get a BluetoothSocket for a connection with the // given BluetoothDevice try { - tmp = getSocket(); + tmp = ApiAdapterFactory.getApiAdapter().getBluetoothSocket(device); } catch (IOException e) { Log.e(Constants.TAG, "create() failed", e); } socket = tmp; } - private BluetoothSocket getSocket() throws IOException { - if (ApiFeatures.getInstance().hasBluetoothDeviceCreateInsecureRfcommSocketToServiceRecord()) { - try { - return device.createInsecureRfcommSocketToServiceRecord(SPP_UUID); - } catch (IOException e) { - Log.e(Constants.TAG, "Unable to get insecure connect.", e); - } - } else { - try { - Class c = device.getClass(); - Method insecure = c.getMethod("createInsecureRfcommSocket", Integer.class); - insecure.setAccessible(true); - return (BluetoothSocket) insecure.invoke(device, 1); - } catch (SecurityException e) { - Log.e(Constants.TAG, "Unable to get insecure connect.", e); - } catch (NoSuchMethodException e) { - Log.e(Constants.TAG, "Unable to get insecure connect.", e); - } catch (IllegalArgumentException e) { - Log.e(Constants.TAG, "Unable to get insecure connect.", e); - } catch (IllegalAccessException e) { - Log.e(Constants.TAG, "Unable to get insecure connect.", e); - } catch (InvocationTargetException e) { - Log.e(Constants.TAG, "Unable to get insecure connect.", e); - } - } - return device.createRfcommSocketToServiceRecord(SPP_UUID); - } - @Override public void run() { Log.d(Constants.TAG, "BEGIN mConnectThread"); diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/sensors/ZephyrMessageParser.java b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/ZephyrMessageParser.java index b70e0fddf..a8354ee95 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/sensors/ZephyrMessageParser.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/ZephyrMessageParser.java @@ -16,7 +16,7 @@ package com.google.android.apps.mytracks.services.sensors; import com.google.android.apps.mytracks.content.Sensor; -import com.google.android.apps.mytracks.util.ApiFeatures; +import com.google.android.apps.mytracks.util.ApiAdapterFactory; import java.util.Arrays; @@ -62,8 +62,7 @@ public class ZephyrMessageParser implements MessageParser { // 0x1A00316550003162 produces erroneous values for Cadence and needs // a workaround based on the stride counter. // Firmware values range from field 3 to 10 (inclusive) of the byte buffer. - byte[] hardwareFirmwareId = ApiFeatures.getInstance().getApiAdapter() - .copyByteArray(buffer, 3, 11); + byte[] hardwareFirmwareId = ApiAdapterFactory.getApiAdapter().copyByteArray(buffer, 3, 11); Sensor.SensorData.Builder cadence = Sensor.SensorData.newBuilder(); if (Arrays.equals(hardwareFirmwareId, CADENCE_BUG_FW_ID)) { diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/sensors/ant/AntDirectSensorManager.java b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/ant/AntDirectSensorManager.java index 4c0db0864..e72635f79 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/sensors/ant/AntDirectSensorManager.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/ant/AntDirectSensorManager.java @@ -22,7 +22,7 @@ import com.dsi.ant.AntMesg; import com.dsi.ant.exception.AntInterfaceException; import com.google.android.apps.mytracks.Constants; import com.google.android.apps.mytracks.content.Sensor; -import com.google.android.apps.mytracks.util.ApiFeatures; +import com.google.android.apps.mytracks.util.ApiAdapterFactory; import com.google.android.maps.mytracks.R; import android.content.Context; @@ -135,7 +135,7 @@ public class AntDirectSensorManager extends AntSensorManager { Constants.SETTINGS_NAME, Context.MODE_PRIVATE); SharedPreferences.Editor editor = prefs.edit(); editor.putInt(context.getString(R.string.ant_heart_rate_sensor_id_key), deviceNumberHRM); - ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges(editor); + ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor); } private void handleMessageResponse(byte[] rawMessage) { diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/sensors/ant/AntSrmBridgeSensorManager.java b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/ant/AntSrmBridgeSensorManager.java index 1db754b4d..1006e4a6b 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/sensors/ant/AntSrmBridgeSensorManager.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/ant/AntSrmBridgeSensorManager.java @@ -22,7 +22,7 @@ import com.dsi.ant.AntMesg; import com.dsi.ant.exception.AntInterfaceException; import com.google.android.apps.mytracks.Constants; import com.google.android.apps.mytracks.content.Sensor; -import com.google.android.apps.mytracks.util.ApiFeatures; +import com.google.android.apps.mytracks.util.ApiAdapterFactory; import com.google.android.apps.mytracks.util.SystemUtils; import com.google.android.maps.mytracks.R; @@ -199,7 +199,7 @@ public class AntSrmBridgeSensorManager extends AntSensorManager { Constants.SETTINGS_NAME, Context.MODE_PRIVATE); SharedPreferences.Editor editor = prefs.edit(); editor.putInt(context.getString(R.string.ant_srm_bridge_sensor_id_key), deviceNumber); - ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges(editor); + ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor); } private void handleMessageResponse(byte[] rawMessage) { diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/tasks/StatusAnnouncerFactory.java b/MyTracks/src/com/google/android/apps/mytracks/services/tasks/StatusAnnouncerFactory.java index 739b81cb3..7e8b56b37 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/tasks/StatusAnnouncerFactory.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/tasks/StatusAnnouncerFactory.java @@ -15,10 +15,9 @@ */ package com.google.android.apps.mytracks.services.tasks; -import com.google.android.apps.mytracks.util.ApiFeatures; +import com.google.android.apps.mytracks.util.ApiAdapterFactory; import android.content.Context; -import android.media.AudioManager; /** * Factory which wraps construction and setup of text-to-speech announcements in @@ -28,30 +27,11 @@ import android.media.AudioManager; */ public class StatusAnnouncerFactory implements PeriodicTaskFactory { - private final boolean hasTts; - - public StatusAnnouncerFactory(ApiFeatures apiFeatures) { - this.hasTts = apiFeatures.hasTextToSpeech(); + public StatusAnnouncerFactory() { } @Override public PeriodicTask create(Context context) { - if (hasTts) { - return ApiFeatures.getInstance().getApiAdapter().getStatusAnnouncerTask(context); - } else { - return null; - } - } - - /** - * Returns the appropriate volume stream for controlling announcement - * volume. - */ - public int getVolumeStream() { - if (hasTts) { - return StatusAnnouncerTask.getVolumeStream(); - } else { - return AudioManager.USE_DEFAULT_STREAM_TYPE; - } + return ApiAdapterFactory.getApiAdapter().getStatusAnnouncerTask(context); } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/tasks/StatusAnnouncerTask.java b/MyTracks/src/com/google/android/apps/mytracks/services/tasks/StatusAnnouncerTask.java index f6532249b..24bbd674b 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/tasks/StatusAnnouncerTask.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/tasks/StatusAnnouncerTask.java @@ -331,13 +331,6 @@ public class StatusAnnouncerTask implements PeriodicTask { } } - /** - * Returns the volume stream to use for controlling announcement volume. - */ - public static int getVolumeStream() { - return TextToSpeech.Engine.DEFAULT_STREAM; - } - /** * Gets a string to announce the time. * diff --git a/MyTracks/src/com/google/android/apps/mytracks/util/Api10Adapter.java b/MyTracks/src/com/google/android/apps/mytracks/util/Api10Adapter.java new file mode 100644 index 000000000..8bdece372 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/util/Api10Adapter.java @@ -0,0 +1,44 @@ +/* + * Copyright 2012 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 com.google.android.apps.mytracks.Constants; +import com.google.android.apps.mytracks.services.sensors.BluetoothConnectionManager; + +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothSocket; +import android.util.Log; + +import java.io.IOException; + +/** + * API level 10 specific implementation of the {@link ApiAdapter}. + * + * @author Jimmy Shih + */ +public class Api10Adapter extends Api9Adapter { + + @Override + public BluetoothSocket getBluetoothSocket(BluetoothDevice bluetoothDevice) throws IOException { + try { + return bluetoothDevice.createInsecureRfcommSocketToServiceRecord( + BluetoothConnectionManager.SPP_UUID); + } catch (IOException e) { + Log.d(Constants.TAG, "Unable to create insecure connection", e); + } + return bluetoothDevice.createRfcommSocketToServiceRecord(BluetoothConnectionManager.SPP_UUID); + }; +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/util/ApiLevel3Adapter.java b/MyTracks/src/com/google/android/apps/mytracks/util/Api7Adapter.java similarity index 63% rename from MyTracks/src/com/google/android/apps/mytracks/util/ApiLevel3Adapter.java rename to MyTracks/src/com/google/android/apps/mytracks/util/Api7Adapter.java index dcd033e14..6b572d045 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/util/ApiLevel3Adapter.java +++ b/MyTracks/src/com/google/android/apps/mytracks/util/Api7Adapter.java @@ -15,50 +15,32 @@ */ package com.google.android.apps.mytracks.util; -import static com.google.android.apps.mytracks.Constants.TAG; - +import com.google.android.apps.mytracks.Constants; import com.google.android.apps.mytracks.io.backup.BackupPreferencesListener; +import com.google.android.apps.mytracks.services.sensors.BluetoothConnectionManager; import com.google.android.apps.mytracks.services.tasks.PeriodicTask; import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerTask; import com.google.api.client.http.HttpTransport; import com.google.api.client.http.apache.ApacheHttpTransport; -import android.app.Notification; -import android.app.NotificationManager; -import android.app.Service; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothSocket; import android.content.Context; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import android.util.Log; +import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; /** - * API level 3 specific implementation of the {@link ApiLevelAdapter}. + * API level 7 specific implementation of the {@link ApiAdapter}. * * @author Bartlomiej Niechwiej */ -public class ApiLevel3Adapter implements ApiLevelAdapter { +public class Api7Adapter implements ApiAdapter { - @Override - public void startForeground(Service service, - NotificationManager notificationManager, int id, - Notification notification) { - setServiceForeground(service, true); - - notificationManager.notify(id, notification); - } - - @Override - public void stopForeground(Service service, - NotificationManager notificationManager, int id) { - setServiceForeground(service, false); - if (id != -1) { - notificationManager.cancel(id); - } - } - @Override public PeriodicTask getStatusAnnouncerTask(Context context) { return new StatusAnnouncerTask(context); @@ -74,24 +56,6 @@ public class ApiLevel3Adapter implements ApiLevelAdapter { }; } - private void setServiceForeground(Service service, boolean foreground) { - // setForeground has been completely removed in API level 11, so we use reflection. - try { - Method setForegroundMethod = Service.class.getMethod("setForeground", boolean.class); - setForegroundMethod.invoke(service, foreground); - } catch (SecurityException e) { - Log.e(TAG, "Unable to set service foreground state", e); - } catch (NoSuchMethodException e) { - Log.e(TAG, "Unable to set service foreground state", e); - } catch (IllegalArgumentException e) { - Log.e(TAG, "Unable to set service foreground state", e); - } catch (IllegalAccessException e) { - Log.e(TAG, "Unable to set service foreground state", e); - } catch (InvocationTargetException e) { - Log.e(TAG, "Unable to set service foreground state", e); - } - } - @Override public void applyPreferenceChanges(Editor editor) { editor.commit(); @@ -114,4 +78,25 @@ public class ApiLevel3Adapter implements ApiLevelAdapter { public HttpTransport getHttpTransport() { return new ApacheHttpTransport(); } + + @Override + public BluetoothSocket getBluetoothSocket(BluetoothDevice bluetoothDevice) throws IOException { + try { + Class c = bluetoothDevice.getClass(); + Method insecure = c.getMethod("createInsecureRfcommSocket", Integer.class); + insecure.setAccessible(true); + return (BluetoothSocket) insecure.invoke(bluetoothDevice, 1); + } catch (SecurityException e) { + Log.d(Constants.TAG, "Unable to create insecure connection", e); + } catch (NoSuchMethodException e) { + Log.d(Constants.TAG, "Unable to create insecure connection", e); + } catch (IllegalArgumentException e) { + Log.d(Constants.TAG, "Unable to create insecure connection", e); + } catch (IllegalAccessException e) { + Log.d(Constants.TAG, "Unable to create insecure connection", e); + } catch (InvocationTargetException e) { + Log.d(Constants.TAG, "Unable to create insecure connection", e); + } + return bluetoothDevice.createRfcommSocketToServiceRecord(BluetoothConnectionManager.SPP_UUID); + } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/util/ApiLevel8Adapter.java b/MyTracks/src/com/google/android/apps/mytracks/util/Api8Adapter.java similarity index 50% rename from MyTracks/src/com/google/android/apps/mytracks/util/ApiLevel8Adapter.java rename to MyTracks/src/com/google/android/apps/mytracks/util/Api8Adapter.java index 35696189d..f8371e081 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/util/ApiLevel8Adapter.java +++ b/MyTracks/src/com/google/android/apps/mytracks/util/Api8Adapter.java @@ -1,3 +1,18 @@ +/* + * Copyright 2012 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 com.google.android.apps.mytracks.io.backup.BackupPreferencesListener; @@ -8,11 +23,11 @@ import com.google.android.apps.mytracks.services.tasks.PeriodicTask; import android.content.Context; /** - * API level 8 specific implementation of the {@link ApiLevelAdapter}. + * API level 8 specific implementation of the {@link ApiAdapter}. * * @author Jimmy Shih */ -public class ApiLevel8Adapter extends ApiLevel5Adapter { +public class Api8Adapter extends Api7Adapter { @Override public PeriodicTask getStatusAnnouncerTask(Context context) { diff --git a/MyTracks/src/com/google/android/apps/mytracks/util/ApiLevel9Adapter.java b/MyTracks/src/com/google/android/apps/mytracks/util/Api9Adapter.java similarity index 61% rename from MyTracks/src/com/google/android/apps/mytracks/util/ApiLevel9Adapter.java rename to MyTracks/src/com/google/android/apps/mytracks/util/Api9Adapter.java index 5ab6ab0aa..86fb12d3d 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/util/ApiLevel9Adapter.java +++ b/MyTracks/src/com/google/android/apps/mytracks/util/Api9Adapter.java @@ -1,3 +1,18 @@ +/* + * Copyright 2012 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 com.google.android.apps.mytracks.Constants; @@ -11,11 +26,11 @@ import android.util.Log; import java.util.Arrays; /** - * API level 9 specific implementation of the {@link ApiLevelAdapter}. + * API level 9 specific implementation of the {@link ApiAdapter}. * * @author Rodrigo Damazio */ -public class ApiLevel9Adapter extends ApiLevel8Adapter { +public class Api9Adapter extends Api8Adapter { @Override public void applyPreferenceChanges(Editor editor) { diff --git a/MyTracks/src/com/google/android/apps/mytracks/util/ApiAdapter.java b/MyTracks/src/com/google/android/apps/mytracks/util/ApiAdapter.java new file mode 100644 index 000000000..868c43af1 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/util/ApiAdapter.java @@ -0,0 +1,101 @@ +/* + * 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.util; + +import com.google.android.apps.mytracks.io.backup.BackupPreferencesListener; +import com.google.android.apps.mytracks.services.tasks.PeriodicTask; +import com.google.api.client.http.HttpTransport; + +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothSocket; +import android.content.Context; +import android.content.SharedPreferences; + +import java.io.IOException; + +/** + * A set of methods that may be implemented differently depending on the Android + * API level. + * + * @author Bartlomiej Niechwiej + */ +public interface ApiAdapter { + + /** + * Gets a status announcer task. + *

+ * Due to changes in API level 8. + * + * @param context the context + */ + public PeriodicTask getStatusAnnouncerTask(Context context); + + /** + * Gets a {@link BackupPreferencesListener}. + *

+ * Due to changes in API level 8. + * + * @param context the context + */ + public BackupPreferencesListener getBackupPreferencesListener(Context context); + + /** + * Applies all the changes done to a given preferences editor. Changes may or + * may not be applied immediately. + *

+ * Due to changes in API level 9. + * + * @param editor the editor + */ + public void applyPreferenceChanges(SharedPreferences.Editor editor); + + /** + * Enables strict mode where supported, only if this is a development build. + *

+ * Due to changes in API level 9. + */ + public void enableStrictMode(); + + /** + * Copies elements from an input byte array into a new byte array, from + * indexes start (inclusive) to end (exclusive). The end index must be less + * than or equal to the input length. + *

+ * Due to changes in API level 9. + * + * @param input the input byte array + * @param start the start index + * @param end the end index + * @return a new array containing elements from the input byte array. + */ + public byte[] copyByteArray(byte[] input, int start, int end); + + /** + * Gets a {@link HttpTransport}. + *

+ * Due to changes in API level 9. + */ + public HttpTransport getHttpTransport(); + + /** + * Gets a {@link BluetoothSocket}. + *

+ * Due to changes in API level 10. + * + * @param bluetoothDevice + */ + public BluetoothSocket getBluetoothSocket(BluetoothDevice bluetoothDevice) throws IOException; +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/util/ApiAdapterFactory.java b/MyTracks/src/com/google/android/apps/mytracks/util/ApiAdapterFactory.java new file mode 100644 index 000000000..5af16534e --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/util/ApiAdapterFactory.java @@ -0,0 +1,50 @@ +/* + * 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.util; + +import android.os.Build; + +/** + * A factory to get the {@link ApiAdapter} for the current device. + * + * @author Rodrigo Damazio + */ +public class ApiAdapterFactory { + + private static ApiAdapter apiAdapter; + + /** + * Gets the {@link ApiAdapter} for the current device. + */ + public static ApiAdapter getApiAdapter() { + if (apiAdapter == null) { + if (Build.VERSION.SDK_INT >= 10) { + apiAdapter = new Api10Adapter(); + return apiAdapter; + } else if (Build.VERSION.SDK_INT >= 9) { + apiAdapter = new Api9Adapter(); + return apiAdapter; + } else if (Build.VERSION.SDK_INT >= 8) { + apiAdapter = new Api8Adapter(); + return apiAdapter; + } else { + apiAdapter = new Api7Adapter(); + return apiAdapter; + } + } + return apiAdapter; + } +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/util/ApiFeatures.java b/MyTracks/src/com/google/android/apps/mytracks/util/ApiFeatures.java deleted file mode 100644 index 700b5a2a6..000000000 --- a/MyTracks/src/com/google/android/apps/mytracks/util/ApiFeatures.java +++ /dev/null @@ -1,122 +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.util; - -import com.google.android.apps.mytracks.Constants; - -import android.os.Build; -import android.util.Log; - -/** - * Utility class for determining if newer-API features are available on the - * current device. - * - * @author Rodrigo Damazio - */ -public class ApiFeatures { - - /** - * The API level of the Android version we are being run under. - */ - private static final int ANDROID_API_LEVEL = Integer.parseInt( - Build.VERSION.SDK); - - private static ApiFeatures instance; - - /** - * The API level adapter for the Android version we are being run under. - */ - private ApiLevelAdapter apiLevelAdapter; - - /** - * Returns the singleton instance of this class. - */ - public static ApiFeatures getInstance() { - if (instance == null) { - instance = new ApiFeatures(); - } - return instance; - } - - /** - * Injects a specific singleton instance, to be used for unit tests. - */ - @SuppressWarnings("hiding") - public static void injectInstance(ApiFeatures instance) { - ApiFeatures.instance = instance; - } - - /** - * Allow subclasses for mocking, but no direct instantiation. - */ - protected ApiFeatures() { - // It is safe to import unsupported classes as long as we only actually - // load the class when supported. - if (getApiLevel() >= 9) { - apiLevelAdapter = new ApiLevel9Adapter(); - } else if (getApiLevel() >= 8) { - apiLevelAdapter = new ApiLevel8Adapter(); - } else if (getApiLevel() >= 5) { - apiLevelAdapter = new ApiLevel5Adapter(); - } else { - apiLevelAdapter = new ApiLevel3Adapter(); - } - - Log.i(Constants.TAG, "Using API level adapter " + apiLevelAdapter.getClass()); - } - - public ApiLevelAdapter getApiAdapter() { - return apiLevelAdapter; - } - - // API Level 4 Changes - - /** - * Returns whether text-to-speech is available. - */ - public boolean hasTextToSpeech() { - return getApiLevel() >= 4; - } - - // API Level 5 Changes - - /** - * There's a bug (#1587) in Cupcake and Donut which prevents you from - * using a SQLiteQueryBuilder twice. That is, if you call buildQuery - * on a given instance (to log the statement for debugging), and then - * call query on the same instance to make it actually do the query, - * it'll regenerate the query for the second call, and will screw it - * up. Specifically, it'll add extra parens which don't belong. - */ - public boolean canReuseSQLiteQueryBuilder() { - return getApiLevel() >= 5; - } - - // API Level 10 changes - - /** - * Returns true if BluetoothDevice.createInsecureRfcommSocketToServiceRecord - * is available. - */ - public boolean hasBluetoothDeviceCreateInsecureRfcommSocketToServiceRecord() { - return getApiLevel() >= 10; - } - - // Visible for testing. - protected int getApiLevel() { - return ANDROID_API_LEVEL; - } -} diff --git a/MyTracks/src/com/google/android/apps/mytracks/util/ApiLevel5Adapter.java b/MyTracks/src/com/google/android/apps/mytracks/util/ApiLevel5Adapter.java deleted file mode 100644 index 0f963f70f..000000000 --- a/MyTracks/src/com/google/android/apps/mytracks/util/ApiLevel5Adapter.java +++ /dev/null @@ -1,41 +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.util; - -import android.app.Notification; -import android.app.NotificationManager; -import android.app.Service; - -/** - * API level 5 specific implementation of the {@link ApiLevelAdapter}. - * - * @author Bartlomiej Niechwiej - */ -public class ApiLevel5Adapter extends ApiLevel3Adapter { - - @Override - public void startForeground(Service service, - NotificationManager notificationManager, int id, - Notification notification) { - service.startForeground(id, notification); - } - - @Override - public void stopForeground(Service service, - NotificationManager notificationManager, int id) { - service.stopForeground(id != -1); - } -} diff --git a/MyTracks/src/com/google/android/apps/mytracks/util/ApiLevelAdapter.java b/MyTracks/src/com/google/android/apps/mytracks/util/ApiLevelAdapter.java deleted file mode 100644 index f2180dfc2..000000000 --- a/MyTracks/src/com/google/android/apps/mytracks/util/ApiLevelAdapter.java +++ /dev/null @@ -1,113 +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.util; - -import com.google.android.apps.mytracks.io.backup.BackupPreferencesListener; -import com.google.android.apps.mytracks.services.tasks.PeriodicTask; -import com.google.api.client.http.HttpTransport; - -import android.app.Notification; -import android.app.NotificationManager; -import android.app.Service; -import android.content.Context; -import android.content.SharedPreferences; - -/** - * A set of methods that may be implemented differently depending on the Android API level. - * - * @author Bartlomiej Niechwiej - */ -public interface ApiLevelAdapter { - - /** - * Puts the specified service into foreground. - * - * Due to changes in API level 5. - * - * @param service the service to be put in foreground. - * @param notificationManager the notification manager used to post the given - * notification. - * @param id the ID of the notification, unique within the application. - * @param notification the notification to post. - */ - void startForeground(Service service, NotificationManager notificationManager, - int id, Notification notification); - - /** - * Puts the given service into background. - * - * Due to changes in API level 5. - * - * @param service the service to put into background. - * @param notificationManager the notification manager to user when removing - * notifications. - * @param id the ID of the notification to be remove, or -1 if the - * notification shouldn't be removed. - */ - void stopForeground(Service service, NotificationManager notificationManager, - int id); - - /** - * Gets a status announcer task. - * - * Due to changes in API level 8. - */ - PeriodicTask getStatusAnnouncerTask(Context context); - - /** - * Gets a {@link BackupPreferencesListener}. - * - * Due to changes in API level 8. - */ - BackupPreferencesListener getBackupPreferencesListener(Context context); - - /** - * Applies all changes done to the given preferences editor. - * Changes may or may not be applied immediately. - * - * Due to changes in API level 9. - */ - void applyPreferenceChanges(SharedPreferences.Editor editor); - - /** - * Enables strict mode where supported, only if this is a development build. - * - * Due to changes in API level 9. - */ - void enableStrictMode(); - - /** - * Copies elements from the input byte array into a new byte array, from - * indexes start (inclusive) to end (exclusive). The end index must be less - * than or equal to input.length. - * - * Due to changes in API level 9. - * - * @param input the input byte array - * @param start the start index - * @param end the end index - * @return a new array containing elements from the input byte array - */ - byte[] copyByteArray(byte[] input, int start, int end); - - - /** - * Gets a {@link HttpTransport}. - * - * Due to changes in API level 9. - */ - HttpTransport getHttpTransport(); -} diff --git a/MyTracks/src/com/google/android/apps/mytracks/util/BluetoothDeviceUtils.java b/MyTracks/src/com/google/android/apps/mytracks/util/BluetoothDeviceUtils.java index 96e8bd878..548d873c7 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/util/BluetoothDeviceUtils.java +++ b/MyTracks/src/com/google/android/apps/mytracks/util/BluetoothDeviceUtils.java @@ -15,186 +15,49 @@ */ package com.google.android.apps.mytracks.util; -import com.google.android.apps.mytracks.Constants; - import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothClass; import android.bluetooth.BluetoothDevice; -import android.os.Build; -import android.util.Log; import java.util.List; import java.util.Set; /** * Utilities for dealing with bluetooth devices. - * This can be used safely even in systems that don't support bluetooth, - * in which case a dummy implementation will be used. * * @author Rodrigo Damazio */ -public abstract class BluetoothDeviceUtils { - public static final String ANY_DEVICE = "any"; - private static BluetoothDeviceUtils instance; +public class BluetoothDeviceUtils { + private BluetoothDeviceUtils() {} + /** - * Dummy implementation, for systems that don't support bluetooth. - */ - private static class DummyImpl extends BluetoothDeviceUtils { - @Override - public void populateDeviceLists(List deviceNames, List deviceAddresses) { - // Do nothing - no devices to add - } - - @Override - public BluetoothDevice findDeviceMatching(String targetDeviceAddress) { - return null; - } - } - - /** - * Real implementation, for systems that DO support bluetooth. - */ - private static class RealImpl extends BluetoothDeviceUtils { - private final BluetoothAdapter bluetoothAdapter; - - public RealImpl() { - bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); - - if (bluetoothAdapter == null) { - throw new IllegalStateException("Unable to get bluetooth adapter"); - } - } - - @Override - public void populateDeviceLists(List deviceNames, List deviceAddresses) { - ensureNotDiscovering(); - - Set pairedDevices = bluetoothAdapter.getBondedDevices(); - for (BluetoothDevice device : pairedDevices) { - BluetoothClass bluetoothClass = device.getBluetoothClass(); - if (bluetoothClass != null) { - // Not really sure what we want, but I know what we don't want. - switch(bluetoothClass.getMajorDeviceClass()) { - case BluetoothClass.Device.Major.COMPUTER: - case BluetoothClass.Device.Major.PHONE: - break; - default: - deviceAddresses.add(device.getAddress()); - deviceNames.add(device.getName()); - } - } - } - } - - @Override - public BluetoothDevice findDeviceMatching(String targetDeviceAddress) { - if (targetDeviceAddress.equals(ANY_DEVICE)) { - return findAnyDevice(); - } else { - return findDeviceByAddress(targetDeviceAddress); - } - } - - /** - * Finds and returns the first suitable bluetooth sensor. - */ - private BluetoothDevice findAnyDevice() { - ensureNotDiscovering(); - - Set pairedDevices = bluetoothAdapter.getBondedDevices(); - for (BluetoothDevice device : pairedDevices) { - // Look for the first paired computer device - if (isSuitableDevice(device)) { - return device; - } - } - - return null; - } - - /** - * Finds and returns a device with the given address, or null if it's not - * a suitable sensor. - */ - private BluetoothDevice findDeviceByAddress(String targetDeviceAddress) { - ensureNotDiscovering(); - - BluetoothDevice device = bluetoothAdapter.getRemoteDevice(targetDeviceAddress); - if (isSuitableDevice(device)) { - return device; - } - - return null; - } - - /** - * Ensures the bluetooth adapter is not in discovery mode. - */ - private void ensureNotDiscovering() { - // If it's in discovery mode, cancel that for now. - bluetoothAdapter.cancelDiscovery(); - } - - /** - * Checks whether the given device is a suitable sensor. - * - * @param device the device to check - * @return true if it's suitable, false otherwise - */ - private boolean isSuitableDevice(BluetoothDevice device) { - // Check that the device is bonded - if (device.getBondState() != BluetoothDevice.BOND_BONDED) { - return false; - } - - return true; - } - } - - /** - * Returns the proper (singleton) instance of this class. - */ - public static BluetoothDeviceUtils getInstance() { - if (instance == null) { - if (!isBluetoothMethodSupported()) { - Log.d(Constants.TAG, "Using dummy bluetooth utils"); - instance = new DummyImpl(); - } else { - Log.d(Constants.TAG, "Using real bluetooth utils"); - try { - instance = new RealImpl(); - } catch (IllegalStateException ise) { - Log.w(Constants.TAG, "Oops, I mean, using dummy bluetooth utils", ise); - instance = new DummyImpl(); - } - } - } - return instance; - } - - /** - * Populates the given lists with the names and addresses of all suitable + * Populates the device names and the device addresses with all the suitable * bluetooth devices. * - * @param deviceNames the list to populate with user-visible names - * @param deviceAddresses the list to populate with device addresses + * @param bluetoothAdapter the bluetooth adapter + * @param deviceNames list of device names + * @param deviceAddresses list of device addresses */ - public abstract void populateDeviceLists(List deviceNames, List deviceAddresses); + public static void populateDeviceLists( + BluetoothAdapter bluetoothAdapter, List deviceNames, List deviceAddresses) { + // Ensure the bluetooth adapter is not in discovery mode. + bluetoothAdapter.cancelDiscovery(); - /** - * Finds the bluetooth device with the given address. - * - * @param targetDeviceAddress the address of the device, or - * {@link #ANY_DEVICE} for using the first suitable device - * @return the device's descriptor, or null if not found - */ - public abstract BluetoothDevice findDeviceMatching(String targetDeviceAddress); - - /** - * @return whether the bluetooth method is supported on this device - */ - public static boolean isBluetoothMethodSupported() { - return Integer.parseInt(Build.VERSION.SDK) >= 5; + Set pairedDevices = bluetoothAdapter.getBondedDevices(); + for (BluetoothDevice device : pairedDevices) { + BluetoothClass bluetoothClass = device.getBluetoothClass(); + if (bluetoothClass != null) { + // Not really sure what we want, but I know what we don't want. + switch (bluetoothClass.getMajorDeviceClass()) { + case BluetoothClass.Device.Major.COMPUTER: + case BluetoothClass.Device.Major.PHONE: + break; + default: + deviceAddresses.add(device.getAddress()); + deviceNames.add(device.getName()); + } + } + } } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/util/EulaUtil.java b/MyTracks/src/com/google/android/apps/mytracks/util/EulaUtil.java index 0008b6e8e..6d2253844 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/util/EulaUtil.java +++ b/MyTracks/src/com/google/android/apps/mytracks/util/EulaUtil.java @@ -45,7 +45,7 @@ public class EulaUtil { EULA_PREFERENCE_FILE, Context.MODE_PRIVATE); Editor editor = preferences.edit(); editor.putBoolean(EULA_PREFERENCE_KEY, true); - ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges(editor); + ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor); } public static String getEulaMessage(Context context) { diff --git a/MyTracksApiSample/AndroidManifest.xml b/MyTracksApiSample/AndroidManifest.xml index 56dd998b4..c00c5c7a4 100644 --- a/MyTracksApiSample/AndroidManifest.xml +++ b/MyTracksApiSample/AndroidManifest.xml @@ -3,7 +3,7 @@ package="com.google.android.apps.mytracks.samples.api" android:versionCode="1" android:versionName="1.0"> - + diff --git a/MyTracksLib/AndroidManifest.xml b/MyTracksLib/AndroidManifest.xml index 2b7da2548..ddbe1b69c 100644 --- a/MyTracksLib/AndroidManifest.xml +++ b/MyTracksLib/AndroidManifest.xml @@ -23,7 +23,7 @@ limitations under the License. See http://developer.android.com/guide/developing/eclipse-adt.html#libraryProject for details on what should go here vs in the application project. --> - + diff --git a/MyTracksLib/src/com/google/android/apps/mytracks/content/MyTracksProviderUtils.java b/MyTracksLib/src/com/google/android/apps/mytracks/content/MyTracksProviderUtils.java index a9506f1fc..b491e2158 100644 --- a/MyTracksLib/src/com/google/android/apps/mytracks/content/MyTracksProviderUtils.java +++ b/MyTracksLib/src/com/google/android/apps/mytracks/content/MyTracksProviderUtils.java @@ -1,12 +1,12 @@ /* * Copyright 2010 Google Inc. - * + * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the @@ -154,7 +154,7 @@ public interface MyTracksProviderUtils { /** * Creates a cursor over the locations in the track points provider which * iterates over a given range of unique ids. - * Caller gets to own the returned cursor. Don't forget to close it. + * Caller owns the returned cursor and is responsible for closing it. * * @param trackId the id of the track for which to get the points * @param minTrackPointId the minimum id for the track points @@ -168,7 +168,7 @@ public interface MyTracksProviderUtils { /** * Creates a cursor over the waypoints of a track. - * Caller gets to own the returned cursor. Don't forget to close it. + * Caller owns the returned cursor and is responsible for closing it. * * @param trackId the id of the track for which to get the points * @param minWaypointId the minimum id for the track points @@ -178,6 +178,18 @@ public interface MyTracksProviderUtils { Cursor getWaypointsCursor(long trackId, long minWaypointId, int maxWaypoints); + /** + * Creates a cursor over waypoints with the given selection. + * Caller owns the returned cursor and is responsible for closing it. + * + * @param selection a given selection + * @param selectionArgs arguments for the given selection + * @param order the order in which to return results + * @param maxWaypoints the maximum number of waypoints to return + * @return a cursor of the selected waypoints + */ + Cursor getWaypointsCursor(String selection, String[] selectionArgs, String order, int maxWaypoints); + /** * Finds a track by given unique track id. * Note that the returned track object does not have any track points attached. @@ -188,24 +200,26 @@ public interface MyTracksProviderUtils { * @return a Track object, or null if not found */ Track getTrack(long id); - + /** * Retrieves all tracks without track points. If no tracks exist, an empty * list will be returned. Use {@link #getLocationIterator(long, long, boolean, LocationFactory)} * to load the track points. - * + * * @return a list of all the recorded tracks */ List getAllTracks(); /** * Creates a cursor over the tracks provider with a given selection. - * Caller gets to own the returned cursor. Don't forget to close it. + * Caller owns the returned cursor and is responsible for closing it. * * @param selection a given selection + * @param selectionArgs parameters for the given selection + * @param order the order to return results in * @return a cursor of the selected tracks */ - Cursor getTracksCursor(String selection); + Cursor getTracksCursor(String selection, String[] selectionArgs, String order); /** * Inserts a track in the tracks provider. @@ -288,7 +302,7 @@ public interface MyTracksProviderUtils { /** * Fill a location object with values from a given cursor. - * + * * @param cursor a cursor pointing at a db or provider with locations * @param location a location object to be overwritten */ @@ -308,7 +322,7 @@ public interface MyTracksProviderUtils { interface LocationIterator extends Iterator { /** * Returns ID of the most recently retrieved track point through a call to {@link #next()}. - * + * * @return the ID of the most recent track point ID. */ long getLocationId(); @@ -328,12 +342,12 @@ public interface MyTracksProviderUtils { * Creates a new {@link Location} object to be populated from the underlying database record. * It's up to the implementing class to decide whether to create a new instance or reuse * existing to optimize for speed. - * + * * @return a {@link Location} to be populated from the database. */ Location createLocation(); } - + /** * The default {@class Location}s factory, which creates a new location of 'gps' type. */ @@ -361,20 +375,20 @@ public interface MyTracksProviderUtils { return locs[lastLoc]; } } - + /** * Creates a new read-only iterator over all track points for the given track. It provides * a lightweight way of iterating over long tracks without failing due to the underlying cursor * limitations. Since it's a read-only iterator, {@link Iterator#remove()} always throws * {@class UnsupportedOperationException}. - * + * * Each call to {@link LocationIterator#next()} may advance to the next DB record, and if so, * the iterator calls {@link LocationFactory#createLocation()} and populates it with information * retrieved from the record. - * + * * When done with iteration, you must call {@link LocationIterator#close()} to make sure that all * resources are properly deallocated. - * + * * Example use: * * ... @@ -389,14 +403,14 @@ public interface MyTracksProviderUtils { * } * ... * - * + * * @param trackId the ID of a track to retrieve locations for. * @param startTrackPointId the ID of the first track point to load, or -1 to start from * the first point. * @param descending if true the results will be returned in descending ID * order (latest location first). * @param locationFactory the factory for creating new locations. - * + * * @return the read-only iterator over the given track's points. */ LocationIterator getLocationIterator(long trackId, long startTrackPointId, boolean descending, diff --git a/MyTracksLib/src/com/google/android/apps/mytracks/content/MyTracksProviderUtilsImpl.java b/MyTracksLib/src/com/google/android/apps/mytracks/content/MyTracksProviderUtilsImpl.java index 44ff1891a..ee4cc81b6 100644 --- a/MyTracksLib/src/com/google/android/apps/mytracks/content/MyTracksProviderUtilsImpl.java +++ b/MyTracksLib/src/com/google/android/apps/mytracks/content/MyTracksProviderUtilsImpl.java @@ -16,7 +16,7 @@ package com.google.android.apps.mytracks.content; import static com.google.android.apps.mytracks.lib.MyTracksLibConstants.TAG; -import com.google.android.apps.mytracks.content.Sensor; + import com.google.android.apps.mytracks.stats.TripStatistics; import com.google.protobuf.InvalidProtocolBufferException; @@ -875,22 +875,34 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { } String selection; + String[] selectionArgs; if (minWaypointId > 0) { - selection = String.format("%s=%d AND %s>=%d", - WaypointsColumns.TRACKID, trackId, - WaypointsColumns._ID, minWaypointId); + selection = String.format("%s = ? AND %s >= ?", + WaypointsColumns.TRACKID, + WaypointsColumns._ID); + selectionArgs = new String[] { + Long.toString(trackId), + Long.toString(minWaypointId) + }; } else { - selection = String.format("%s=%d", - WaypointsColumns.TRACKID, trackId); + selection = String.format("%s=?", WaypointsColumns.TRACKID); + selectionArgs = new String[] { Long.toString(trackId) }; } - String sortOrder = "_id ASC"; + return getWaypointsCursor(selection, selectionArgs, null, maxWaypoints); + } + + @Override + public Cursor getWaypointsCursor(String selection, String[] selectionArgs, String order, int maxWaypoints) { + if (order == null) { + order = "_id ASC"; + } if (maxWaypoints > 0) { - sortOrder += " LIMIT " + maxWaypoints; + order += " LIMIT " + maxWaypoints; } return contentResolver.query( - WaypointsColumns.CONTENT_URI, null, selection, null, sortOrder); + WaypointsColumns.CONTENT_URI, null, selection, selectionArgs, order); } @Override @@ -905,7 +917,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { @Override public List getAllTracks() { - Cursor cursor = getTracksCursor(null); + Cursor cursor = getTracksCursor(null, null, TracksColumns._ID); ArrayList tracks = new ArrayList(); if (cursor != null) { tracks.ensureCapacity(cursor.getCount()); @@ -923,10 +935,9 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { } @Override - public Cursor getTracksCursor(String selection) { - Cursor cursor = contentResolver.query( - TracksColumns.CONTENT_URI, null, selection, null, "_id"); - return cursor; + public Cursor getTracksCursor(String selection, String[] selectionArgs, String order) { + return contentResolver.query( + TracksColumns.CONTENT_URI, null, selection, selectionArgs, order); } @Override diff --git a/MyTracksLib/src/com/google/android/apps/mytracks/stats/TripStatistics.java b/MyTracksLib/src/com/google/android/apps/mytracks/stats/TripStatistics.java index 09508bf41..dd318095e 100644 --- a/MyTracksLib/src/com/google/android/apps/mytracks/stats/TripStatistics.java +++ b/MyTracksLib/src/com/google/android/apps/mytracks/stats/TripStatistics.java @@ -1,12 +1,12 @@ /* * Copyright 2010 Google Inc. - * + * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the @@ -26,7 +26,7 @@ import android.os.Parcelable; * * @author Rodrigo Damazio */ -public class TripStatistics implements Parcelable { +public class TripStatistics implements Parcelable { /** * The start time for the trip. This is system time which might not match gps @@ -144,7 +144,7 @@ public class TripStatistics implements Parcelable { /** * Gets the time that this track started. - * + * * @return The number of milliseconds since epoch to the time when this track * started */ @@ -154,7 +154,7 @@ public class TripStatistics implements Parcelable { /** * Gets the time that this track stopped. - * + * * @return The number of milliseconds since epoch to the time when this track * stopped */ @@ -176,7 +176,7 @@ public class TripStatistics implements Parcelable { /** * Gets the total distance the user traveled. - * + * * @return The total distance traveled in meters */ public double getTotalDistance() { @@ -197,7 +197,7 @@ public class TripStatistics implements Parcelable { /** * Gets the the average speed the user traveled when they were actively * moving. - * + * * @return The average moving speed in m/s */ public double getAverageMovingSpeed() { @@ -206,7 +206,7 @@ public class TripStatistics implements Parcelable { /** * Gets the the maximum speed for this track. - * + * * @return The maximum speed in m/s */ public double getMaxSpeed() { @@ -215,7 +215,7 @@ public class TripStatistics implements Parcelable { /** * Gets the moving time. - * + * * @return The total number of milliseconds the user was moving */ public long getMovingTime() { @@ -225,7 +225,7 @@ public class TripStatistics implements Parcelable { /** * Gets the total elevation gain for this trip. This is calculated as the sum * of all positive differences in the smoothed elevation. - * + * * @return The elevation gain in meters for this trip */ public double getTotalElevationGain() { @@ -233,39 +233,79 @@ public class TripStatistics implements Parcelable { } /** - * Returns the leftmost position (lowest longitude) of the track, in signed - * decimal degrees. + * Returns the leftmost position (lowest longitude) of the track, in signed degrees. + */ + public double getLeftDegrees() { + return longitudeExtremities.getMin(); + } + + /** + * Returns the leftmost position (lowest longitude) of the track, in signed millions of degrees. */ public int getLeft() { return (int) (longitudeExtremities.getMin() * 1E6); } /** - * Returns the rightmost position (highest longitude) of the track, in signed - * decimal degrees. + * Returns the rightmost position (highest longitude) of the track, in signed degrees. + */ + public double getRightDegrees() { + return longitudeExtremities.getMax(); + } + + /** + * Returns the rightmost position (highest longitude) of the track, in signed millions of degrees. */ public int getRight() { return (int) (longitudeExtremities.getMax() * 1E6); } /** - * Returns the bottommost position (lowest latitude) of the track, in meters. + * Returns the bottommost position (lowest latitude) of the track, in signed degrees. + */ + public double getBottomDegrees() { + return latitudeExtremities.getMin(); + } + + /** + * Returns the bottommost position (lowest latitude) of the track, in signed millions of degrees. */ public int getBottom() { return (int) (latitudeExtremities.getMin() * 1E6); } /** - * Returns the topmost position (highest latitude) of the track, in meters. + * Returns the topmost position (highest latitude) of the track, in signed degrees. + */ + public double getTopDegrees() { + return latitudeExtremities.getMax(); + } + + /** + * Returns the topmost position (highest latitude) of the track, in signed millions of degrees. */ public int getTop() { return (int) (latitudeExtremities.getMax() * 1E6); } + /** + * Returns the mean position (center latitude) of the track, in signed degrees. + */ + public double getMeanLatitude() { + return (getBottomDegrees() + getTopDegrees()) / 2.0; + } + + /** + * Returns the mean position (center longitude) of the track, in signed degrees. + */ + public double getMeanLongitude() { + return (getLeftDegrees() + getRightDegrees()) / 2.0; + } + /** * Gets the minimum elevation seen on this trip. This is calculated from the * smoothed elevation so this can actually be more than the current elevation. - * + * * @return The smallest elevation reading for this trip in meters */ public double getMinElevation() { @@ -275,7 +315,7 @@ public class TripStatistics implements Parcelable { /** * Gets the maximum elevation seen on this trip. This is calculated from the * smoothed elevation so this can actually be less than the current elevation. - * + * * @return The largest elevation reading for this trip in meters */ public double getMaxElevation() { @@ -284,7 +324,7 @@ public class TripStatistics implements Parcelable { /** * Gets the maximum grade for this trip. - * + * * @return The maximum grade for this trip as a fraction */ public double getMaxGrade() { @@ -293,7 +333,7 @@ public class TripStatistics implements Parcelable { /** * Gets the minimum grade for this trip. - * + * * @return The minimum grade for this trip as a fraction */ public double getMinGrade() { @@ -443,7 +483,7 @@ public class TripStatistics implements Parcelable { void addMovingTime(long time) { movingTime += time; } - + /** * Accounts for a new latitude value for the bounding box. * diff --git a/MyTracksTest/AndroidManifest.xml b/MyTracksTest/AndroidManifest.xml index 2a83ff6a9..113e39ba2 100644 --- a/MyTracksTest/AndroidManifest.xml +++ b/MyTracksTest/AndroidManifest.xml @@ -4,7 +4,7 @@ android:versionCode="1" android:versionName="1.0" android:debuggable="true"> - + diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/content/SearchEngineTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/content/SearchEngineTest.java new file mode 100644 index 000000000..3fef63c1d --- /dev/null +++ b/MyTracksTest/src/com/google/android/apps/mytracks/content/SearchEngineTest.java @@ -0,0 +1,313 @@ +/** + * + */ +package com.google.android.apps.mytracks.content; + +import com.google.android.apps.mytracks.content.SearchEngine.ScoredResult; +import com.google.android.apps.mytracks.content.SearchEngine.SearchQuery; +import com.google.android.apps.mytracks.services.TrackRecordingServiceTest.MockContext; +import com.google.android.apps.mytracks.stats.TripStatistics; + +import android.content.ContentUris; +import android.location.Location; +import android.net.Uri; +import android.test.AndroidTestCase; +import android.test.RenamingDelegatingContext; +import android.test.mock.MockContentResolver; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Tests for {@link SearchEngine}. + * These are not meant to be quality tests, but instead feature-by-feature tests + * (in other words, they don't test the mixing of different score boostings, just + * each boosting separately) + * + * @author Rodrigo Damazio + */ +public class SearchEngineTest extends AndroidTestCase { + + private static final Location HERE = new Location("gps"); + private static final long NOW = 1234567890000L; // After OLDEST_ALLOWED_TIMESTAMP + private MyTracksProviderUtils providerUtils; + private SearchEngine engine; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + MockContentResolver mockContentResolver = new MockContentResolver(); + RenamingDelegatingContext targetContext = new RenamingDelegatingContext( + getContext(), getContext(), "test."); + MockContext context = new MockContext(mockContentResolver, targetContext); + MyTracksProvider provider = new MyTracksProvider(); + provider.attachInfo(context, null); + mockContentResolver.addProvider(MyTracksProviderUtils.AUTHORITY, provider); + setContext(context); + + providerUtils = MyTracksProviderUtils.Factory.get(context); + engine = new SearchEngine(providerUtils); + } + + @Override + protected void tearDown() throws Exception { + providerUtils.deleteAllTracks(); + + super.tearDown(); + } + + private long insertTrack(String title, String description, String category, double distance, long hoursAgo) { + Track track = new Track(); + track.setName(title); + track.setDescription(description); + track.setCategory(category); + + TripStatistics stats = track.getStatistics(); + if (hoursAgo > 0) { + // Started twice hoursAgo, so the average time is hoursAgo. + stats.setStartTime(NOW - hoursAgo * 1000L * 60L * 60L * 2); + stats.setStopTime(NOW); + } + + int latitude = (int) ((HERE.getLatitude() + distance) * 1E6); + int longitude = (int) ((HERE.getLongitude() + distance) * 1E6); + stats.setBounds(latitude, longitude, latitude, longitude); + + Uri uri = providerUtils.insertTrack(track); + return ContentUris.parseId(uri); + } + + private long insertTrack(String title, String description, String category) { + return insertTrack(title, description, category, 0, -1); + } + + private long insertTrack(String title, double distance) { + return insertTrack(title, "", "", distance, -1); + } + + private long insertTrack(String title, long hoursAgo) { + return insertTrack(title, "", "", 0.0, hoursAgo); + } + + private long insertWaypoint(String title, String description, String category, double distance, long hoursAgo, long trackId) { + Waypoint waypoint = new Waypoint(); + waypoint.setName(title); + waypoint.setDescription(description); + waypoint.setCategory(category); + waypoint.setTrackId(trackId); + + Location location = new Location(HERE); + location.setLatitude(location.getLatitude() + distance); + location.setLongitude(location.getLongitude() + distance); + if (hoursAgo >= 0) { + location.setTime(NOW - hoursAgo * 1000L * 60L * 60L); + } + waypoint.setLocation(location); + + Uri uri = providerUtils.insertWaypoint(waypoint); + return ContentUris.parseId(uri); + } + + private long insertWaypoint(String title, String description, String category) { + return insertWaypoint(title, description, category, 0.0, -1, -1); + } + + private long insertWaypoint(String title, double distance) { + return insertWaypoint(title, "", "", distance, -1, -1); + } + + private long insertWaypoint(String title, long hoursAgo) { + return insertWaypoint(title, "", "", 0.0, hoursAgo, -1); + } + + private long insertWaypoint(String title, long hoursAgo, long trackId) { + return insertWaypoint(title, "", "", 0.0, hoursAgo, trackId); + } + + public void testSearchText() { + // Insert 7 tracks (purposefully out of result order): + // - one which won't match + // - one which will match the description + // - one which will match the category + // - one which will match the title + // - one which will match in title and category + // - one which will match in title and description + // - one which will match in all fields + insertTrack("bb", "cc", "dd"); + long descriptionMatchId = insertTrack("bb", "aa", "cc"); + long categoryMatchId = insertTrack("bb", "cc", "aa"); + long titleMatchId = insertTrack("aa", "bb", "cc"); + long titleCategoryMatchId = insertTrack("aa", "bb", "ca"); + long titleDescriptionMatchId = insertTrack("aa", "ba", "cc"); + long allMatchId = insertTrack("aa", "ba", "ca"); + + SearchQuery query = new SearchQuery("a", null, -1, NOW); + ArrayList results = new ArrayList(engine.search(query)); + + // Title > Description > Category. + assertTrackResults(results, + allMatchId, titleDescriptionMatchId, titleCategoryMatchId, titleMatchId, descriptionMatchId, + categoryMatchId); + } + + public void testSearchWaypointText() { + // Insert 7 waypoints (purposefully out of result order): + // - one which won't match + // - one which will match the description + // - one which will match the category + // - one which will match the title + // - one which will match in title and category + // - one which will match in title and description + // - one which will match in all fields + insertWaypoint("bb", "cc", "dd"); + long descriptionMatchId = insertWaypoint("bb", "aa", "cc"); + long categoryMatchId = insertWaypoint("bb", "cc", "aa"); + long titleMatchId = insertWaypoint("aa", "bb", "cc"); + long titleCategoryMatchId = insertWaypoint("aa", "bb", "ca"); + long titleDescriptionMatchId = insertWaypoint("aa", "ba", "cc"); + long allMatchId = insertWaypoint("aa", "ba", "ca"); + + SearchQuery query = new SearchQuery("a", null, -1, NOW); + ArrayList results = new ArrayList(engine.search(query)); + + // Title > Description > Category. + assertWaypointResults(results, + allMatchId, titleDescriptionMatchId, titleCategoryMatchId, titleMatchId, descriptionMatchId, + categoryMatchId); + } + + public void testSearchMixedText() { + // Insert 5 entries (purposefully out of result order): + // - one waypoint which will match by description + // - one waypoint which won't match + // - one waypoint which will match by title + // - one track which won't match + // - one track which will match by title + long descriptionWaypointId = insertWaypoint("bb", "aa", "cc"); + insertWaypoint("bb", "cc", "dd"); + long titleWaypointId = insertWaypoint("aa", "bb", "cc"); + insertTrack("bb", "cc", "dd"); + long trackId = insertTrack("aa", "bb", "cc"); + + SearchQuery query = new SearchQuery("a", null, -1, NOW); + ArrayList results = new ArrayList(engine.search(query)); + + // Title > Description > Category. + assertEquals(results.toString(), 3, results.size()); + assertTrackResult(trackId, results.get(0)); + assertWaypointResult(titleWaypointId, results.get(1)); + assertWaypointResult(descriptionWaypointId, results.get(2)); + } + + public void testSearchTrackDistance() { + // All results match text, but they're at difference distances from the user. + long farFarAwayId = insertTrack("aa", 0.3); + long nearId = insertTrack("ab", 0.1); + long farId = insertTrack("ac", 0.2); + + SearchQuery query = new SearchQuery("a", HERE, -1, NOW); + ArrayList results = new ArrayList(engine.search(query)); + + // Distance order. + assertTrackResults(results, nearId, farId, farFarAwayId); + } + + public void testSearchWaypointDistance() { + // All results match text, but they're at difference distances from the user. + long farFarAwayId = insertWaypoint("aa", 0.3); + long nearId = insertWaypoint("ab", 0.1); + long farId = insertWaypoint("ac", 0.2); + + SearchQuery query = new SearchQuery("a", HERE, -1, NOW); + ArrayList results = new ArrayList(engine.search(query)); + + // Distance order. + assertWaypointResults(results, nearId, farId, farFarAwayId); + } + + public void testSearchTrackRecent() { + // All results match text, but they're were recorded at different times. + long oldestId = insertTrack("aa", 3); + long recentId = insertTrack("ab", 1); + long oldId = insertTrack("ac", 2); + + SearchQuery query = new SearchQuery("a", null, -1, NOW); + ArrayList results = new ArrayList(engine.search(query)); + + // Reverse time order. + assertTrackResults(results, recentId, oldId, oldestId); + } + + public void testSearchWaypointRecent() { + // All results match text, but they're were recorded at different times. + long oldestId = insertWaypoint("aa", 2); + long recentId = insertWaypoint("ab", 0); + long oldId = insertWaypoint("ac", 1); + + SearchQuery query = new SearchQuery("a", null, -1, NOW); + ArrayList results = new ArrayList(engine.search(query)); + + // Reverse time order. + assertWaypointResults(results, recentId, oldId, oldestId); + } + + public void testSearchCurrentTrack() { + // All results match text, but one of them is the current track. + long currentId = insertTrack("ab", 1); + long otherId = insertTrack("aa", 1); + + SearchQuery query = new SearchQuery("a", null, currentId, NOW); + ArrayList results = new ArrayList(engine.search(query)); + + // Current track should be demoted. + assertTrackResults(results, otherId, currentId); + } + + public void testSearchCurrentTrackWaypoint() { + // All results match text, but one of them is in the current track. + long otherId = insertWaypoint("aa", 1, 456); + long currentId = insertWaypoint("ab", 1, 123); + + SearchQuery query = new SearchQuery("a", null, 123, NOW); + ArrayList results = new ArrayList(engine.search(query)); + + // Waypoint in current track should be promoted. + assertWaypointResults(results, currentId, otherId); + } + + private void assertTrackResult(long trackId, ScoredResult result) { + assertNotNull("Not a track", result.track); + assertNull("Ambiguous result", result.waypoint); + assertEquals(trackId, result.track.getId()); + } + + private void assertTrackResults(List results, long... trackIds) { + String errMsg = "Expected IDs=" + Arrays.toString(trackIds) + "; results=" + results; + assertEquals(results.size(), trackIds.length); + for (int i = 0; i < results.size(); i++) { + ScoredResult result = results.get(i); + assertNotNull(errMsg, result.track); + assertNull(errMsg, result.waypoint); + assertEquals(errMsg, trackIds[i], result.track.getId()); + } + } + + private void assertWaypointResult(long waypointId, ScoredResult result) { + assertNotNull("Not a waypoint", result.waypoint); + assertNull("Ambiguous result", result.track); + assertEquals(waypointId, result.waypoint.getId()); + } + + private void assertWaypointResults(List results, long... waypointIds) { + String errMsg = "Expected IDs=" + Arrays.toString(waypointIds) + "; results=" + results; + assertEquals(results.size(), waypointIds.length); + for (int i = 0; i < results.size(); i++) { + ScoredResult result = results.get(i); + assertNotNull(errMsg, result.waypoint); + assertNull(errMsg, result.track); + assertEquals(errMsg, waypointIds[i], result.waypoint.getId()); + } + } +} diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/io/docs/DocsHelperTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/io/docs/DocsHelperTest.java deleted file mode 100644 index 93d1d3b03..000000000 --- a/MyTracksTest/src/com/google/android/apps/mytracks/io/docs/DocsHelperTest.java +++ /dev/null @@ -1,102 +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.io.docs; - -import com.google.android.apps.mytracks.io.gdata.GDataWrapper; -import com.google.wireless.gdata.client.GDataServiceClient; - -import android.content.Context; -import android.test.mock.MockContext; - -import junit.framework.TestCase; - -import java.io.IOException; - -/** - * Tests for {@link DocsHelper}, with the exception of - * {@link DocsHelper#addTrackRow}, which is in - * {@link DocsHelper_AddTrackRowTest}. - * - * @author Matthew Simmons - */ -public class DocsHelperTest extends TestCase { - private Context mockContext = new MockContext(); - - // TODO(simmonmt): Use AndroidMock to mock this class. We do it the hard - // way because use of AndroidMock to mock GDataWrapper triggers a compile - // error in an unrelated file. Specifically, it causes a NoClassDefFound - // exception for com.google.wireless.gdata2.client.AuthenticationException, - // (wrongly) attributed to the first source file in the project. Using the - // @UsesMocks(GDataWrapper.class) annotation is enough -- you don't have to - // touch AndroidMock at all to get this failure. - // The bug is filed with Android Mock as - // http://code.google.com/p/android-mock/issues/detail?id=3 - private class MockGDataWrapper extends GDataWrapper { - private final boolean returnValue; - - MockGDataWrapper(boolean returnValue) { - this.returnValue = returnValue; - } - - @Override - public boolean runQuery(QueryFunction queryFunction) { - return returnValue; - } - } - - public void testCreateSpreadsheet_noException() throws Exception { - // Our mock GDataWrapper isn't able to affect the return value from - // DocsHelper#createSpreadsheet. As such, we're only able to simulate the - // case where there weren't any GData errors, but not spreadsheet ID was - // actually returned. createSpreadsheet is defined to return null in that - // situation. - assertNull(new DocsHelper().createSpreadsheet( - mockContext, new MockGDataWrapper(true), "sheetName")); - } - - public void testCreateSpreadsheet_exception() throws Exception { - try { - new DocsHelper().createSpreadsheet( - mockContext, new MockGDataWrapper(false), "sheetName"); - fail(); - } catch (IOException expected) {} - } - - public void testRequestSpreadsheetId_noException() throws Exception { - assertNull(new DocsHelper().requestSpreadsheetId( - new MockGDataWrapper(true), "sheetName")); - } - - public void testRequestSpreadsheetId_exception() throws Exception { - try { - new DocsHelper().requestSpreadsheetId(new MockGDataWrapper(false), - "sheetName"); - fail(); - } catch (IOException expected) {} - } - - public void testGetWorksheetId_noException() throws Exception { - assertNull(new DocsHelper().getWorksheetId(new MockGDataWrapper(true), - "sheetId")); - } - - public void testGetWorksheetId_exception() throws Exception { - try { - new DocsHelper().getWorksheetId(new MockGDataWrapper(false), "sheetId"); - fail(); - } catch (IOException expected) {} - } -} diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/io/docs/DocsHelper_AddTrackRowTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/io/docs/DocsHelper_AddTrackRowTest.java deleted file mode 100644 index 541717330..000000000 --- a/MyTracksTest/src/com/google/android/apps/mytracks/io/docs/DocsHelper_AddTrackRowTest.java +++ /dev/null @@ -1,128 +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.io.docs; - -import com.google.android.apps.mytracks.content.Track; -import com.google.android.apps.mytracks.io.AuthManager; -import com.google.android.apps.mytracks.stats.TripStatistics; -import com.google.android.apps.mytracks.util.StringUtils; -import com.google.android.testing.mocking.AndroidMock; -import com.google.android.testing.mocking.UsesMocks; - -import android.test.AndroidTestCase; -import android.test.mock.MockResources; - -import java.io.IOException; - -/** - * Tests for {@link DocsHelper#addTrackRow} - * - * @author Matthew Simmons - */ -public class DocsHelper_AddTrackRowTest extends AndroidTestCase { - private static final long TIME = 1288721514000L; - - private static class StringWritingDocsHelper extends DocsHelper { - String writtenSheetUri = null; - String writtenData = null; - - @Override - protected void writeRowData(AuthManager trixAuth, String worksheetUri, - String postText) { - writtenSheetUri = worksheetUri; - writtenData = postText; - } - } - - public void testAddTrackRow_imperial() throws Exception { - StringWritingDocsHelper docsHelper = new StringWritingDocsHelper(); - addTrackRow(docsHelper, false); - - String expectedData = - "" - + "" - + "" - + "" - + "" - + "" - + "" - + "" - + "" - + "" - + "" - + "" - + "" - + "" - + "" - + "" - + "" - + "" - + "" - + "" - + ""; - - assertEquals( - "https://spreadsheets.google.com/feeds/list/ssid/wsid/private/full", - docsHelper.writtenSheetUri); - assertEquals(expectedData, docsHelper.writtenData); - } - - public void testAddTrackRow_metric() throws Exception { - StringWritingDocsHelper docsHelper = new StringWritingDocsHelper(); - addTrackRow(docsHelper, true); - - // The imperial test verifies that the tags come out in the proper order, - // and with the proper names. We need only verify that the labels are - // correct, and that at least one of the unit-dependent value tags is - // correct. - assertTrue(docsHelper.writtenData.contains( - "")); - assertTrue(docsHelper.writtenData.contains( - "")); - assertTrue(docsHelper.writtenData.contains( - "")); - - assertTrue(docsHelper.writtenData.contains( - "")); - } - - /** Adds a row to the spreadsheet, using the provided helper. */ - @UsesMocks({AuthManager.class, MockResources.class, Track.class}) - private void addTrackRow(DocsHelper docsHelper, boolean useMetric) throws IOException { - AuthManager mockAuthManager = AndroidMock.createMock(AuthManager.class); - AndroidMock.replay(mockAuthManager); - - TripStatistics stats = new TripStatistics(); - stats.setStartTime(TIME); - stats.setTotalTime(5000); - stats.setMovingTime(4000); - stats.setTotalDistance(20000); - stats.setMaxSpeed(1500); - stats.setTotalElevationGain(6000); - stats.setMinElevation(-500); - stats.setMaxElevation(550); - - Track track = new Track(); - track.setName("trackName"); - track.setDescription("trackDescription"); - track.setMapId("trackMapId"); - track.setStatistics(stats); - - docsHelper.addTrackRow(getContext(), mockAuthManager, "ssid", "wsid", - track, useMetric); - } -} diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/io/docs/DocsTagBuilderTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/io/docs/DocsTagBuilderTest.java deleted file mode 100644 index 0145f6a9e..000000000 --- a/MyTracksTest/src/com/google/android/apps/mytracks/io/docs/DocsTagBuilderTest.java +++ /dev/null @@ -1,60 +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.io.docs; - -import junit.framework.TestCase; - -/** - * This class tests {@link DocsTagBuilder} - * - * @author Matthew Simmons - */ -public class DocsTagBuilderTest extends TestCase { - public void testAppend() { - assertEquals("", - new DocsTagBuilder(false).append("tag", "value").build()); - } - - public void testMultiAppend() { - String actual = new DocsTagBuilder(false) - .append("tag1", "value1") - .append("tag2", "value2") - .build(); - - assertEquals("" - + "", actual); - } - - public void testAppendLargeUnits_imperial() { - assertEquals("", - new DocsTagBuilder(false).appendLargeUnits("tag", 100.0).build()); - } - - public void testAppendLargeUnits_metric() { - assertEquals("", - new DocsTagBuilder(true).appendLargeUnits("tag", 100.0).build()); - } - - public void testAppendSmallUnits_imperial() { - assertEquals("", - new DocsTagBuilder(false).appendSmallUnits("tag", 100.0).build()); - } - - public void testAppendSmallUnits_metric() { - assertEquals("", - new DocsTagBuilder(true).appendSmallUnits("tag", 100.0).build()); - } -} diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/io/docs/SendDocsUtilsTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/io/docs/SendDocsUtilsTest.java new file mode 100644 index 000000000..7823269b9 --- /dev/null +++ b/MyTracksTest/src/com/google/android/apps/mytracks/io/docs/SendDocsUtilsTest.java @@ -0,0 +1,246 @@ +/* + * Copyright 2012 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.docs; + +import com.google.android.apps.mytracks.content.Track; +import com.google.android.apps.mytracks.io.gdata.docs.SpreadsheetsClient.WorksheetEntry; +import com.google.android.apps.mytracks.stats.TripStatistics; +import com.google.android.apps.mytracks.util.StringUtils; +import com.google.wireless.gdata.data.Entry; + +import android.test.AndroidTestCase; + +/** + * Tests {@link SendDocsUtils}. + * + * @author jshih@google.com (Jimmy Shih) + */ +public class SendDocsUtilsTest extends AndroidTestCase { + + private static final long TIME = 1288721514000L; + + /** + * Tests {@link SendDocsUtils#getEntryId(Entry)} with a valid id. + */ + public void testGetEntryId_valid() { + Entry entry = new Entry(); + entry.setId("https://docs.google.com/feeds/documents/private/full/spreadsheet%3A123"); + assertEquals("123", SendDocsUtils.getEntryId(entry)); + } + + /** + * Tests {@link SendDocsUtils#getEntryId(Entry)} with an invalid id. + */ + public void testGetEntryId_invalid() { + Entry entry = new Entry(); + entry.setId("123"); + assertEquals(null, SendDocsUtils.getEntryId(entry)); + } + + /** + * Tests {@link SendDocsUtils#getNewSpreadsheetId(String)} with a valid id. + */ + public void testGetNewSpreadsheetId_valid_id() { + assertEquals("123", SendDocsUtils.getNewSpreadsheetId( + "https://docs.google.com/feeds/documents/private/full/spreadsheet%3A123")); + } + + /** + * Tests {@link SendDocsUtils#getNewSpreadsheetId(String)} without an open + * tag, . + */ + public void testGetNewSpreadsheetId_no_open_tag() { + assertEquals(null, SendDocsUtils.getNewSpreadsheetId( + "https://docs.google.com/feeds/documents/private/full/spreadsheet%3A123")); + } + + /** + * Tests {@link SendDocsUtils#getNewSpreadsheetId(String)} without an end tag, + * . + */ + public void testGetNewSpreadsheetId_no_end_tag() { + assertEquals(null, SendDocsUtils.getNewSpreadsheetId( + "https://docs.google.com/feeds/documents/private/full/spreadsheet%3A123")); + } + + /** + * Tests {@link SendDocsUtils#getNewSpreadsheetId(String)} without the url + * prefix, + * https://docs.google.com/feeds/documents/private/full/spreadsheet%3A. + */ + public void testGetNewSpreadsheetId_no_prefix() { + assertEquals(null, SendDocsUtils.getNewSpreadsheetId("123")); + } + + /** + * Tests {@link SendDocsUtils#getWorksheetEntryId(WorksheetEntry)} with a + * valid entry. + */ + public void testGetWorksheetEntryId_valid() { + WorksheetEntry entry = new WorksheetEntry(); + entry.setId("/123"); + assertEquals("123", SendDocsUtils.getWorksheetEntryId(entry)); + } + + /** + * Tests {@link SendDocsUtils#getWorksheetEntryId(WorksheetEntry)} with a + * invalid entry. + */ + public void testGetWorksheetEntryId_invalid() { + WorksheetEntry entry = new WorksheetEntry(); + entry.setId("123"); + assertEquals(null, SendDocsUtils.getWorksheetEntryId(entry)); + } + + /** + * Tests {@link SendDocsUtils#getRowContent(Track, boolean, + * android.content.Context)} with metric units. + */ + public void testGetRowContent_metric() throws Exception { + Track track = getTrack(); + String expectedData = "" + + "" + + "" + "" + + "" + + "" + + "" + + "" + + "" + + "" + "" + + "" + + "" + + "" + + "" + + "" + + "" + "" + + "" + "" + + ""; + assertEquals(expectedData, SendDocsUtils.getRowContent(track, true, getContext())); + } + + /** + * Tests {@link SendDocsUtils#getRowContent(Track, boolean, + * android.content.Context)} with imperial units. + */ + public void testGetRowContent_imperial() throws Exception { + Track track = getTrack(); + String expectedData = "" + + "" + + "" + "" + + "" + + "" + + "" + + "" + + "" + + "" + "" + + "" + + "" + + "" + + "" + + "" + + "" + "" + + "" + "" + + ""; + assertEquals(expectedData, SendDocsUtils.getRowContent(track, false, getContext())); + } + + /** + * Gets a track for testing {@link SendDocsUtils#getRowContent(Track, boolean, + * android.content.Context)}. + */ + private Track getTrack() { + TripStatistics stats = new TripStatistics(); + stats.setStartTime(TIME); + stats.setTotalTime(5000); + stats.setMovingTime(4000); + stats.setTotalDistance(20000); + stats.setMaxSpeed(1500); + stats.setTotalElevationGain(6000); + stats.setMinElevation(-500); + stats.setMaxElevation(550); + + Track track = new Track(); + track.setName("trackName"); + track.setDescription("trackDescription"); + track.setMapId("trackMapId"); + track.setStatistics(stats); + return track; + } + + /** + * Tests {@link SendDocsUtils#appendTag(StringBuilder, String, String)} with + * repeated calls. + */ + public void testAppendTag() { + StringBuilder stringBuilder = new StringBuilder(); + SendDocsUtils.appendTag(stringBuilder, "name1", "value1"); + assertEquals("", stringBuilder.toString()); + + SendDocsUtils.appendTag(stringBuilder, "name2", "value2"); + assertEquals( + "", + stringBuilder.toString()); + } + + /** + * Tests {@link SendDocsUtils#getDistance(double, boolean)} with metric units. + */ + public void testGetDistance_metric() { + assertEquals("1.22", SendDocsUtils.getDistance(1222.3, true)); + } + + /** + * Tests {@link SendDocsUtils#getDistance(double, boolean)} with imperial + * units. + */ + public void testGetDistance_imperial() { + assertEquals("0.76", SendDocsUtils.getDistance(1222.3, false)); + } + + /** + * Tests {@link SendDocsUtils#getSpeed(double, boolean)} with metric units. + */ + public void testGetSpeed_metric() { + assertEquals("15.55", SendDocsUtils.getSpeed(4.32, true)); + } + + /** + * Tests {@link SendDocsUtils#getSpeed(double, boolean)} with imperial units. + */ + public void testGetSpeed_imperial() { + assertEquals("9.66", SendDocsUtils.getSpeed(4.32, false)); + } + + /** + * Tests {@link SendDocsUtils#getElevation(double, boolean)} with metric + * units. + */ + public void testGetElevation_metric() { + assertEquals("3", SendDocsUtils.getElevation(3.456, true)); + } + + /** + * Tests {@link SendDocsUtils#getElevation(double, boolean)} with imperial + * units. + */ + public void testGetElevation_imperial() { + assertEquals("11", SendDocsUtils.getElevation(3.456, false)); + } +} diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/io/file/TrackWriterTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/io/file/TrackWriterTest.java index 55204969f..92c830953 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/io/file/TrackWriterTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/io/file/TrackWriterTest.java @@ -20,6 +20,7 @@ import android.test.RenamingDelegatingContext; import android.test.mock.MockContentResolver; import java.io.ByteArrayOutputStream; +import java.io.File; import java.io.FileNotFoundException; import java.io.OutputStream; @@ -59,6 +60,10 @@ public class TrackWriterTest extends AndroidTestCase { this.stream = stream; this.canWrite = canWrite; + + // The directory is set in the canWriteFile. However, this class + // overwrites canWriteFile, thus needs to set it. + setDirectory(new File("/")); } @Override diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/io/fusiontables/SendFusionTablesUtilsTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/io/fusiontables/SendFusionTablesUtilsTest.java new file mode 100644 index 000000000..a75aca1a1 --- /dev/null +++ b/MyTracksTest/src/com/google/android/apps/mytracks/io/fusiontables/SendFusionTablesUtilsTest.java @@ -0,0 +1,286 @@ +/* + * Copyright 2012 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.fusiontables; + +import com.google.android.apps.mytracks.content.Track; +import com.google.android.apps.mytracks.io.fusiontables.SendFusionTablesUtils; +import com.google.android.apps.mytracks.stats.TripStatistics; + +import android.location.Location; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.util.ArrayList; + +import junit.framework.TestCase; + +/** + * Tests {@link SendFusionTablesUtils}. + * + * @author jshih@google.com (Jimmy Shih) + */ +public class SendFusionTablesUtilsTest extends TestCase { + + /** + * Tests {@link SendFusionTablesUtils#getMapUrl(Track)} with null track. + */ + public void testGetMapUrl_null_track() { + assertEquals(null, SendFusionTablesUtils.getMapUrl(null)); + } + + /** + * Tests {@link SendFusionTablesUtils#getMapUrl(Track)} with null table id. + */ + public void testGetMapUrl_null_table_id() { + Track track = new Track(); + TripStatistics stats = new TripStatistics(); + stats.setBounds((int) 100.E6, (int) 10.E6, (int) 50.E6, (int) 5.E6); + track.setStatistics(stats); + track.setTableId(null); + assertEquals(null, SendFusionTablesUtils.getMapUrl(track)); + } + + /** + * Tests {@link SendFusionTablesUtils#getMapUrl(Track)} with null stats. + */ + public void testGetMapUrl_null_stats() { + Track track = new Track(); + track.setStatistics(null); + track.setTableId("123"); + assertEquals(null, SendFusionTablesUtils.getMapUrl(track)); + } + + /** + * Tests {@link SendFusionTablesUtils#getMapUrl(Track)} with a valid track. + */ + public void testGetMapUrl_valid_track() { + Track track = new Track(); + TripStatistics stats = new TripStatistics(); + stats.setBounds((int) 100.E6, (int) 10.E6, (int) 50.E6, (int) 5.E6); + track.setStatistics(stats); + track.setTableId("123"); + assertEquals( + "https://www.google.com/fusiontables/embedviz?" + + "viz=MAP&q=select+col0,+col1,+col2,+col3+from+123+&h=false&lat=7.500000&lng=75.000000" + + "&z=15&t=1&l=col2", SendFusionTablesUtils.getMapUrl(track)); + } + + /** + * Tests (@link {@link SendFusionTablesUtils#formatSqlValues(String...)} with + * no value. + */ + public void testFormatSqlValues_no_value() { + assertEquals("()", SendFusionTablesUtils.formatSqlValues()); + } + + /** + * Tests (@link {@link SendFusionTablesUtils#formatSqlValues(String...)} with + * an empty string. + */ + public void testFormatSqlValues_empty_string() { + assertEquals("(\'\')", SendFusionTablesUtils.formatSqlValues("")); + } + + /** + * Tests (@link {@link SendFusionTablesUtils#formatSqlValues(String...)} with + * one value. + */ + public void testFormatSqlValues_one_value() { + assertEquals("(\'first\')", SendFusionTablesUtils.formatSqlValues("first")); + } + + /** + * Tests (@link {@link SendFusionTablesUtils#formatSqlValues(String...)} with + * two values. + */ + public void testFormatSqlValues_two_values() { + assertEquals( + "(\'first\',\'second\')", SendFusionTablesUtils.formatSqlValues("first", "second")); + } + + /** + * Tests {@link SendFusionTablesUtils#escapeSqlString(String)} with a value + * containing several single quotes. + */ + public void testEscapeSqValuel() { + String value = "let's'"; + assertEquals("let''s''", SendFusionTablesUtils.escapeSqlString(value)); + } + + /** + * Tests {@link SendFusionTablesUtils#getKmlPoint(Location)} with a null + * point. + */ + public void testGetKmlPoint_null_point() { + assertEquals( + "", SendFusionTablesUtils.getKmlPoint(null)); + } + + /** + * Tests {@link SendFusionTablesUtils#getKmlPoint(Location)} with a valid + * point. + */ + public void testGetKmlPoint_valid_point() { + Location location = new Location("test"); + location.setLongitude(10.1); + location.setLatitude(20.2); + location.setAltitude(30.3); + assertEquals("10.1,20.2,30.3", + SendFusionTablesUtils.getKmlPoint(location)); + } + + /** + * Tests {@link SendFusionTablesUtils#getKmlLineString(ArrayList)} with a null + * locations. + */ + public void testKmlLineString_null_locations() { + assertEquals("", + SendFusionTablesUtils.getKmlLineString(null)); + } + + /** + * Tests {@link SendFusionTablesUtils#getKmlLineString(ArrayList)} with no + * location. + */ + public void testKmlLineString_no_location() { + ArrayList locations = new ArrayList(); + assertEquals("", + SendFusionTablesUtils.getKmlLineString(locations)); + } + + /** + * Tests {@link SendFusionTablesUtils#getKmlLineString(ArrayList)} with one + * location. + */ + public void testKmlLineString_one_location() { + ArrayList locations = new ArrayList(); + Location location = new Location("test"); + location.setLongitude(10.1); + location.setLatitude(20.2); + location.setAltitude(30.3); + locations.add(location); + assertEquals("10.1,20.2,30.3", + SendFusionTablesUtils.getKmlLineString(locations)); + } + + /** + * Tests {@link SendFusionTablesUtils#getKmlLineString(ArrayList)} with two + * locations. + */ + public void testKmlLineString_two_locations() { + ArrayList locations = new ArrayList(); + Location location1 = new Location("test"); + location1.setLongitude(10.1); + location1.setLatitude(20.2); + location1.setAltitude(30.3); + locations.add(location1); + + Location location2 = new Location("test"); + location2.setLongitude(1.1); + location2.setLatitude(2.2); + location2.removeAltitude(); + locations.add(location2); + + assertEquals("10.1,20.2,30.3 1.1,2.2", + SendFusionTablesUtils.getKmlLineString(locations)); + } + + /** + * Tests {@link SendFusionTablesUtils#appendLocation(Location, StringBuilder)} + * with no altitude. + */ + public void testAppendLocation_no_altitude() { + StringBuilder builder = new StringBuilder(); + Location location = new Location("test"); + location.setLongitude(10.1); + location.setLatitude(20.2); + location.removeAltitude(); + SendFusionTablesUtils.appendLocation(location, builder); + assertEquals("10.1,20.2", builder.toString()); + } + + /** + * Tests {@link SendFusionTablesUtils#appendLocation(Location, StringBuilder)} + * with altitude. + */ + public void testAppendLocation_has_altitude() { + StringBuilder builder = new StringBuilder(); + Location location = new Location("test"); + location.setLongitude(10.1); + location.setLatitude(20.2); + location.setAltitude(30.3); + SendFusionTablesUtils.appendLocation(location, builder); + assertEquals("10.1,20.2,30.3", builder.toString()); + } + + /** + * Tests {@link SendFusionTablesUtils#getTableId(InputStream)} with a null + * inputstream. + */ + public void testGetTableId_null() { + assertEquals(null, SendFusionTablesUtils.getTableId(null)); + } + + /** + * Tests {@link SendFusionTablesUtils#getTableId(InputStream)} with an + * inputstream containing no data. + */ + public void testGetTableId_no_data() { + InputStream inputStream = new ByteArrayInputStream(new byte[0]); + assertEquals(null, SendFusionTablesUtils.getTableId(inputStream)); + } + + /** + * Tests {@link SendFusionTablesUtils#getTableId(InputStream)} with an empty + * inputstream. + */ + public void testGetTableId_empty() { + String string = ""; + InputStream inputStream = new ByteArrayInputStream(string.getBytes()); + assertEquals(null, SendFusionTablesUtils.getTableId(inputStream)); + } + + /** + * Tests {@link SendFusionTablesUtils#getTableId(InputStream)} with an one + * line inputstream. + */ + public void testGetTableId_one_line() { + String string = "tableid"; + InputStream inputStream = new ByteArrayInputStream(string.getBytes()); + assertEquals(null, SendFusionTablesUtils.getTableId(inputStream)); + } + + /** + * Tests {@link SendFusionTablesUtils#getTableId(InputStream)} with an + * inputstream not containing "tableid". + */ + public void testGetTableId_no_table_id() { + String string = "error\n123"; + InputStream inputStream = new ByteArrayInputStream(string.getBytes()); + assertEquals(null, SendFusionTablesUtils.getTableId(inputStream)); + } + + /** + * Tests {@link SendFusionTablesUtils#getTableId(InputStream)} with a valid + * inputstream. + */ + public void testGetTableId() { + String string = "tableid\n123"; + InputStream inputStream = null; + inputStream = new ByteArrayInputStream(string.getBytes()); + assertEquals("123", SendFusionTablesUtils.getTableId(inputStream)); + } +} \ No newline at end of file diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/io/maps/SendMapsUtilsTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/io/maps/SendMapsUtilsTest.java new file mode 100644 index 000000000..a22e09a60 --- /dev/null +++ b/MyTracksTest/src/com/google/android/apps/mytracks/io/maps/SendMapsUtilsTest.java @@ -0,0 +1,179 @@ +/* + * Copyright 2012 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.maps; + +import com.google.android.apps.mytracks.content.Track; +import com.google.android.apps.mytracks.io.gdata.maps.MapsFeature; +import com.google.android.maps.GeoPoint; + +import android.location.Location; + +import java.util.ArrayList; + +import junit.framework.TestCase; + +/** + * Tests {@link SendMapsUtils}. + * + * @author Jimmy Shih + */ +public class SendMapsUtilsTest extends TestCase { + + /** + * Tests {@link SendMapsUtils#getMapUrl(Track)} with null track. + */ + public void testGetMapUrl_null_track() { + assertEquals(null, SendMapsUtils.getMapUrl(null)); + } + + /** + * Tests {@link SendMapsUtils#getMapUrl(Track)} with null map id. + */ + public void testGetMapUrl_null_map_id() { + Track track = new Track(); + track.setMapId(null); + assertEquals(null, SendMapsUtils.getMapUrl(track)); + } + + /** + * Tests {@link SendMapsUtils#getMapUrl(Track)} with a valid track. + */ + public void testGetMapUrl_valid_track() { + Track track = new Track(); + track.setMapId("123"); + assertEquals("https://maps.google.com/maps/ms?msa=0&msid=123", SendMapsUtils.getMapUrl(track)); + } + + /** + * Test {@link SendMapsUtils#buildMapsMarkerFeature(String, String, String, + * GeoPoint)} with a title. + */ + public void testBuildMapsMarkerFeature_with_title() { + MapsFeature mapFeature = SendMapsUtils.buildMapsMarkerFeature( + "name", "this\nmap\ndescription", "url", new GeoPoint(123, 456)); + assertEquals(MapsFeature.MARKER, mapFeature.getType()); + assertNotNull(mapFeature.getAndroidId()); + assertEquals("name", mapFeature.getTitle()); + assertEquals("this
map
description", mapFeature.getDescription()); + assertEquals("url", mapFeature.getIconUrl()); + assertEquals(123, mapFeature.getPoint(0).getLatitudeE6()); + assertEquals(456, mapFeature.getPoint(0).getLongitudeE6()); + } + + /** + * Test {@link SendMapsUtils#buildMapsMarkerFeature(String, String, String, + * GeoPoint)} with an empty title. + */ + public void testBuildMapsMarkerFeature_empty_title() { + MapsFeature mapFeature = SendMapsUtils.buildMapsMarkerFeature( + "", "description", "url", new GeoPoint(123, 456)); + assertEquals(MapsFeature.MARKER, mapFeature.getType()); + assertNotNull(mapFeature.getAndroidId()); + assertEquals("-", mapFeature.getTitle()); + assertEquals("description", mapFeature.getDescription()); + assertEquals("url", mapFeature.getIconUrl()); + assertEquals(123, mapFeature.getPoint(0).getLatitudeE6()); + assertEquals(456, mapFeature.getPoint(0).getLongitudeE6()); + } + + /** + * Test {@link SendMapsUtils#buildMapsMarkerFeature(String, String, String, + * GeoPoint)} with a null title. + */ + public void testBuildMapsMarkerFeature_null_title() { + MapsFeature mapFeature = SendMapsUtils.buildMapsMarkerFeature( + null, "description", "url", new GeoPoint(123, 456)); + assertEquals(MapsFeature.MARKER, mapFeature.getType()); + assertNotNull(mapFeature.getAndroidId()); + assertEquals("-", mapFeature.getTitle()); + assertEquals("description", mapFeature.getDescription()); + assertEquals("url", mapFeature.getIconUrl()); + assertEquals(123, mapFeature.getPoint(0).getLatitudeE6()); + assertEquals(456, mapFeature.getPoint(0).getLongitudeE6()); + } + + /** + * Test {@link SendMapsUtils#buildMapsLineFeature(String, ArrayList)} with a + * title. + */ + public void testBuildMapsLineFeature_with_title() { + ArrayList locations = new ArrayList(); + Location location = new Location("test"); + location.setLatitude(50); + location.setLongitude(100); + locations.add(location); + MapsFeature mapFeature = SendMapsUtils.buildMapsLineFeature("name", locations); + + assertEquals(MapsFeature.LINE, mapFeature.getType()); + assertNotNull(mapFeature.getAndroidId()); + assertEquals("name", mapFeature.getTitle()); + assertEquals(0x80FF0000, mapFeature.getColor()); + assertEquals(50000000, mapFeature.getPoint(0).getLatitudeE6()); + assertEquals(100000000, mapFeature.getPoint(0).getLongitudeE6()); + } + + /** + * Test {@link SendMapsUtils#buildMapsLineFeature(String, ArrayList)} with an + * empty title. + */ + public void testBuildMapsLineFeature_empty_title() { + ArrayList locations = new ArrayList(); + Location location = new Location("test"); + location.setLatitude(50); + location.setLongitude(100); + locations.add(location); + MapsFeature mapFeature = SendMapsUtils.buildMapsLineFeature("", locations); + + assertEquals(MapsFeature.LINE, mapFeature.getType()); + assertNotNull(mapFeature.getAndroidId()); + assertEquals("-", mapFeature.getTitle()); + assertEquals(0x80FF0000, mapFeature.getColor()); + assertEquals(50000000, mapFeature.getPoint(0).getLatitudeE6()); + assertEquals(100000000, mapFeature.getPoint(0).getLongitudeE6()); + } + + /** + * Test {@link SendMapsUtils#buildMapsLineFeature(String, ArrayList)} with a + * null title. + */ + public void testBuildMapsLineFeature_null_title() { + ArrayList locations = new ArrayList(); + Location location = new Location("test"); + location.setLatitude(50); + location.setLongitude(100); + locations.add(location); + MapsFeature mapFeature = SendMapsUtils.buildMapsLineFeature(null, locations); + + assertEquals(MapsFeature.LINE, mapFeature.getType()); + assertNotNull(mapFeature.getAndroidId()); + assertEquals("-", mapFeature.getTitle()); + assertEquals(0x80FF0000, mapFeature.getColor()); + assertEquals(50000000, mapFeature.getPoint(0).getLatitudeE6()); + assertEquals(100000000, mapFeature.getPoint(0).getLongitudeE6()); + } + + /** + * Test {@link SendMapsUtils#getGeoPoint(Location)}. + */ + public void testGeoPoint() { + Location location = new Location("test"); + location.setLatitude(50); + location.setLongitude(100); + GeoPoint geoPoint = SendMapsUtils.getGeoPoint(location); + assertEquals(50000000, geoPoint.getLatitudeE6()); + assertEquals(100000000, geoPoint.getLongitudeE6()); + } +} \ No newline at end of file diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/io/sendtogoogle/ResultDialogFactoryTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/io/sendtogoogle/ResultDialogFactoryTest.java deleted file mode 100644 index 5f710d6d3..000000000 --- a/MyTracksTest/src/com/google/android/apps/mytracks/io/sendtogoogle/ResultDialogFactoryTest.java +++ /dev/null @@ -1,130 +0,0 @@ -/* - * 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 com.google.android.apps.mytracks.MyTracks; -import com.google.android.maps.mytracks.R; - -import android.app.AlertDialog; -import android.content.DialogInterface; -import android.test.ActivityInstrumentationTestCase2; -import android.view.View; -import android.widget.ListAdapter; -import android.widget.ListView; - -import java.util.ArrayList; -import java.util.List; - -/** - * Tests {@link ResultDialogFactory}. - * - * @author Matthew Simmons - */ -public class ResultDialogFactoryTest extends ActivityInstrumentationTestCase2 { - public ResultDialogFactoryTest() { - super(MyTracks.class); - } - - private List makeResults(SendResult... results) { - List resultsList = new ArrayList(); - for (SendResult result : results) { - resultsList.add(result); - } - return resultsList; - } - - private DialogInterface.OnClickListener clickListener = new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) {} - }; - - private MyTracks myTracks; - - protected void setUp() throws Exception { - super.setUp(); - myTracks = getActivity(); - }; - - public void testSuccess_noShare() { - List results = makeResults(new SendResult(SendType.MAPS, true), - new SendResult(SendType.DOCS, true)); - - AlertDialog dialog = ResultDialogFactory.makeDialog( - myTracks, results, clickListener, null, null); - dialog.show(); - - ListView listView = (ListView) dialog.findViewById(R.id.send_to_google_result_list); - ListAdapter listAdapter = listView.getAdapter(); - assertEquals(2, listAdapter.getCount()); - - assertEquals(SendType.MAPS, ((SendResult) listAdapter.getItem(0)).getType()); - assertEquals(SendType.DOCS, ((SendResult) listAdapter.getItem(1)).getType()); - - // Checking the return code of Dialog#getButton doesn't appear to be - // sufficient, as a Button will be returned even if it doesn't appear in - // the rendered dialog. - assertEquals(View.VISIBLE, dialog.getButton(AlertDialog.BUTTON_POSITIVE).getVisibility()); - assertEquals(View.GONE, dialog.getButton(AlertDialog.BUTTON_NEUTRAL).getVisibility()); - - assertEquals(View.VISIBLE, - dialog.findViewById(R.id.send_to_google_result_comment).getVisibility()); - assertEquals(View.GONE, - dialog.findViewById(R.id.send_to_google_result_error).getVisibility()); - - // TODO: Test correct listener invocation. Neither TouchUtils#clickView - // nor Button#performClick appear to cause listener invocation. This - // might be Android bug - // http://code.google.com/p/android/issues/detail?id=6564 - } - - public void testSuccess_share() { - List results = makeResults( - new SendResult(SendType.MAPS, true), new SendResult(SendType.DOCS, true)); - - AlertDialog dialog = ResultDialogFactory.makeDialog( - myTracks, results, clickListener, clickListener, null); - dialog.show(); - - assertEquals(View.VISIBLE, dialog.getButton(AlertDialog.BUTTON_POSITIVE).getVisibility()); - assertEquals(View.VISIBLE, dialog.getButton(AlertDialog.BUTTON_NEGATIVE).getVisibility()); - } - - public void testFailure_noShare() { - List results = makeResults(new SendResult(SendType.MAPS, true), - new SendResult(SendType.DOCS, false)); - - AlertDialog dialog = ResultDialogFactory.makeDialog( - myTracks, results, clickListener, null, null); - dialog.show(); - - assertEquals(View.GONE, - dialog.findViewById(R.id.send_to_google_result_comment).getVisibility()); - assertEquals(View.VISIBLE, - dialog.findViewById(R.id.send_to_google_result_error).getVisibility()); - } - - public void testFailure_share() { - List results = makeResults(new SendResult(SendType.MAPS, true), - new SendResult(SendType.DOCS, false)); - - AlertDialog dialog = ResultDialogFactory.makeDialog( - myTracks, results, clickListener, clickListener, null); - dialog.show(); - - assertEquals(View.VISIBLE, dialog.getButton(AlertDialog.BUTTON_POSITIVE).getVisibility()); - assertEquals(View.VISIBLE, dialog.getButton(AlertDialog.BUTTON_NEGATIVE).getVisibility()); - } -} diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/io/sendtogoogle/ResultListAdapterTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/io/sendtogoogle/ResultListAdapterTest.java deleted file mode 100644 index 5ceac6481..000000000 --- a/MyTracksTest/src/com/google/android/apps/mytracks/io/sendtogoogle/ResultListAdapterTest.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * 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 com.google.android.maps.mytracks.R; - -import android.content.Context; -import android.test.AndroidTestCase; -import android.view.LayoutInflater; -import android.view.View; - -import java.util.ArrayList; -import java.util.List; - -/** - * Tests {@link ResultListAdapter}. - * - * @author Matthew Simmons - */ -public class ResultListAdapterTest extends AndroidTestCase { - private static class TestResultListAdapter extends ResultListAdapter { - public View contentView; - public int drawableId; - public int nameId; - public int urlId; - - public TestResultListAdapter(Context context, List results) { - super(context, 0, results); - } - - @Override - protected void setImage(View content, int drawableId) { - saveContentView(content); - this.drawableId = drawableId; - } - - @Override - protected void setName(View content, int nameId) { - saveContentView(content); - this.nameId = nameId; - } - - @Override - protected void setUrl(View content, int urlId) { - saveContentView(content); - this.urlId = urlId; - } - - @SuppressWarnings("hiding") - private void saveContentView(View contentView) { - if (this.contentView == null) { - this.contentView = contentView; - } else { - AndroidTestCase.assertEquals(this.contentView, contentView); - } - } - } - - private final SendType sendType = SendType.MAPS; - - private View inflateContentView(int viewId) { - LayoutInflater layoutInflater - = (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE); - return layoutInflater.inflate(viewId, null); - } - - private List makeResult(SendType type, boolean success) { - List results = new ArrayList(); - results.add(new SendResult(type, success)); - return results; - } - - public void testSuccess_createsNewView() { - TestResultListAdapter adapter = new TestResultListAdapter(getContext(), - makeResult(sendType, true)); - adapter.getView(0, null, null); - - assertEquals(R.drawable.success, adapter.drawableId); - assertEquals(sendType.getServiceName(), adapter.nameId); - assertEquals(sendType.getServiceUrl(), adapter.urlId); - } - - public void testSuccess_reusesView() { - View contentView = inflateContentView(R.layout.send_to_google_result_list_item); - TestResultListAdapter adapter = new TestResultListAdapter(getContext(), - makeResult(sendType, true)); - adapter.getView(0, contentView, null); - - assertEquals(contentView, adapter.contentView); - assertEquals(R.drawable.success, adapter.drawableId); - assertEquals(sendType.getServiceName(), adapter.nameId); - assertEquals(sendType.getServiceUrl(), adapter.urlId); - } - - public void testFailure() { - TestResultListAdapter adapter = new TestResultListAdapter(getContext(), - makeResult(sendType, false)); - adapter.getView(0, null, null); - - assertEquals(R.drawable.failure, adapter.drawableId); - assertEquals(sendType.getServiceName(), adapter.nameId); - assertEquals(sendType.getServiceUrl(), adapter.urlId); - } -} diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/services/TestRecordingService.java b/MyTracksTest/src/com/google/android/apps/mytracks/services/TestRecordingService.java new file mode 100644 index 000000000..2eaa20aad --- /dev/null +++ b/MyTracksTest/src/com/google/android/apps/mytracks/services/TestRecordingService.java @@ -0,0 +1,64 @@ +// Copyright 2012 Google Inc. All Rights Reserved. + +package com.google.android.apps.mytracks.services; + +import android.app.Notification; +import android.app.Service; +import android.test.ServiceTestCase; +import android.util.Log; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +/** + * A {@link TrackRecordingService} that can be used with {@link ServiceTestCase}. + * {@link ServiceTestCase} throws a null pointer exception when the service + * calls {@link Service#startForeground(int, android.app.Notification)} and + * {@link Service#stopForeground(boolean)}. + *

+ * See http://code.google.com/p/android/issues/detail?id=12122 + *

+ * Wrap these two methods in wrappers and override them. + * + * @author Jimmy Shih + */ +public class TestRecordingService extends TrackRecordingService { + + private static final String TAG = TestRecordingService.class.getSimpleName(); + + @Override + protected void startForegroundService(Notification notification) { + try { + Method setForegroundMethod = Service.class.getMethod("setForeground", boolean.class); + setForegroundMethod.invoke(this, true); + } catch (SecurityException e) { + Log.e(TAG, "Unable to start a service in foreground", e); + } catch (NoSuchMethodException e) { + Log.e(TAG, "Unable to start a service in foreground", e); + } catch (IllegalArgumentException e) { + Log.e(TAG, "Unable to start a service in foreground", e); + } catch (IllegalAccessException e) { + Log.e(TAG, "Unable to start a service in foreground", e); + } catch (InvocationTargetException e) { + Log.e(TAG, "Unable to start a service in foreground", e); + } + } + + @Override + protected void stopForegroundService() { + try { + Method setForegroundMethod = Service.class.getMethod("setForeground", boolean.class); + setForegroundMethod.invoke(this, false); + } catch (SecurityException e) { + Log.e(TAG, "Unable to start a service in foreground", e); + } catch (NoSuchMethodException e) { + Log.e(TAG, "Unable to start a service in foreground", e); + } catch (IllegalArgumentException e) { + Log.e(TAG, "Unable to start a service in foreground", e); + } catch (IllegalAccessException e) { + Log.e(TAG, "Unable to start a service in foreground", e); + } catch (InvocationTargetException e) { + Log.e(TAG, "Unable to start a service in foreground", e); + } + } +} diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/services/TrackRecordingServiceTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/services/TrackRecordingServiceTest.java index 25fe512ef..df0b079a3 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/services/TrackRecordingServiceTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/services/TrackRecordingServiceTest.java @@ -24,7 +24,6 @@ import com.google.android.apps.mytracks.content.Track; import com.google.android.apps.mytracks.content.Waypoint; import com.google.android.apps.mytracks.content.WaypointCreationRequest; import com.google.android.apps.mytracks.stats.TripStatistics; -import com.google.android.apps.mytracks.util.ApiFeatures; import com.google.android.maps.mytracks.R; import android.content.BroadcastReceiver; @@ -57,8 +56,7 @@ import java.util.List; * is called, which prevents from testing multiple scenarios in a single * test (see runFunctionTest for more details). */ -public class TrackRecordingServiceTest - extends ServiceTestCase { +public class TrackRecordingServiceTest extends ServiceTestCase { private Context context; private MyTracksProviderUtils providerUtils; @@ -74,7 +72,7 @@ public class TrackRecordingServiceTest private Intent serviceIntent; public TrackRecordingServiceTest() { - super(TrackRecordingService.class); + super(TestRecordingService.class); } /** @@ -96,19 +94,6 @@ public class TrackRecordingServiceTest } } - /** - * A mock class that forces API level < 5 to make sure we can workaround a bug - * in ServiceTestCase (throwing a NPE). - * See http://code.google.com/p/android/issues/detail?id=12122 for more - * details. - */ - private static class MockApiFeatures extends ApiFeatures { - @Override - protected int getApiLevel() { - return 4; - } - } - @Override protected IBinder bindService(Intent intent) { if (getService() != null) { @@ -138,8 +123,6 @@ public class TrackRecordingServiceTest protected void setUp() throws Exception { super.setUp(); - ApiFeatures.injectInstance(new MockApiFeatures()); - MockContentResolver mockContentResolver = new MockContentResolver(); RenamingDelegatingContext targetContext = new RenamingDelegatingContext( getContext(), getContext(), "test."); diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/services/tasks/StatusAnnouncerFactoryTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/services/tasks/StatusAnnouncerFactoryTest.java index ea9a8d441..91d9b3eeb 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/services/tasks/StatusAnnouncerFactoryTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/services/tasks/StatusAnnouncerFactoryTest.java @@ -15,13 +15,6 @@ */ package com.google.android.apps.mytracks.services.tasks; -import com.google.android.apps.mytracks.services.tasks.PeriodicTask; -import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerFactory; -import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerTask; -import com.google.android.apps.mytracks.util.ApiFeatures; - -import android.media.AudioManager; -import android.speech.tts.TextToSpeech; import android.test.AndroidTestCase; /** @@ -31,62 +24,9 @@ import android.test.AndroidTestCase; * @author Rodrigo Damazio */ public class StatusAnnouncerFactoryTest extends AndroidTestCase { - /** - * Mock version of the {@link ApiFeatures} class. - */ - private class MockApiFeatures extends ApiFeatures { - private boolean hasTts; - - public void setHasTextToSpeech(boolean hasTts) { - this.hasTts = hasTts; - } - - @Override - public boolean hasTextToSpeech() { - return hasTts; - } - } - - private MockApiFeatures apiFeatures; - - @Override - protected void setUp() throws Exception { - super.setUp(); - - apiFeatures = new MockApiFeatures(); - } - public void testCreate() { - apiFeatures.setHasTextToSpeech(true); - PeriodicTaskFactory factory = - new StatusAnnouncerFactory(apiFeatures); + PeriodicTaskFactory factory = new StatusAnnouncerFactory(); PeriodicTask task = factory.create(getContext()); assertTrue(task instanceof StatusAnnouncerTask); } - - public void testCreate_notAvailable() { - apiFeatures.setHasTextToSpeech(false); - PeriodicTaskFactory factory = - new StatusAnnouncerFactory(apiFeatures); - PeriodicTask task = factory.create(getContext()); - assertNull(task); - } - - public void testGetVolumeStream() { - apiFeatures.setHasTextToSpeech(true); - StatusAnnouncerFactory factory = - new StatusAnnouncerFactory(apiFeatures); - assertEquals( - TextToSpeech.Engine.DEFAULT_STREAM, - factory.getVolumeStream()); - } - - public void testGetVolumeStream_notAvailable() { - apiFeatures.setHasTextToSpeech(false); - StatusAnnouncerFactory factory = - new StatusAnnouncerFactory(apiFeatures); - assertEquals( - AudioManager.USE_DEFAULT_STREAM_TYPE, - factory.getVolumeStream()); - } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/ProgressIndicator.java b/MyTracksTest/src/com/google/android/apps/mytracks/util/ApiAdapterFactoryTest.java similarity index 59% rename from MyTracks/src/com/google/android/apps/mytracks/ProgressIndicator.java rename to MyTracksTest/src/com/google/android/apps/mytracks/util/ApiAdapterFactoryTest.java index da75e6557..4b73a6a6f 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/ProgressIndicator.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/util/ApiAdapterFactoryTest.java @@ -1,31 +1,29 @@ /* - * Copyright 2008 Google Inc. - * + * 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; +package com.google.android.apps.mytracks.util; + +import junit.framework.TestCase; /** - * Interface that allows to set a progress dialog message and progress value - * in percent. + * Tests {@link ApiAdapterFactory}. * - * @author Leif Hendrik Wilden + * @author Rodrigo Damazio */ -public interface ProgressIndicator { - - public void setProgressMessage(String message); - public void clearProgressMessage(); - - public void setProgressValue(int percent); - +public class ApiAdapterFactoryTest extends TestCase { + public void testGetApiLevelAdapter() { + assertNotNull(ApiAdapterFactory.getApiAdapter()); + } } diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/util/ApiFeaturesTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/util/ApiFeaturesTest.java deleted file mode 100644 index 5b08a975f..000000000 --- a/MyTracksTest/src/com/google/android/apps/mytracks/util/ApiFeaturesTest.java +++ /dev/null @@ -1,62 +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.util; - -import junit.framework.TestCase; - -/** - * Tests the API feature detection code in {@link ApiFeatures}. - * This test requires Froyo+ to run. - * - * @author Rodrigo Damazio - */ -public class ApiFeaturesTest extends TestCase { - - private TestableApiFeatures features; - - private class TestableApiFeatures extends ApiFeatures { - private int apiLevel; - - public void setApiLevel(int apiLevel) { - this.apiLevel = apiLevel; - } - - @Override - protected int getApiLevel() { - return apiLevel; - } - } - - @Override - protected void setUp() throws Exception { - super.setUp(); - - features = new TestableApiFeatures(); - } - - public void testHasTextToSpeech() { - features.setApiLevel(3); - assertFalse(features.hasTextToSpeech()); - for (int i = 4; i <= 8; i++) { - features.setApiLevel(i); - assertTrue(features.hasTextToSpeech()); - } - } - - public void testGetApiAdapter() { - assertNotNull(features.getApiAdapter()); - } -} diff --git a/SignalStrengthSampler/AndroidManifest.xml b/SignalStrengthSampler/AndroidManifest.xml index 8bdd8768d..f7ad5f3c9 100644 --- a/SignalStrengthSampler/AndroidManifest.xml +++ b/SignalStrengthSampler/AndroidManifest.xml @@ -52,7 +52,7 @@ limitations under the License. - + - + \ No newline at end of file