diff --git a/MyTracks/AndroidManifest.xml b/MyTracks/AndroidManifest.xml index a9982299b..d64ece980 100755 --- a/MyTracks/AndroidManifest.xml +++ b/MyTracks/AndroidManifest.xml @@ -285,6 +285,7 @@ limitations under the License. + diff --git a/MyTracks/res/values/strings.xml b/MyTracks/res/values/strings.xml index f1f84a8b0..a6cb81f23 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 + + - 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 - - No account found - - Authenticating to %1$s... - - - Creating a new %1$s item... - - - Preparing track... - Sending to %1$s. Please wait... - - Reading track... - - - Sending a track to %1$s... - 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/Constants.java b/MyTracks/src/com/google/android/apps/mytracks/Constants.java index b15d4af09..62588021a 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/Constants.java +++ b/MyTracks/src/com/google/android/apps/mytracks/Constants.java @@ -40,23 +40,22 @@ public abstract class Constants { * 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 CHOOSE_MAP = 1; - public static final int SHOW_TRACK = 2; - public static final int AUTHENTICATE_TO_MAPS = 3; - public static final int SHARE_GPX_FILE = 4; - public static final int SHARE_KML_FILE = 5; - public static final int SHARE_CSV_FILE = 6; - public static final int SHARE_TCX_FILE = 7; - public static final int SAVE_GPX_FILE = 8; - public static final int SAVE_KML_FILE = 9; - public static final int SAVE_CSV_FILE = 10; - public static final int SAVE_TCX_FILE = 11; - public static final int SHOW_WAYPOINT = 12; - public static final int WELCOME = 13; - public static final int CHOOSE_ACCOUNT = 14; + 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; + public static final int CHOOSE_ACCOUNT = 11; + public static final int CHOOSE_MAP = 12; + public static final int SEND_MAPS = 13; + public static final int SEND_FUSION_TABLES = 14; public static final int SEND_DOCS = 15; - public static final int SEND_FUSION_TABLES = 16; /* * Menu ids: 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/ProgressIndicator.java b/MyTracks/src/com/google/android/apps/mytracks/ProgressIndicator.java deleted file mode 100644 index da75e6557..000000000 --- a/MyTracks/src/com/google/android/apps/mytracks/ProgressIndicator.java +++ /dev/null @@ -1,31 +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; - -/** - * Interface that allows to set a progress dialog message and progress value - * in percent. - * - * @author Leif Hendrik Wilden - */ -public interface ProgressIndicator { - - public void setProgressMessage(String message); - public void clearProgressMessage(); - - public void setProgressValue(int percent); - -} 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/SendToMaps.java b/MyTracks/src/com/google/android/apps/mytracks/io/SendToMaps.java deleted file mode 100644 index 8d51071f0..000000000 --- a/MyTracks/src/com/google/android/apps/mytracks/io/SendToMaps.java +++ /dev/null @@ -1,457 +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(context.getString( - R.string.send_google_track_part_label, splitTrack.getName(), 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/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/android/apps/mytracks/io/maps/MapsFacade.java b/MyTracks/src/com/google/android/apps/mytracks/io/maps/MapsFacade.java deleted file mode 100644 index 45b8bf515..000000000 --- a/MyTracks/src/com/google/android/apps/mytracks/io/maps/MapsFacade.java +++ /dev/null @@ -1,339 +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.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 { - - 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); - } - - /** - * 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 = isStart ? context.getString(R.string.marker_label_start, trackName) : - context.getString(R.string.marker_label_end, trackName); - 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/MapsGDataConverter.java b/MyTracks/src/com/google/android/apps/mytracks/io/maps/MapsGDataConverter.java index 6004fa469..573e4bdf6 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/maps/MapsGDataConverter.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/maps/MapsGDataConverter.java @@ -52,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/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..93a1144a6 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/io/maps/SendMapsActivity.java @@ -0,0 +1,134 @@ +/* + * 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.maps.mytracks.R; + +import android.accounts.Account; +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 { + + // parameters in the input intent + public static final String ACCOUNT = "account"; + public static final String TRACK_ID = "trackId"; + public static final String MAP_ID = "mapId"; + + // parameters in the output intent + public static final String SUCCESS = "success"; + + private static final int PROGRESS_DIALOG = 1; + + private SendMapsAsyncTask asyncTask; + private ProgressDialog progressDialog; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Object retained = getLastNonConfigurationInstance(); + if (retained instanceof SendMapsAsyncTask) { + asyncTask = (SendMapsAsyncTask) retained; + asyncTask.setActivity(this); + } else { + Intent intent = getIntent(); + Account account = intent.getParcelableExtra(ACCOUNT); + if (account == null) { + setResult(RESULT_OK, new Intent().putExtra(SUCCESS, false)); + finish(); + return; + } + long trackId = intent.getLongExtra(TRACK_ID, -1L); + if (trackId == -1L) { + setResult(RESULT_OK, new Intent().putExtra(SUCCESS, false)); + finish(); + return; + } + String mapId = intent.getStringExtra(MAP_ID); + + asyncTask = new SendMapsAsyncTask(this, account, trackId, mapId); + 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); + setResult(RESULT_CANCELED); + finish(); + } + }); + return progressDialog; + default: + return null; + } + } + + /** + * Invokes when the associated AsyncTask completes. + * + * @param success true if success + */ + public void onAsyncTaskCompleted(boolean success) { + setResult(RESULT_OK, new Intent().putExtra(SUCCESS, success)); + finish(); + } + + /** + * 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); + } + } +} 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..4c7e2a9ef --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/io/maps/SendMapsAsyncTask.java @@ -0,0 +1,554 @@ +/* + * 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.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 Account account; + private final long trackId; + 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, Account account, long trackId, String chooseMapId) { + this.activity = activity; + this.account = account; + this.trackId = trackId; + 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..1cbae922e --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/io/maps/SendMapsUtils.java @@ -0,0 +1,223 @@ +/* + * 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.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/SendActivity.java b/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/SendActivity.java index ba13d0b9e..c603980e5 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/SendActivity.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/SendActivity.java @@ -19,36 +19,27 @@ 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.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.SendToMaps; import com.google.android.apps.mytracks.io.docs.SendDocsActivity; import com.google.android.apps.mytracks.io.maps.ChooseMapActivity; -import com.google.android.apps.mytracks.io.maps.MapsConstants; -import com.google.android.apps.mytracks.io.maps.MapsFacade; +import com.google.android.apps.mytracks.io.maps.SendMapsActivity; +import com.google.android.apps.mytracks.io.maps.SendMapsUtils; 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.Intent; import android.os.Bundle; import android.util.Log; -import java.util.HashMap; - /** * Helper activity for managing the sending of tracks to Google services. * * @author Rodrigo Damazio */ -public class SendActivity extends Activity implements ProgressIndicator { +public class SendActivity extends Activity { // Items in the intent that starts the activity. public static final String TRACK_ID = "trackId"; @@ -59,20 +50,18 @@ public class SendActivity extends Activity implements ProgressIndicator { 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_ACCOUNT = "account"; private static final String STATE_MAP_ID = "mapId"; + private static final String STATE_DOCS_SUCCESS = "docsSuccess"; + private static final String STATE_FUSION_TABLES_SUCCESS = "fusionTablesSuccess"; + private static final String STATE_MAPS_SUCCESS = "mapsSuccess"; /** States for the state machine that defines the upload process. */ private enum SendState { CHOOSE_ACCOUNT, + CHOOSE_MAP, START, - AUTHENTICATE_MAPS, - PICK_MAP, SEND_TO_MAPS, SEND_TO_MAPS_DONE, SEND_TO_FUSION_TABLES, @@ -85,9 +74,6 @@ public class SendActivity extends Activity implements ProgressIndicator { NOT_READY } - private static final int PROGRESS_DIALOG = 1; - private ProgressDialog progressDialog; - // Set in Activity.onCreate private MyTracksProviderUtils providerUtils; private GoogleAnalyticsTracker tracker; @@ -100,19 +86,19 @@ public class SendActivity extends Activity implements ProgressIndicator { private boolean sendToFusionTables; private boolean sendToDocs; - // Authentication information. - private Account account; - private HashMap authMap = new HashMap(); - // Current sending state. private SendState currentState; + // Authentication information. + private Account account; + + // Map id from choosing a Google Map + private String mapId; + // Send result information. Used by the results dialog. private boolean sendToMapsSuccess; private boolean sendToFusionTablesSuccess; private boolean sendToDocsSuccess; - private String sendToMapsMapId; - private String sendToFusionTablesTableId; @Override protected void onCreate(Bundle savedInstanceState) { @@ -141,7 +127,8 @@ public class SendActivity extends Activity implements ProgressIndicator { // If we had the instance restored after it was done, reset it. if (currentState == SendState.DONE) { - resetState(); + finish(); + return; } // Execute the state machine, at the start or restored state. @@ -165,40 +152,24 @@ public class SendActivity extends Activity implements ProgressIndicator { return true; } - @Override - protected Dialog onCreateDialog(int id) { - switch (id) { - case PROGRESS_DIALOG: - return createProgressDialog(); - default: - return null; - } - } - private void restoreInstanceState(Bundle savedInstanceState) { - account = savedInstanceState.getParcelable(STATE_ACCOUNT); - currentState = SendState.values()[savedInstanceState.getInt(STATE_STATE)]; - + account = savedInstanceState.getParcelable(STATE_ACCOUNT); + mapId = savedInstanceState.getString(STATE_MAP_ID); sendToMapsSuccess = savedInstanceState.getBoolean(STATE_MAPS_SUCCESS); - sendToFusionTablesSuccess = savedInstanceState.getBoolean(STATE_FUSION_SUCCESS); + sendToFusionTablesSuccess = savedInstanceState.getBoolean(STATE_FUSION_TABLES_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.putParcelable(STATE_ACCOUNT, account); + outState.putString(STATE_MAP_ID, mapId); outState.putBoolean(STATE_MAPS_SUCCESS, sendToMapsSuccess); - outState.putBoolean(STATE_FUSION_SUCCESS, sendToFusionTablesSuccess); + outState.putBoolean(STATE_FUSION_TABLES_SUCCESS, sendToFusionTablesSuccess); outState.putBoolean(STATE_DOCS_SUCCESS, sendToDocsSuccess); - outState.putString(STATE_MAP_ID, sendToMapsMapId); - outState.putString(STATE_TABLE_ID, sendToFusionTablesTableId); } @Override @@ -226,12 +197,10 @@ public class SendActivity extends Activity implements ProgressIndicator { switch (state) { case CHOOSE_ACCOUNT: return chooseAccount(); + case CHOOSE_MAP: + return chooseMap(); 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: @@ -267,7 +236,7 @@ public class SendActivity extends Activity implements ProgressIndicator { */ private SendState startSend() { if (sendToMaps) { - return SendState.AUTHENTICATE_MAPS; + return SendState.SEND_TO_MAPS; } else if (sendToFusionTables) { return SendState.SEND_TO_FUSION_TABLES; } else if (sendToDocs) { @@ -278,78 +247,24 @@ public class SendActivity extends Activity implements ProgressIndicator { } } - 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"); - showDialog(PROGRESS_DIALOG); - 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() { + private SendState chooseMap() { if (!sendToMapsNewMap) { - Intent intent = new Intent(this, ChooseMapActivity.class); - intent.putExtra(ChooseMapActivity.ACCOUNT, account); + Intent intent = new Intent(this, ChooseMapActivity.class) + .putExtra(ChooseMapActivity.ACCOUNT, account); startActivityForResult(intent, Constants.CHOOSE_MAP); return SendState.NOT_READY; } else { - return SendState.SEND_TO_MAPS; + return SendState.START; } } 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(); - + Intent intent = new Intent(this, SendMapsActivity.class) + .putExtra(SendMapsActivity.ACCOUNT, account) + .putExtra(SendMapsActivity.TRACK_ID, sendTrackId) + .putExtra(SendMapsActivity.MAP_ID, mapId); + startActivityForResult(intent, Constants.SEND_MAPS); return SendState.NOT_READY; } @@ -395,12 +310,10 @@ public class SendActivity extends Activity implements ProgressIndicator { private SendState onSendToGoogleDone() { tracker.dispatch(); - String mapsUrl = sendToMaps && sendToMapsSuccess - ? MapsFacade.buildMapUrl(sendToMapsMapId) - : null; + Track track = providerUtils.getTrack(sendTrackId); + String mapsUrl = sendToMaps && sendToMapsSuccess ? SendMapsUtils.getMapUrl(track) : null; String fusionTablesUrl = sendToFusionTables && sendToFusionTablesSuccess - ? SendFusionTablesUtils.getMapUrl(providerUtils.getTrack(sendTrackId)) - : null; + ? SendFusionTablesUtils.getMapUrl(track) : null; Intent intent = new Intent(this, UploadResultActivity.class) .putExtra(UploadResultActivity.HAS_MAPS_RESULT, sendToMaps) .putExtra(UploadResultActivity.HAS_FUSION_TABLES_RESULT, sendToFusionTables) @@ -420,44 +333,11 @@ public class SendActivity extends Activity implements ProgressIndicator { return SendState.DONE; } - /** - * 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.CHOOSE_MAP: { - if (resultCode == RESULT_CANCELED) { - nextState = SendState.FINISH; - break; - } - sendToMapsMapId = results.getStringExtra(ChooseMapActivity.MAP_ID); - if (sendToMapsMapId == null) { - nextState = SendState.FINISH; - break; - } - nextState = SendState.SEND_TO_MAPS; - break; - } case Constants.CHOOSE_ACCOUNT: { if (resultCode == RESULT_CANCELED) { nextState = SendState.FINISH; @@ -468,16 +348,29 @@ public class SendActivity extends Activity implements ProgressIndicator { nextState = SendState.FINISH; break; } - nextState = SendState.START; + nextState = SendState.CHOOSE_MAP; break; } - case Constants.SEND_DOCS: { + case Constants.CHOOSE_MAP: { if (resultCode == RESULT_CANCELED) { nextState = SendState.FINISH; break; } - sendToDocsSuccess = results.getBooleanExtra(SendDocsActivity.SUCCESS, false); - nextState = SendState.SEND_TO_DOCS_DONE; + mapId = results.getStringExtra(ChooseMapActivity.MAP_ID); + if (mapId == null) { + nextState = SendState.FINISH; + break; + } + nextState = SendState.START; + break; + } + case Constants.SEND_MAPS: { + if (resultCode == RESULT_CANCELED) { + nextState = SendState.FINISH; + break; + } + sendToMapsSuccess = results.getBooleanExtra(SendMapsActivity.SUCCESS, false); + nextState = SendState.SEND_TO_MAPS_DONE; break; } case Constants.SEND_FUSION_TABLES: { @@ -487,10 +380,18 @@ public class SendActivity extends Activity implements ProgressIndicator { } sendToFusionTablesSuccess = results.getBooleanExtra( SendFusionTablesActivity.SUCCESS, false); - sendToFusionTablesTableId = results.getStringExtra(SendFusionTablesActivity.TABLE_ID); nextState = SendState.SEND_TO_FUSION_TABLES_DONE; break; } + case Constants.SEND_DOCS: { + if (resultCode == RESULT_CANCELED) { + nextState = SendState.FINISH; + break; + } + sendToDocsSuccess = results.getBooleanExtra(SendDocsActivity.SUCCESS, false); + nextState = SendState.SEND_TO_DOCS_DONE; + break; + } default: { Log.e(TAG, "Unrequested result: " + requestCode); return; @@ -502,85 +403,15 @@ public class SendActivity extends Activity implements ProgressIndicator { } } - private void onLoginSuccess(int requestCode) { - SendState nextState; - switch (requestCode) { - case Constants.AUTHENTICATE_TO_MAPS: - // Authenticated with Google Maps - nextState = SendState.PICK_MAP; - 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; + account = null; + mapId = null; 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/SendFusionTablesActivity.java b/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/SendFusionTablesActivity.java index 91a856c25..bc1361c84 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/SendFusionTablesActivity.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/SendFusionTablesActivity.java @@ -48,7 +48,6 @@ public class SendFusionTablesActivity extends Activity { // parameters in the output intent public static final String SUCCESS = "success"; - public static final String TABLE_ID = "tableId"; private static final int PROGRESS_DIALOG = 1; @@ -67,15 +66,13 @@ public class SendFusionTablesActivity extends Activity { Intent intent = getIntent(); Account account = intent.getParcelableExtra(ACCOUNT); if (account == null) { - setResult( - RESULT_OK, new Intent().putExtra(SUCCESS, false).putExtra(TABLE_ID, (String) null)); + setResult(RESULT_OK, new Intent().putExtra(SUCCESS, false)); finish(); return; } long trackId = intent.getLongExtra(TRACK_ID, -1L); if (trackId == -1L) { - setResult( - RESULT_OK, new Intent().putExtra(SUCCESS, false).putExtra(TABLE_ID, (String) null)); + setResult(RESULT_OK, new Intent().putExtra(SUCCESS, false)); finish(); return; } @@ -121,10 +118,9 @@ public class SendFusionTablesActivity extends Activity { * Invokes when the associated AsyncTask completes. * * @param success true if success - * @param tableId tableId if available */ - public void onAsyncTaskCompleted(boolean success, String tableId) { - setResult(RESULT_OK, new Intent().putExtra(SUCCESS, success).putExtra(TABLE_ID, tableId)); + public void onAsyncTaskCompleted(boolean success) { + setResult(RESULT_OK, new Intent().putExtra(SUCCESS, success)); finish(); } diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/SendFusionTablesAsyncTask.java b/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/SendFusionTablesAsyncTask.java index 126083f01..9d62bbb8f 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/SendFusionTablesAsyncTask.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/SendFusionTablesAsyncTask.java @@ -7,7 +7,6 @@ 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.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; @@ -125,7 +124,7 @@ public class SendFusionTablesAsyncTask extends AsyncTask public void setActivity(SendFusionTablesActivity activity) { this.activity = activity; if (completed && activity != null) { - activity.onAsyncTaskCompleted(success, tableId); + activity.onAsyncTaskCompleted(success); } } @@ -160,7 +159,7 @@ public class SendFusionTablesAsyncTask extends AsyncTask } } if (activity != null) { - activity.onAsyncTaskCompleted(success, tableId); + activity.onAsyncTaskCompleted(success); } } @@ -325,7 +324,7 @@ public class SendFusionTablesAsyncTask extends AsyncTask } // Upload periodically - if (readCount % MAX_POINTS_PER_UPLOAD == MAX_POINTS_PER_UPLOAD) { + if (readCount % MAX_POINTS_PER_UPLOAD == 0) { if (!prepareAndUploadPoints(track, locations, false)) { Log.d(TAG, "Unable to upload points"); return false; @@ -371,7 +370,7 @@ public class SendFusionTablesAsyncTask extends AsyncTask */ private boolean prepareAndUploadPoints(Track track, List locations, boolean lastBatch) { // Prepare locations - ArrayList splitTracks = prepareLocations(track, locations); + ArrayList splitTracks = SendToGoogleUtils.prepareLocations(track, locations); // Upload segments boolean onlyOneSegment = lastBatch && currentSegment == 1 && splitTracks.size() == 1; @@ -389,99 +388,6 @@ public class SendFusionTablesAsyncTask extends AsyncTask return true; } - /** - * Prepares a list of locations to send to 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. - */ - private 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 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 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. Fusion Tables doesn't like too many - // points. - LocationUtils.decimate(segment, 2.0); - - splitTracks.add(segment); - } - /** * Uploads all the waypoints. * 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/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/maps/SendMapsUtilsTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/io/maps/SendMapsUtilsTest.java new file mode 100644 index 000000000..753a0950c --- /dev/null +++ b/MyTracksTest/src/com/google/android/apps/mytracks/io/maps/SendMapsUtilsTest.java @@ -0,0 +1,178 @@ +/* + * 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.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