From d9904eb5f164e2ff584eda9d8540352bfd3aae4e Mon Sep 17 00:00:00 2001 From: Rodrigo Damazio Date: Tue, 22 Feb 2011 19:55:27 -0300 Subject: [PATCH] Bring My Maps back --- MyTracks/.classpath | 1 + MyTracks/AndroidManifest.xml | 5 + MyTracks/libs/README.txt | 17 + MyTracks/libs/libmymaps.jar | Bin 0 -> 32376 bytes MyTracks/res/layout/list.xml | 103 ++++ .../res/layout/mytracks_send_to_google.xml | 35 ++ MyTracks/res/values-ca/strings.xml | 1 - MyTracks/res/values-de/strings.xml | 3 +- MyTracks/res/values-es-rES/strings.xml | 1 - MyTracks/res/values-es/strings.xml | 1 - MyTracks/res/values-fr/strings.xml | 1 - MyTracks/res/values-it/strings.xml | 2 +- MyTracks/res/values-ja/strings.xml | 1 - MyTracks/res/values-pl/strings.xml | 1 - MyTracks/res/values-pt-rBR/strings.xml | 54 +- MyTracks/res/values-sv/strings.xml | 1 - MyTracks/res/values-zh-rCN/strings.xml | 1 - MyTracks/res/values/keys.xml | 1 + MyTracks/res/values/strings.xml | 36 +- .../android/apps/mytracks/DialogManager.java | 11 +- .../android/apps/mytracks/MyMapsList.java | 210 ++++++++ .../apps/mytracks/MyMapsListAdapter.java | 135 +++++ .../android/apps/mytracks/MyTracks.java | 488 ++++++++++++------ .../apps/mytracks/MyTracksConstants.java | 40 +- .../android/apps/mytracks/MyTracksList.java | 2 +- .../android/apps/mytracks/MyTracksMap.java | 2 +- .../apps/mytracks/SendToGoogleDialog.java | 40 +- .../apps/mytracks/io/MyMapsFactory.java | 68 +++ .../android/apps/mytracks/io/SendToDocs.java | 7 +- .../apps/mytracks/io/SendToMyMaps.java | 458 ++++++++++++++++ .../services/TrackRecordingService.java | 2 +- .../XmlDocsGDataParserFactory.java} | 51 +- .../wireless/gdata/maps/MapFeatureEntry.java | 50 -- .../wireless/gdata/maps/MapsClient.java | 113 ---- .../gdata/maps/xml/XmlMapsGDataParser.java | 178 ------- .../maps/xml/XmlMapsGDataSerializer.java | 277 ---------- 36 files changed, 1504 insertions(+), 893 deletions(-) create mode 100644 MyTracks/libs/README.txt create mode 100644 MyTracks/libs/libmymaps.jar create mode 100644 MyTracks/res/layout/list.xml create mode 100644 MyTracks/src/com/google/android/apps/mytracks/MyMapsList.java create mode 100644 MyTracks/src/com/google/android/apps/mytracks/MyMapsListAdapter.java create mode 100644 MyTracks/src/com/google/android/apps/mytracks/io/MyMapsFactory.java create mode 100644 MyTracks/src/com/google/android/apps/mytracks/io/SendToMyMaps.java rename MyTracks/src/com/google/wireless/gdata/{maps/xml/XmlMapsGDataParserFactory.java => docs/XmlDocsGDataParserFactory.java} (57%) delete mode 100644 MyTracks/src/com/google/wireless/gdata/maps/MapFeatureEntry.java delete mode 100644 MyTracks/src/com/google/wireless/gdata/maps/MapsClient.java delete mode 100644 MyTracks/src/com/google/wireless/gdata/maps/xml/XmlMapsGDataParser.java delete mode 100644 MyTracks/src/com/google/wireless/gdata/maps/xml/XmlMapsGDataSerializer.java diff --git a/MyTracks/.classpath b/MyTracks/.classpath index 636d59be6..adbe8087e 100644 --- a/MyTracks/.classpath +++ b/MyTracks/.classpath @@ -9,5 +9,6 @@ + diff --git a/MyTracks/AndroidManifest.xml b/MyTracks/AndroidManifest.xml index 3d9b308fd..b13c4f11c 100755 --- a/MyTracks/AndroidManifest.xml +++ b/MyTracks/AndroidManifest.xml @@ -56,6 +56,11 @@ limitations under the License. android:label="MyTracks" android:exported="false" /> + + diff --git a/MyTracks/libs/README.txt b/MyTracks/libs/README.txt new file mode 100644 index 000000000..6e03a45b1 --- /dev/null +++ b/MyTracks/libs/README.txt @@ -0,0 +1,17 @@ +The libraries contained here in binary format are licensed according to their +own licenses, which allow their usage for the My Tracks application. Any other +applications that wish to use these files should consult the appropriate +licenses or the JAR's authors. + +References are provided to the libraries: + +accounts.jar (Android accounts library): +http://code.google.com/p/libs-for-android/wiki/AccountsLibrary + +google-api-client-*.jar (GData client): +http://code.google.com/p/google-api-java-client/ + +google-common.jar, +googleloginclient-helper.jar, +libmymaps.jar (Google My Maps upload library): +Licensed exclusively for use in My Tracks and derived Android applications. diff --git a/MyTracks/libs/libmymaps.jar b/MyTracks/libs/libmymaps.jar new file mode 100644 index 0000000000000000000000000000000000000000..babb14aac1c34e157532827e6c8ae7704114ee62 GIT binary patch literal 32376 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 g?|gK#zvBPQ{wvBrLI1c)@SY!l=ZVLj + + + + + + + + + + + + + + + + + + diff --git a/MyTracks/res/layout/mytracks_send_to_google.xml b/MyTracks/res/layout/mytracks_send_to_google.xml index 276bd58ac..e0ead4c75 100644 --- a/MyTracks/res/layout/mytracks_send_to_google.xml +++ b/MyTracks/res/layout/mytracks_send_to_google.xml @@ -41,6 +41,39 @@ limitations under the License. android:stretchColumns="1" android:layout_marginTop="5dip" android:layout_marginLeft="5dip"> + + + + + + + + + + + + + + + + Totes les rutes: Aquesta ruta Comparteix amb amics... - Un enllaç a Google Fusion Table Un fitxer GPX Un fitxer KML Un fitxer CSV diff --git a/MyTracks/res/values-de/strings.xml b/MyTracks/res/values-de/strings.xml index c742a76d8..5c0809ca1 100644 --- a/MyTracks/res/values-de/strings.xml +++ b/MyTracks/res/values-de/strings.xml @@ -129,8 +129,7 @@ limitations under the License. Einen Track auswählen. Lange berühren für mehr Optionen. Alle Tracks Dieser Track - Mit Freunden teilen… - Ein Link zur Google Fusion Tables-Karte + Mit Freunden teilen… Eine GPX-Datei Eine KML-Datei Eine CSV-Datei diff --git a/MyTracks/res/values-es-rES/strings.xml b/MyTracks/res/values-es-rES/strings.xml index 1a660a31e..2cb837ae1 100644 --- a/MyTracks/res/values-es-rES/strings.xml +++ b/MyTracks/res/values-es-rES/strings.xml @@ -47,7 +47,6 @@ limitations under the License. Guardar en archivo TCX Enviar a Google Fusion Tables - Un enlace a Google Fusion Tables Un archivo CSV Un archivo TCX diff --git a/MyTracks/res/values-es/strings.xml b/MyTracks/res/values-es/strings.xml index 3fba9958b..e4cc06155 100644 --- a/MyTracks/res/values-es/strings.xml +++ b/MyTracks/res/values-es/strings.xml @@ -121,7 +121,6 @@ limitations under the License. Todos los recorridos:" Este recorrido" Compartir con amigos..." - Un enlace a Google Fusion Tables Un archivo GPX" Un archivo KML" "Limpiar mapa" diff --git a/MyTracks/res/values-fr/strings.xml b/MyTracks/res/values-fr/strings.xml index eaabdce72..86c470297 100644 --- a/MyTracks/res/values-fr/strings.xml +++ b/MyTracks/res/values-fr/strings.xml @@ -127,7 +127,6 @@ limitations under the License. Tous les parcours: Ce parcours Partager... - Lien sur Google Fusion Tables Fichier GPX Fichier KML Effacer la carte diff --git a/MyTracks/res/values-it/strings.xml b/MyTracks/res/values-it/strings.xml index 6451b836a..a3419cafd 100644 --- a/MyTracks/res/values-it/strings.xml +++ b/MyTracks/res/values-it/strings.xml @@ -129,7 +129,7 @@ limitations under the License. Tutti i percorsi: Questo percorso Condividi con amici... - + Un link alla mappa Un file GPX Un file KML Cancella la mappa diff --git a/MyTracks/res/values-ja/strings.xml b/MyTracks/res/values-ja/strings.xml index 8895c3c22..52719208a 100644 --- a/MyTracks/res/values-ja/strings.xml +++ b/MyTracks/res/values-ja/strings.xml @@ -133,7 +133,6 @@ limitations under the License. 全てのトラック: このトラック 共有... - Google Fusion Tablesへのリンク GPXファイル KMLファイル CSVファイル diff --git a/MyTracks/res/values-pl/strings.xml b/MyTracks/res/values-pl/strings.xml index 264a87990..63b9cde9c 100644 --- a/MyTracks/res/values-pl/strings.xml +++ b/MyTracks/res/values-pl/strings.xml @@ -131,7 +131,6 @@ limitations under the License. Wszystkie trasy: Ta trasa Podziel się z przyjaciółmi... - Plik GPX PLik KML Plik CSV diff --git a/MyTracks/res/values-pt-rBR/strings.xml b/MyTracks/res/values-pt-rBR/strings.xml index 5dfb033b3..f9ee674ca 100644 --- a/MyTracks/res/values-pt-rBR/strings.xml +++ b/MyTracks/res/values-pt-rBR/strings.xml @@ -18,9 +18,9 @@ limitations under the License. Gravar caminho Para gravação - Modo do satélite" - Modo do mapa" - Minha Localização" + Modo do satélite + Modo do mapa + Minha Localização Caminhos Marcadores Apagar todos os caminhos @@ -31,10 +31,10 @@ limitations under the License. Último segmento - Gerenciador de localização não encontrado neste dispositivo!" - "Provedor de posição de GPS não encontrado neste dispositivo." - Ocorreu um erro ao enviar seu caminho para o Google Fusion Tables. Por favor tente novamente mais tarde." - Ocorreu um erro ao enviar seu caminho para o Google Docs. Por favor tente novamente mais tarde." + Gerenciador de localização não encontrado neste dispositivo! + "Provedor de posição de GPS não encontrado neste dispositivo. + Ocorreu um erro ao enviar seu caminho para o Google Fusion Tables. Por favor tente novamente mais tarde. + Ocorreu um erro ao enviar seu caminho para o Google Docs. Por favor tente novamente mais tarde. Impossível iniciar a gravação. Tente novamente. Impossível interromper a gravação. Tente novamente. Impossível ler do arquivo. @@ -52,7 +52,7 @@ limitations under the License. Seu caminho foi enviado para maps.google.com. Seu caminho foi enviado para uma nova planilha em docs.google.com. Seu caminho foi enviado para docs.google.com. - "Por favor pressione aqui para ir aos ajustes de segurança e localização e verifique que o GPS esteja habilitado. Pressione o botão de voltar para retornar." + "Por favor pressione aqui para ir aos ajustes de segurança e localização e verifique que o GPS esteja habilitado. Pressione o botão de voltar para retornar. Nota: Uma localização aproximada, determinada por torres de telefonia celular, está sendo mostrada. Sua localização não pôde ser determinada. Habilite fontes adicionais de localização (veja configurações de Segurança e Localização) e assegure-se de que o céu está visível. Um marcador de ponto de passagem foi inserido. @@ -79,19 +79,19 @@ limitations under the License. O marcador será permanentemente apagado deste telefone. - "Criado por My Tracks no Android." - "Criado por <a href='http://mytracks.appspot.com'>My Tracks</a> no Android." + "Criado por My Tracks no Android. + "Criado por <a href='http://mytracks.appspot.com'>My Tracks</a> no Android. Pontos - Gravado" - "Tipo de atividade" - "Tipo de marcador" - "(Parte %d)" + Gravado + "Tipo de atividade + "Tipo de marcador + "(Parte %d) - "Gravando seu caminho..." - "Caminho %d" + "Gravando seu caminho... + "Caminho %d Não @@ -122,22 +122,22 @@ limitations under the License. Enviar para o Google Fusion Tables Apagar todos os caminhos Escolha um caminho. Pressione&segure para mais opções. - Todos os Caminhos:" - Este Caminho" - Compartilhar com amigos..." - Um link para a Google Fusion Table" - Um arquivo GPX" - Um arquivo KML" - "Limpar mapa" + Todos os Caminhos: + Este Caminho + Compartilhar com amigos... + Um link para a mapa + Um arquivo GPX + Um arquivo KML + Limpar mapa Apagar todos os marcadores Este Marcador Apagar - Editar detalhes" - Mostrar no mapa" - Inserir ponto no caminho" - Inserir estatísticas" + Editar detalhes + Mostrar no mapa + Inserir ponto no caminho + Inserir estatísticas Escolha um marcador. Pressione&segure para mais opções. diff --git a/MyTracks/res/values-sv/strings.xml b/MyTracks/res/values-sv/strings.xml index a74ef8b99..950e8b1dc 100644 --- a/MyTracks/res/values-sv/strings.xml +++ b/MyTracks/res/values-sv/strings.xml @@ -131,7 +131,6 @@ limitations under the License. Alla spår: Det här spåret Dela med vänner... - En GPX-fil En KML-fil En CSV-fil diff --git a/MyTracks/res/values-zh-rCN/strings.xml b/MyTracks/res/values-zh-rCN/strings.xml index d9b27a43c..0da162428 100644 --- a/MyTracks/res/values-zh-rCN/strings.xml +++ b/MyTracks/res/values-zh-rCN/strings.xml @@ -132,7 +132,6 @@ limitations under the License. 全部轨迹: 此轨迹 与朋友分享... - 链接到 Google Fusion Table GPX 文件 KML 文件 CSV 文件 diff --git a/MyTracks/res/values/keys.xml b/MyTracks/res/values/keys.xml index b8e43f668..241e6c65e 100644 --- a/MyTracks/res/values/keys.xml +++ b/MyTracks/res/values/keys.xml @@ -36,6 +36,7 @@ limitations under the License. sendStatsAndPoints sendToDocs sendToMyMaps + sendToFusionTables sensorType shareUrlOnly splitFrequency diff --git a/MyTracks/res/values/strings.xml b/MyTracks/res/values/strings.xml index 9b5d4a24f..07f7707c3 100644 --- a/MyTracks/res/values/strings.xml +++ b/MyTracks/res/values/strings.xml @@ -35,6 +35,7 @@ limitations under the License. No location manager was found on this device! No GPS location provider was found on this device. + There was an error while sending your track to Google My Map. Please try again later. There was an error while sending your track to Google Fusion Tables. Please try again later. There was an error while sending your track to Google Docs. Please try again later. Unable to start recording. Try again. @@ -50,10 +51,12 @@ limitations under the License. Waiting for a better GPS fix. Make sure the sky is visible. Now recording a new track. Stopped recording your track. + A new Google My Map has been created at maps.google.com. A new Google Fusion table has been created at google.com/fusiontables. Your track has been sent to maps.google.com. Your track has been sent to a new spreadsheet at docs.google.com. Your track has been sent to docs.google.com. + It may sometimes take a few minutes before new tracks appear. Please tap here to go to Security and Location Settings and check \'enable GPS satellites\'. Press back button to return. Note: An approximate location as determined from cell towers and WIFI networks is shown. A location can currently not be determined. Enable more location sources (see Security and Location Settings) and make sure the sky is visible. Try again later. @@ -64,9 +67,13 @@ limitations under the License. Please wait... Sending your track to Google. Reading track... + Preparing track... Authenticating to Google Fusion Tables... + Authenticating to Google Maps... Authenticating to Google Docs... Sending track to Google Docs... + Sending track to Google Maps... + Creating new My Map... Sending track to Fusion Tables... Creating new Fusion table... Importing a GPS track. This may take a while. @@ -83,6 +90,10 @@ limitations under the License. Created by My Tracks on Android. Created by <a href=\'http://mytracks.appspot.com\'>My Tracks</a> on Android. + + Created by <a href=\'http://mytracks.appspot.com\'>My Tracks</a> on Android. + New map + Points Recorded @@ -113,6 +124,8 @@ limitations under the License. Show + Send to Google My Maps + Send to New Google My Map Send to Google Fusion Tables Send to New Google Fusion Table Send to Google Docs @@ -125,6 +138,7 @@ limitations under the License. Save a CSV file Save a TCX file Send to Google Fusion Tables + Send to Google My Maps Delete all tracks Export all tracks @@ -133,7 +147,7 @@ limitations under the License. All Tracks: This Track Share with Friends... - A link to Google Fusion Tables + A link to the map A GPX file A KML file A CSV file @@ -243,6 +257,23 @@ limitations under the License. GPS Satellites + + Pick a map from your account: + Create new map... + Send to Google My Maps + Loading maps... + Your maps could not be loaded at this time! + Select this Map + Edit Map Details + Share Track with Friends + Delete Map from your Account + Map Title: + Map Description: + Make this map and its contents show up in search results + Do not let this map or its contents show up in search results, though they may still be viewed be others through the direct URL + This map and its contents will show in search results + Neither this map nor its contents will show in search results, but they may still be viewed by others through a direct link + Share map with friends I would like to share a track with you @@ -366,8 +397,11 @@ limitations under the License. Send to... Google Fusion Tables + Google My Maps Google Docs Send now + Create a new map + Pick an existing map Send track statistics Send statistics and points diff --git a/MyTracks/src/com/google/android/apps/mytracks/DialogManager.java b/MyTracks/src/com/google/android/apps/mytracks/DialogManager.java index e8512ac3b..69d673296 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/DialogManager.java +++ b/MyTracks/src/com/google/android/apps/mytracks/DialogManager.java @@ -88,7 +88,10 @@ public class DialogManager { builder.setNeutralButton(activity.getString(R.string.share_track), new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { - activity.shareLinkToFusionTable(activity.sendToTrackId); + SendToGoogleDialog sendToGoogleDialog = getSendToGoogleDialog(); + final boolean sentToMyMaps = sendToGoogleDialog.getSendToMyMaps(); + final boolean sentToFusionTables = sendToGoogleDialog.getSendToFusionTables(); + activity.shareLinkToMap(sentToMyMaps, sentToFusionTables); dialog.dismiss(); } }); @@ -119,9 +122,11 @@ public class DialogManager { sendToGoogleResultDialog.setIcon(success ? android.R.drawable.ic_dialog_info : android.R.drawable.ic_dialog_alert); - sendToGoogleResultDialog.setMessage(activity.getFusionTablesResultMessage()); + sendToGoogleResultDialog.setMessage(activity.getSendToGoogleResultMessage()); - boolean canShare = activity.getSendToFusionTablesTableId() != null; + boolean canShare = + activity.getSendToFusionTablesTableId() != null && + activity.getSendToMyMapsMapId() != null; View share = sendToGoogleResultDialog.findViewById(android.R.id.button3); if (share != null) { diff --git a/MyTracks/src/com/google/android/apps/mytracks/MyMapsList.java b/MyTracks/src/com/google/android/apps/mytracks/MyMapsList.java new file mode 100644 index 000000000..949c90026 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/MyMapsList.java @@ -0,0 +1,210 @@ +/* + * Copyright 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.google.android.apps.mytracks; + +import com.google.android.accounts.Account; +import com.google.android.apps.mytracks.io.AuthManager; +import com.google.android.apps.mytracks.io.AuthManagerFactory; +import com.google.android.apps.mytracks.io.MyMapsFactory; +import com.google.android.apps.mytracks.io.mymaps.MapsFacade; +import com.google.android.apps.mytracks.io.mymaps.MapsService; +import com.google.android.maps.mytracks.R; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.view.ContextMenu; +import android.view.ContextMenu.ContextMenuInfo; +import android.view.MenuItem; +import android.view.View; +import android.view.View.OnCreateContextMenuListener; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.ListView; +import android.widget.TextView; + +/** + * Activity which displays the list of current My Maps tracks for the user. + * Returns RESULT_OK if the user picked a map, and returns "mapid" as an extra. + * + * @author Rodrigo Damazio + */ +public class MyMapsList extends Activity implements MapsFacade.MapsListCallback { + private static final int MENU_OPEN = 0; + private static final int MENU_SHARE = 2; + private static final int GET_LOGIN = 1; + + private MapsFacade mapsClient; + private AuthManager auth; + private MyMapsListAdapter listAdapter; + + private int contextPosition; + + private final OnCreateContextMenuListener contextMenuListener = + new OnCreateContextMenuListener() { + @Override + public void onCreateContextMenu(ContextMenu menu, View v, + ContextMenuInfo menuInfo) { + AdapterView.AdapterContextMenuInfo info = + (AdapterView.AdapterContextMenuInfo) menuInfo; + contextPosition = info.position; + menu.add(0, MENU_OPEN, 0, R.string.open_map); + menu.add(0, MENU_SHARE, 0, R.string.share_map); + } + }; + + private final OnItemClickListener clickListener = + new OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View v, int position, + long id) { + Intent result = new Intent(); + result.putExtra("mapid", (String) listAdapter.getItem(position)); + setResult(RESULT_OK, result); + finish(); + } + }; + + /** Called when the activity is first created. */ + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + + auth = AuthManagerFactory.getAuthManager(this, GET_LOGIN, null, true, + MapsService.getServiceName()); + + setContentView(R.layout.list); + + listAdapter = new MyMapsListAdapter(this); + + ListView list = (ListView) findViewById(R.id.maplist); + list.setOnItemClickListener(clickListener); + list.setOnCreateContextMenuListener(contextMenuListener); + list.setAdapter(listAdapter); + + startLogin(); + } + + private void startLogin() { + // Starts in the UI thread. + // TODO fix this for non-froyo devices. + if (AuthManagerFactory.useModernAuthManager()) { + MyTracks.getInstance().getAccountChooser().chooseAccount( + MyMapsList.this, + new AccountChooser.AccountHandler() { + @Override + public void handleAccountSelected(Account account) { + // Account selection happens in the UI thread. + if (account != null) { + // The user did not quit and there was a valid google + // account. + doLogin(account); + } + } + }); + } else { + doLogin(null); + } + } + + private void doLogin(final Account account) { + // Starts in the UI thread. + auth.doLogin(new Runnable() { + public void run() { + // Runs in UI thread. + mapsClient = MyMapsFactory.newMapsClient(MyMapsList.this, auth); + + startLookup(); + } + }, account); + } + + private void startLookup() { + // Starts in the UI thread. + new Thread() { + @Override + public void run() { + // Communication with Maps happens in its own thread. + // This will call onReceivedMapListing below. + final boolean success = mapsClient.getMapsList(MyMapsList.this); + + runOnUiThread(new Runnable() { + @Override + public void run() { + // Updating the UI when done happens in the UI thread. + onLookupDone(success); + } + }); + } + }.start(); + } + + @Override + public void onReceivedMapListing(final String mapId, final String title, + final String description, final boolean isPublic) { + // Starts in the communication thread. + runOnUiThread(new Runnable() { + @Override + public void run() { + // Updating the list with new contents happens in the UI thread. + listAdapter.addMapListing(mapId, title, description, isPublic); + } + }); + } + + private void onLookupDone(boolean success) { + // Starts in the UI thread. + findViewById(R.id.loading).setVisibility(View.GONE); + if (!success) { + findViewById(R.id.failed).setVisibility(View.VISIBLE); + } + TextView emptyView = (TextView) findViewById(R.id.mapslist_empty); + ListView list = (ListView) findViewById(R.id.maplist); + list.setEmptyView(emptyView); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, + Intent data) { + if (requestCode == GET_LOGIN) { + auth.authResult(resultCode, data); + } + super.onActivityResult(requestCode, resultCode, data); + } + + @Override + public boolean onMenuItemSelected(int featureId, MenuItem item) { + switch (item.getItemId()) { + case MENU_OPEN: + clickListener.onItemClick(null, null, contextPosition, 0); + return true; + case MENU_SHARE: + Intent shareIntent = new Intent(Intent.ACTION_SEND); + shareIntent.setType("text/plain"); + shareIntent.putExtra(Intent.EXTRA_SUBJECT, + getText(R.string.share_map_subject)); + String[] listItem = (String[]) listAdapter.getMapListingArray(contextPosition); + shareIntent.putExtra(Intent.EXTRA_TEXT, String.format( + getText(R.string.share_map_body_format).toString(), + listItem[1], + MapsService.buildMapUrl(listItem[0]))); + startActivity(Intent.createChooser(shareIntent, + getText(R.string.share_map).toString())); + return true; + } + return false; + } +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/MyMapsListAdapter.java b/MyTracks/src/com/google/android/apps/mytracks/MyMapsListAdapter.java new file mode 100644 index 000000000..5780a950b --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/MyMapsListAdapter.java @@ -0,0 +1,135 @@ +package com.google.android.apps.mytracks; + +import com.google.android.maps.mytracks.R; + +import android.app.Activity; +import android.database.DataSetObserver; +import android.graphics.Color; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ListAdapter; +import android.widget.TextView; + +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; +import java.util.Vector; + +public class MyMapsListAdapter implements ListAdapter { + private Vector mapsList; + private Vector publicList; + private Set observerSet; + private final Activity activity; + + public MyMapsListAdapter(Activity activity) { + this.activity = activity; + + mapsList = new Vector(); + publicList = new Vector(); + observerSet = new HashSet(); + } + + public void addMapListing(String mapId, String title, + String description, boolean isPublic) { + synchronized (mapsList) { + // Search through the maps list to see if it has the mapid and + // remove it if so, so that we can replace it with updated info + for (int i = 0; i < mapsList.size(); ++i) { + if (mapId.equals(mapsList.get(i)[0])) { + mapsList.remove(i); + publicList.remove(i); + --i; + } + } + mapsList.add(new String[] { mapId, title, description }); + publicList.add(isPublic); + } + + Iterator iter = observerSet.iterator(); + while (iter.hasNext()) { + iter.next().onChanged(); + } + } + + public String[] getMapListingArray(int position) { + return mapsList.get(position); + } + + @Override + public boolean areAllItemsEnabled() { + return true; + } + + @Override + public boolean isEnabled(int position) { + return true; + } + + @Override + public int getCount() { + return mapsList.size(); + } + + @Override + public Object getItem(int position) { + return mapsList.get(position)[0]; + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public int getItemViewType(int position) { + return 0; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + if (convertView == null) { + convertView = + activity.getLayoutInflater().inflate(R.layout.listitem, parent, false); + } + + String[] map = mapsList.get(position); + ((TextView) convertView.findViewById(R.id.maplistitem)).setText(map[1]); + ((TextView) convertView.findViewById(R.id.maplistdesc)).setText(map[2]); + TextView publicUnlisted = + (TextView) convertView.findViewById(R.id.maplistpublic); + if (publicList.get(position)) { + publicUnlisted.setTextColor(Color.RED); + publicUnlisted.setText(R.string.public_map); + } else { + publicUnlisted.setTextColor(Color.GREEN); + publicUnlisted.setText(R.string.unlisted_map); + } + + return convertView; + } + + @Override + public int getViewTypeCount() { + return 1; + } + + @Override + public boolean hasStableIds() { + return false; + } + + @Override + public boolean isEmpty() { + return mapsList.isEmpty(); + } + + @Override + public void registerDataSetObserver(DataSetObserver observer) { + observerSet.add(observer); + } + + @Override + public void unregisterDataSetObserver(DataSetObserver observer) { + observerSet.remove(observer); + } +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java b/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java index 34f6a7361..0390065af 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java +++ b/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java @@ -29,11 +29,13 @@ import com.google.android.apps.mytracks.io.AuthManagerFactory; import com.google.android.apps.mytracks.io.GpxImporter; import com.google.android.apps.mytracks.io.SendToDocs; import com.google.android.apps.mytracks.io.SendToFusionTables; +import com.google.android.apps.mytracks.io.SendToFusionTables.OnSendCompletedListener; +import com.google.android.apps.mytracks.io.SendToMyMaps; import com.google.android.apps.mytracks.io.TempFileCleaner; import com.google.android.apps.mytracks.io.TrackWriter; import com.google.android.apps.mytracks.io.TrackWriterFactory; -import com.google.android.apps.mytracks.io.SendToFusionTables.OnSendCompletedListener; import com.google.android.apps.mytracks.io.TrackWriterFactory.TrackFileFormat; +import com.google.android.apps.mytracks.io.mymaps.MapsService; import com.google.android.apps.mytracks.services.ITrackRecordingService; import com.google.android.apps.mytracks.services.StatusAnnouncerFactory; import com.google.android.apps.mytracks.services.TrackRecordingService; @@ -67,9 +69,9 @@ import android.view.Menu; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; -import android.view.Window; import android.view.View.OnTouchListener; import android.view.ViewGroup.LayoutParams; +import android.view.Window; import android.widget.RelativeLayout; import android.widget.TabHost; import android.widget.Toast; @@ -122,8 +124,11 @@ public class MyTracks extends TabActivity implements OnTouchListener, */ public long sendToTrackId = -1; + public boolean sendToMyMapsSuccess = false; public boolean sendToFusionTablesSuccess = false; public boolean sendToDocsSuccess = false; + public String sendToMyMapsMapId; + public String sendToMyMapsMessage = ""; public String sendToFusionTablesTableId; public String sendToFusionTablesMessage = ""; public String sendToDocsMessage = ""; @@ -446,27 +451,8 @@ public class MyTracks extends TabActivity implements OnTouchListener, final Intent results) { TrackFileFormat exportFormat = null; switch (requestCode) { - case MyTracksConstants.GET_MAP: { - if (resultCode == RESULT_OK) { - results.putExtra("trackid", selectedTrackId); - if (results.hasExtra("tableid")) { - sendToFusionTablesTableId = results.getStringExtra("tableid"); - } - setProgressMessage( - R.string.progress_message_authenticating_fusiontables); - authenticate(results, MyTracksConstants.SEND_TO_GOOGLE, - SendToFusionTables.SERVICE_ID); - } else { - dialogManager.dismissDialogSafely(DIALOG_PROGRESS); - } - break; - } case MyTracksConstants.GET_LOGIN: { - if (resultCode == RESULT_OK && auth != null) { - if (!auth.authResult(resultCode, results)) { - dialogManager.dismissDialogSafely(DIALOG_PROGRESS); - } - } else { + if (resultCode != RESULT_OK || auth == null || !auth.authResult(resultCode, results)) { dialogManager.dismissDialogSafely(DIALOG_PROGRESS); } break; @@ -515,67 +501,48 @@ public class MyTracks extends TabActivity implements OnTouchListener, } break; } - case MyTracksConstants.AUTHENTICATE_TO_DOCLIST: { - if (resultCode == RESULT_OK) { - setProgressValue(0); - setProgressMessage( - R.string.progress_message_authenticating_docs); - authenticate(results, - MyTracksConstants.AUTHENTICATE_TO_TRIX, - SendToDocs.GDATA_SERVICE_NAME_DOCLIST); - } else { - dialogManager.dismissDialogSafely(DIALOG_PROGRESS); - } - break; - } - case MyTracksConstants.AUTHENTICATE_TO_TRIX: { - if (resultCode == RESULT_OK) { - setProgressValue(30); - setProgressMessage( - R.string.progress_message_authenticating_docs); - authenticate(results, MyTracksConstants.SEND_TO_DOCS, - SendToDocs.GDATA_SERVICE_NAME_TRIX); - } else { - dialogManager.dismissDialogSafely(DIALOG_PROGRESS); - } - break; - } - case MyTracksConstants.SEND_TO_DOCS: { - if (results != null && resultCode == RESULT_OK) { - Log.d(MyTracksConstants.TAG, "Sending to Docs...."); - setProgressValue(50); - setProgressMessage(R.string.progress_message_sending_docs); - final long trackId = results.getLongExtra("trackid", selectedTrackId); - final SendToDocs sender = new SendToDocs(this, - authMap.get(SendToDocs.GDATA_SERVICE_NAME_TRIX), - authMap.get(SendToDocs.GDATA_SERVICE_NAME_DOCLIST), trackId); - sendToTrackId = trackId; - Runnable onCompletion = new Runnable() { - public void run() { - setProgressValue(100); - dialogManager.dismissDialogSafely(DIALOG_PROGRESS); - runOnUiThread(new Runnable() { - public void run() { - sendToDocsMessage = sender.getStatusMessage(); - sendToDocsSuccess = sender.wasSuccess(); - handleFusionTablesFinish(trackId); - } - }); - } - }; - sender.setOnCompletion(onCompletion); - sender.run(); - } else { - dialogManager.dismissDialogSafely(DIALOG_PROGRESS); - } - break; - } case MyTracksConstants.SEND_TO_GOOGLE_DIALOG: { - shareRequested = false; + shareRequested = false; dialogManager.showDialogSafely(DIALOG_SEND_TO_GOOGLE); break; } - case MyTracksConstants.SEND_TO_GOOGLE: { + case MyTracksConstants.GET_MAP: { + // User picked a map to upload to + if (resultCode == RESULT_OK) { + results.putExtra("trackid", selectedTrackId); + if (results.hasExtra("mapid")) { + sendToMyMapsMapId = results.getStringExtra("mapid"); + } + authenticateToGoogleMaps(results); + } else { + onSendToGoogleDone(); + } + break; + } + case MyTracksConstants.AUTHENTICATE_TO_MY_MAPS: { + // Authenticated with Google My Maps + if (results != null && resultCode == RESULT_OK) { + final String mapId; + final long trackId; + if (results.hasExtra("mapid")) { + mapId = results.getStringExtra("mapid"); + } else { + mapId = "new"; + } + if (results.hasExtra("trackid")) { + trackId = results.getLongExtra("trackid", -1); + } else { + trackId = selectedTrackId; + } + + sendToGoogleMaps(trackId, mapId); + } else { + onSendToGoogleDone(); + } + break; + } + case MyTracksConstants.AUTHENTICATE_TO_FUSION_TABLES: { + // Authenticated with Google Fusion Tables if (results != null && resultCode == RESULT_OK) { final long trackId; if (results.hasExtra("trackid")) { @@ -583,48 +550,29 @@ public class MyTracks extends TabActivity implements OnTouchListener, } else { trackId = selectedTrackId; } - OnSendCompletedListener onCompletion = new OnSendCompletedListener() { - @Override - public void onSendCompleted(String tableId, boolean success, - int statusMessage) { - sendToFusionTablesMessage = getString(statusMessage); - sendToFusionTablesSuccess = success; - if (sendToFusionTablesSuccess) { - sendToFusionTablesTableId = tableId; - // Update the table id for this track: - try { - Track track = providerUtils.getTrack(trackId); - track.setTableId(tableId); - providerUtils.updateTrack(track); - } catch (RuntimeException e) { - // If that fails whatever reasons we'll just log an error, but - // continue. - Log.w(MyTracksConstants.TAG, "Updating table id failed.", e); - } - } - if (dialogManager.getSendToGoogleDialog().getSendToDocs()) { - onActivityResult(MyTracksConstants.AUTHENTICATE_TO_DOCLIST, - RESULT_OK, new Intent()); - } else { - dialogManager.dismissDialogSafely(DIALOG_PROGRESS); - runOnUiThread(new Runnable() { - public void run() { - handleFusionTablesFinish(trackId); - } - }); - } - } - }; - sendToTrackId = trackId; - final SendToFusionTables sender = new SendToFusionTables(this, auth, - trackId, this/*progressIndicator*/, onCompletion); - HandlerThread handlerThread = new HandlerThread("SendToFusionTables"); - handlerThread.start(); - Handler handler = new Handler(handlerThread.getLooper()); - handler.post(sender); + sendToFusionTables(trackId); } else { - dialogManager.dismissDialogSafely(DIALOG_PROGRESS); + onSendToGoogleDone(); + } + break; + } + case MyTracksConstants.AUTHENTICATE_TO_DOCLIST: { + // Authenticated with Google Docs + if (resultCode == RESULT_OK) { + authenticateToGoogleTrix(); + } else { + onSendToGoogleDone(); + } + break; + } + case MyTracksConstants.AUTHENTICATE_TO_TRIX: { + // Authenticated with Trix + if (resultCode == RESULT_OK) { + final long trackId = results.getLongExtra("trackid", selectedTrackId); + sendToGoogleDocs(trackId); + } else { + onSendToGoogleDone(); } break; } @@ -650,8 +598,10 @@ public class MyTracks extends TabActivity implements OnTouchListener, case MyTracksConstants.SHARE_LINK: { Track selectedTrack = providerUtils.getTrack(selectedTrackId); if (selectedTrack != null) { - if (selectedTrack.getTableId().length() > 0) { - shareLinkToFusionTable(selectedTrackId); + if (!selectedTrack.getMapId().isEmpty()) { + shareLinkToMap(MapsService.buildMapUrl(selectedTrack.getMapId())); + } else if (!selectedTrack.getTableId().isEmpty()) { + shareLinkToMap(getFusionTablesUrl(selectedTrackId)); } else { shareRequested = true; dialogManager.showDialogSafely(DIALOG_SEND_TO_GOOGLE); @@ -720,6 +670,9 @@ public class MyTracks extends TabActivity implements OnTouchListener, * Resets status information for sending to MyMaps/Docs. */ public void resetSendToGoogleStatus() { + sendToMyMapsMapId = null; + sendToMyMapsMessage = ""; + sendToMyMapsSuccess = true; sendToFusionTablesMessage = ""; sendToFusionTablesSuccess = true; sendToDocsMessage = ""; @@ -792,10 +745,10 @@ public class MyTracks extends TabActivity implements OnTouchListener, } /** - * Shares a link to a fusion table via external app (email, gmail, ...) + * Shares a link to a My Map or Fusion Table via external app (email, gmail, ...) * A chooser with apps that support text/plain will be shown to the user. */ - public void shareLinkToFusionTable(long trackId) { + public void shareLinkToMap(String url) { Intent shareIntent = new Intent(Intent.ACTION_SEND); shareIntent.setType("text/plain"); shareIntent.putExtra(Intent.EXTRA_SUBJECT, @@ -807,8 +760,6 @@ public class MyTracks extends TabActivity implements OnTouchListener, getString(R.string.share_url_only_key), false); } - Track track = providerUtils.getTrack(trackId); - String url = SendToFusionTables.getMapVisualizationUrl(track); String msg = shareUrlOnly ? url : String.format( getResources().getText(R.string.share_map_body_format).toString(), url); shareIntent.putExtra(Intent.EXTRA_TEXT, msg); @@ -1027,6 +978,8 @@ public class MyTracks extends TabActivity implements OnTouchListener, * Initiates the process to send tracks to google. * This is called once the user has selected sending options via the * SendToGoogleDialog. + * + * TODO: Change this whole flow to an actual state machine. */ public void sendToGoogle() { SendToGoogleDialog sendToGoogleDialog = @@ -1037,17 +990,253 @@ public class MyTracks extends TabActivity implements OnTouchListener, setProgressValue(0); clearProgressMessage(); dialogManager.showDialogSafely(DIALOG_PROGRESS); - if (sendToGoogleDialog.getSendToFusionTables()) { - setProgressValue(0); - setProgressMessage(R.string.progress_message_authenticating_fusiontables); - authenticate(new Intent(), MyTracksConstants.SEND_TO_GOOGLE, - SendToFusionTables.SERVICE_ID); - } else { - onActivityResult(MyTracksConstants.AUTHENTICATE_TO_DOCLIST, RESULT_OK, - new Intent()); + + if (sendToGoogleDialog.getSendToMyMaps()) { + sendToGoogleMapsOrPickMap(sendToGoogleDialog); + } else if (sendToGoogleDialog.getSendToFusionTables()) { + authenticateToFusionTables(null); + } else if (sendToGoogleDialog.getSendToDocs()) { + authenticateToGoogleDocs(); + } else { + Log.w(MyTracksConstants.TAG, "Nowhere to upload to"); + onSendToGoogleDone(); } } + private void sendToGoogleMapsOrPickMap(SendToGoogleDialog sendToGoogleDialog) { + if (!sendToGoogleDialog.getCreateNewMap()) { + // Ask the user to choose a map to upload into + Intent listIntent = new Intent(this, MyMapsList.class); + startActivityForResult(listIntent, MyTracksConstants.GET_MAP); + // The callback for GET_MAP calls authenticateToGoogleMaps + } else { + authenticateToGoogleMaps(null); + } + } + + private void authenticateToGoogleMaps(Intent results) { + if (results == null) { results = new Intent(); } + + setProgressValue(0); + setProgressMessage( + R.string.progress_message_authenticating_mymaps); + authenticate(results, MyTracksConstants.AUTHENTICATE_TO_MY_MAPS, + MapsService.getServiceName()); + // AUTHENTICATE_TO_MY_MAPS callback calls sendToGoogleMaps + } + + private void sendToGoogleMaps(final long trackId, String mapId) { + SendToMyMaps.OnSendCompletedListener onCompletion = new SendToMyMaps.OnSendCompletedListener() { + @Override + public void onSendCompleted(String mapId, boolean success, int statusMessage) { + sendToMyMapsMessage = getString(statusMessage); + sendToMyMapsSuccess = success; + if (sendToMyMapsSuccess) { + sendToMyMapsMapId = mapId; + // Update the map id for this track: + try { + Track track = providerUtils.getTrack(trackId); + track.setMapId(mapId); + providerUtils.updateTrack(track); + } catch (RuntimeException e) { + // If that fails whatever reasons we'll just log an error, but + // continue. + Log.w(MyTracksConstants.TAG, "Updating map id failed.", e); + } + } + + onSendToGoogleMapsDone(); + } + }; + final SendToMyMaps sender = new SendToMyMaps(this, mapId, auth, + trackId, this /*progressIndicator*/, onCompletion); + + HandlerThread handlerThread = new HandlerThread("SendToMyMaps"); + handlerThread.start(); + Handler handler = new Handler(handlerThread.getLooper()); + handler.post(sender); + } + + private void onSendToGoogleMapsDone() { + SendToGoogleDialog sendToGoogleDialog = dialogManager.getSendToGoogleDialog(); + if (sendToGoogleDialog.getSendToFusionTables()) { + authenticateToFusionTables(null); + } else if (sendToGoogleDialog.getSendToDocs()) { + authenticateToGoogleDocs(); + } else { + onSendToGoogleDone(); + } + } + + private void authenticateToFusionTables(Intent results) { + if (results == null) { results = new Intent(); } + + setProgressValue(0); + setProgressMessage(R.string.progress_message_authenticating_fusiontables); + authenticate(results, MyTracksConstants.AUTHENTICATE_TO_FUSION_TABLES, + SendToFusionTables.SERVICE_ID); + // AUTHENTICATE_TO_FUSION_TABLES callback calls sendToFusionTables + } + + private void sendToFusionTables(final long trackId) { + OnSendCompletedListener onCompletion = new OnSendCompletedListener() { + @Override + public void onSendCompleted(String tableId, boolean success, + int statusMessage) { + sendToFusionTablesMessage = getString(statusMessage); + sendToFusionTablesSuccess = success; + if (sendToFusionTablesSuccess) { + sendToFusionTablesTableId = tableId; + // Update the table id for this track: + try { + Track track = providerUtils.getTrack(trackId); + track.setTableId(tableId); + providerUtils.updateTrack(track); + } catch (RuntimeException e) { + // If that fails whatever reasons we'll just log an error, but + // continue. + Log.w(MyTracksConstants.TAG, "Updating table id failed.", e); + } + } + + onSendToFusionTablesDone(); + } + }; + sendToTrackId = trackId; + final SendToFusionTables sender = new SendToFusionTables(this, auth, + trackId, this/*progressIndicator*/, onCompletion); + + HandlerThread handlerThread = new HandlerThread("SendToFusionTables"); + handlerThread.start(); + Handler handler = new Handler(handlerThread.getLooper()); + handler.post(sender); + } + + private void onSendToFusionTablesDone() { + SendToGoogleDialog sendToGoogleDialog = dialogManager.getSendToGoogleDialog(); + if (sendToGoogleDialog.getSendToDocs()) { + authenticateToGoogleDocs(); + } else { + onSendToGoogleDone(); + } + } + + private void authenticateToGoogleDocs() { + setProgressValue(0); + setProgressMessage( + R.string.progress_message_authenticating_docs); + authenticate(new Intent(), + MyTracksConstants.AUTHENTICATE_TO_DOCLIST, + SendToDocs.GDATA_SERVICE_NAME_DOCLIST); + // AUTHENTICATE_TO_DOCLIST callback calls authenticateToGoogleTrix + } + + private void authenticateToGoogleTrix() { + setProgressValue(30); + setProgressMessage( + R.string.progress_message_authenticating_docs); + authenticate(new Intent(), + MyTracksConstants.AUTHENTICATE_TO_TRIX, + SendToDocs.GDATA_SERVICE_NAME_TRIX); + // AUTHENTICATE_TO_TRIX callback calls sendToGoogleDocs + } + + private void sendToGoogleDocs(final long trackId) { + Log.d(MyTracksConstants.TAG, "Sending to Docs...."); + setProgressValue(50); + setProgressMessage(R.string.progress_message_sending_docs); + final SendToDocs sender = new SendToDocs(this, + authMap.get(SendToDocs.GDATA_SERVICE_NAME_TRIX), + authMap.get(SendToDocs.GDATA_SERVICE_NAME_DOCLIST), trackId); + sendToTrackId = trackId; + Runnable onCompletion = new Runnable() { + public void run() { + setProgressValue(100); + dialogManager.dismissDialogSafely(DIALOG_PROGRESS); + sendToDocsMessage = sender.getStatusMessage(); + sendToDocsSuccess = sender.wasSuccess(); + + onSendToGoogleDocsDone(); + } + }; + sender.setOnCompletion(onCompletion); + sender.run(); + } + + private void onSendToGoogleDocsDone() { + onSendToGoogleDone(); + } + + private void onSendToGoogleDone() { + SendToGoogleDialog sendToGoogleDialog = dialogManager.getSendToGoogleDialog(); + final boolean sentToMyMaps = sendToGoogleDialog.getSendToMyMaps(); + final boolean sentToFusionTables = sendToGoogleDialog.getSendToFusionTables(); + + dialogManager.dismissDialogSafely(DIALOG_PROGRESS); + runOnUiThread(new Runnable() { + public void run() { + if (shareRequested) { + Toast.makeText(MyTracks.this, getSendToGoogleResultMessage(), Toast.LENGTH_LONG) + .show(); + + if (shareLinkToMap(sentToMyMaps, sentToFusionTables)) { + return; + } + } + + // If anything failed or sharing was not requested, show the dialog + dialogManager.showDialogSafely( + DialogManager.DIALOG_SEND_TO_GOOGLE_RESULT); + } + }); + } + + boolean shareLinkToMap(boolean sentToMyMaps, boolean sentToFusionTables) { + String url = null; + if (sentToMyMaps && sendToMyMapsSuccess) { + // Prefer a link to My Maps + url = MapsService.buildMapUrl(sendToMyMapsMapId); + } else if (sentToFusionTables && sendToFusionTablesSuccess) { + // Otherwise try using the link to fusion tables + url = getFusionTablesUrl(sendToTrackId); + } + + if (url != null) { + shareLinkToMap(url); + return true; + } + + return false; + } + + protected String getFusionTablesUrl(long sendToTrackId2) { + Track track = providerUtils.getTrack(sendToTrackId); + return SendToFusionTables.getMapVisualizationUrl(track); + } + + String getSendToGoogleResultMessage() { + StringBuilder message = new StringBuilder(); + SendToGoogleDialog sendToGoogleDialog = + dialogManager.getSendToGoogleDialog(); + if (sendToGoogleDialog.getSendToMyMaps()) { + message.append(sendToMyMapsMessage); + } + if (sendToGoogleDialog.getSendToFusionTables()) { + message.append(sendToFusionTablesMessage); + } + if (sendToGoogleDialog.getSendToDocs()) { + if (message.length() > 0) { + message.append(' '); + } + message.append(sendToDocsMessage); + } + if (sendToMyMapsSuccess && sendToFusionTablesSuccess && sendToDocsSuccess) { + message.append(' '); + message.append(getString(R.string.status_mymap_info)); + } + return message.toString(); + } + /** * Writes the selected track id to the shared preferences. * Executed on the UI thread. @@ -1157,35 +1346,6 @@ public class MyTracks extends TabActivity implements OnTouchListener, writer.writeTrackAsync(); } - /** - * Notifies that uploading to fusion tables is finished. - */ - private void handleFusionTablesFinish(long trackId) { - if (shareRequested && sendToFusionTablesSuccess) { - // Just share - Toast.makeText(this, getFusionTablesResultMessage(), Toast.LENGTH_LONG).show(); - shareLinkToFusionTable(trackId); - } else { - dialogManager.showDialogSafely(DialogManager.DIALOG_SEND_TO_GOOGLE_RESULT); - } - } - - public String getFusionTablesResultMessage() { - StringBuilder message = new StringBuilder(); - SendToGoogleDialog sendToGoogleDialog = - dialogManager.getSendToGoogleDialog(); - if (sendToGoogleDialog.getSendToFusionTables()) { - message.append(sendToFusionTablesMessage); - } - if (sendToGoogleDialog.getSendToDocs()) { - if (message.length() > 0) { - message.append(' '); - } - message.append(sendToDocsMessage); - } - return message.toString(); - } - public AccountChooser getAccountChooser() { return accountChooser; } @@ -1202,6 +1362,10 @@ public class MyTracks extends TabActivity implements OnTouchListener, return dialogManager; } + public String getSendToMyMapsMapId() { + return sendToMyMapsMapId; + } + public String getSendToFusionTablesTableId() { return sendToFusionTablesTableId; } diff --git a/MyTracks/src/com/google/android/apps/mytracks/MyTracksConstants.java b/MyTracks/src/com/google/android/apps/mytracks/MyTracksConstants.java index b9d11009b..083e3f35e 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/MyTracksConstants.java +++ b/MyTracks/src/com/google/android/apps/mytracks/MyTracksConstants.java @@ -50,26 +50,26 @@ public abstract class MyTracksConstants { public static final int FEATURE_DETAILS = 5; public static final int START_RECORDING = 6; public static final int STOP_RECORDING = 7; - public static final int AUTHENTICATE_TO_DOCLIST = 8; - public static final int AUTHENTICATE_TO_TRIX = 9; - public static final int SEND_TO_DOCS = 10; - public static final int DELETE_TRACK = 11; - public static final int SEND_TO_GOOGLE = 12; - public static final int SEND_TO_GOOGLE_DIALOG = 13; - public static final int SHARE_LINK = 14; - public static final int SHARE_GPX_FILE = 15; - public static final int SHARE_KML_FILE = 16; - public static final int SHARE_CSV_FILE = 17; - public static final int SHARE_TCX_FILE = 18; - public static final int EDIT_DETAILS = 19; - public static final int SAVE_GPX_FILE = 20; - public static final int SAVE_KML_FILE = 21; - public static final int SAVE_CSV_FILE = 22; - public static final int SAVE_TCX_FILE = 23; - public static final int CLEAR_MAP = 24; - public static final int SHOW_WAYPOINT = 25; - public static final int EDIT_WAYPOINT = 26; - public static final int WELCOME = 27; + public static final int AUTHENTICATE_TO_MY_MAPS = 8; + public static final int AUTHENTICATE_TO_FUSION_TABLES = 9; + public static final int AUTHENTICATE_TO_DOCLIST = 10; + public static final int AUTHENTICATE_TO_TRIX = 11; + public static final int DELETE_TRACK = 13; + public static final int SEND_TO_GOOGLE_DIALOG = 14; + public static final int SHARE_LINK = 15; + public static final int SHARE_GPX_FILE = 16; + public static final int SHARE_KML_FILE = 17; + public static final int SHARE_CSV_FILE = 18; + public static final int SHARE_TCX_FILE = 19; + public static final int EDIT_DETAILS = 20; + public static final int SAVE_GPX_FILE = 21; + public static final int SAVE_KML_FILE = 22; + public static final int SAVE_CSV_FILE = 23; + public static final int SAVE_TCX_FILE = 24; + public static final int CLEAR_MAP = 25; + public static final int SHOW_WAYPOINT = 26; + public static final int EDIT_WAYPOINT = 27; + public static final int WELCOME = 28; /* * Menu ids: diff --git a/MyTracks/src/com/google/android/apps/mytracks/MyTracksList.java b/MyTracks/src/com/google/android/apps/mytracks/MyTracksList.java index 79f295b2e..f88421868 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/MyTracksList.java +++ b/MyTracks/src/com/google/android/apps/mytracks/MyTracksList.java @@ -83,7 +83,7 @@ public class MyTracksList extends ListActivity SubMenu share = menu.addSubMenu(0, MyTracksConstants.MENU_SHARE, 0, R.string.tracklist_share_track); share.add(0, MyTracksConstants.MENU_SHARE_LINK, 0, - R.string.tracklist_share_linkfusion); + R.string.tracklist_share_link); share.add(0, MyTracksConstants.MENU_SHARE_GPX_FILE, 0, R.string.tracklist_share_gpx_file); share.add(0, MyTracksConstants.MENU_SHARE_KML_FILE, 0, diff --git a/MyTracks/src/com/google/android/apps/mytracks/MyTracksMap.java b/MyTracks/src/com/google/android/apps/mytracks/MyTracksMap.java index 758c0f550..af000e7b1 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/MyTracksMap.java +++ b/MyTracks/src/com/google/android/apps/mytracks/MyTracksMap.java @@ -789,7 +789,7 @@ public class MyTracksMap extends MapActivity SubMenu share = menu.addSubMenu(0, MyTracksConstants.MENU_SHARE, 0, R.string.tracklist_share_track); share.add(0, MyTracksConstants.MENU_SHARE_LINK, 0, - R.string.tracklist_share_linkfusion); + R.string.tracklist_share_link); share.add(0, MyTracksConstants.MENU_SHARE_GPX_FILE, 0, R.string.tracklist_share_gpx_file); share.add(0, MyTracksConstants.MENU_SHARE_KML_FILE, 0, diff --git a/MyTracks/src/com/google/android/apps/mytracks/SendToGoogleDialog.java b/MyTracks/src/com/google/android/apps/mytracks/SendToGoogleDialog.java index f39be1d84..be5b83e81 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/SendToGoogleDialog.java +++ b/MyTracks/src/com/google/android/apps/mytracks/SendToGoogleDialog.java @@ -29,6 +29,7 @@ import android.widget.CheckBox; import android.widget.CompoundButton; import android.widget.CompoundButton.OnCheckedChangeListener; import android.widget.RadioButton; +import android.widget.RadioGroup; /** * A dialog where the user can choose where to send the tracks to, i.e. @@ -38,6 +39,10 @@ import android.widget.RadioButton; */ public class SendToGoogleDialog extends Dialog { + private RadioGroup groupMyMaps; + private RadioButton createNewMapRadioButton; + private RadioButton pickMapRadioButton; + private CheckBox sendToMyMapsCheckBox; private CheckBox sendToFusionTablesCheckBox; private CheckBox sendToDocsCheckBox; private RadioButton sendStatsRadioButton; @@ -72,19 +77,29 @@ public class SendToGoogleDialog extends Dialog { OnCheckedChangeListener checkBoxListener = new OnCheckedChangeListener() { public void onCheckedChanged(CompoundButton button, boolean checked) { sendButton.setEnabled( + sendToMyMapsCheckBox.isChecked() || sendToFusionTablesCheckBox.isChecked() || sendToDocsCheckBox.isChecked()); + groupMyMaps.setVisibility(sendToMyMapsCheckBox.isChecked() ? View.VISIBLE : View.INVISIBLE); //groupDocs.setVisibility( // sendToDocsCheckBox.isChecked() ? View.VISIBLE : View.INVISIBLE); } }; sendButton = (Button) findViewById(R.id.sendtogoogle_send_now); + groupMyMaps = (RadioGroup) findViewById(R.id.sendtogoogle_group_mymaps); + sendToMyMapsCheckBox = + (CheckBox) findViewById(R.id.sendtogoogle_google_mymaps); + sendToMyMapsCheckBox.setOnCheckedChangeListener(checkBoxListener); sendToFusionTablesCheckBox = (CheckBox) findViewById(R.id.sendtogoogle_google_fusiontables); sendToFusionTablesCheckBox.setOnCheckedChangeListener(checkBoxListener); sendToDocsCheckBox = (CheckBox) findViewById(R.id.sendtogoogle_google_docs); sendToDocsCheckBox.setOnCheckedChangeListener(checkBoxListener); + createNewMapRadioButton = + (RadioButton) findViewById(R.id.sendtogoogle_create_new_map); + pickMapRadioButton = + (RadioButton) findViewById(R.id.sendtogoogle_pick_existing_map); sendStatsRadioButton = (RadioButton) findViewById(R.id.sendtogoogle_send_stats); sendStatsAndPointsRadioButton = (RadioButton) findViewById( @@ -93,12 +108,21 @@ public class SendToGoogleDialog extends Dialog { SharedPreferences prefs = getContext().getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0); if (prefs != null) { - sendToFusionTablesCheckBox.setChecked( + sendToMyMapsCheckBox.setChecked( prefs.getBoolean( getContext().getString(R.string.send_to_my_maps_key), true)); + sendToFusionTablesCheckBox.setChecked( + prefs.getBoolean( + getContext().getString(R.string.send_to_fusion_tables_key), true)); sendToDocsCheckBox.setChecked( prefs.getBoolean( getContext().getString(R.string.send_to_docs_key), true)); + pickMapRadioButton.setChecked( + prefs.getBoolean( + getContext().getString(R.string.pick_existing_map_key), false)); + createNewMapRadioButton.setChecked( + !prefs.getBoolean( + getContext().getString(R.string.pick_existing_map_key), false)); sendStatsAndPointsRadioButton.setChecked( prefs.getBoolean( getContext().getString(R.string.send_stats_and_points_key), @@ -119,10 +143,16 @@ public class SendToGoogleDialog extends Dialog { if (editor != null) { editor.putBoolean( getContext().getString(R.string.send_to_my_maps_key), + sendToMyMapsCheckBox.isChecked()); + editor.putBoolean( + getContext().getString(R.string.send_to_fusion_tables_key), sendToFusionTablesCheckBox.isChecked()); editor.putBoolean( getContext().getString(R.string.send_to_docs_key), sendToDocsCheckBox.isChecked()); + editor.putBoolean( + getContext().getString(R.string.pick_existing_map_key), + pickMapRadioButton.isChecked()); editor.putBoolean( getContext().getString(R.string.send_stats_and_points_key), sendStatsAndPointsRadioButton.isChecked()); @@ -132,6 +162,10 @@ public class SendToGoogleDialog extends Dialog { super.onStop(); } + public boolean getSendToMyMaps() { + return sendToMyMapsCheckBox.isChecked(); + } + public boolean getSendToFusionTables() { return sendToFusionTablesCheckBox.isChecked(); } @@ -140,6 +174,10 @@ public class SendToGoogleDialog extends Dialog { return sendToDocsCheckBox.isChecked(); } + public boolean getCreateNewMap() { + return createNewMapRadioButton.isChecked(); + } + public boolean getSendStatsAndPoints() { return sendStatsAndPointsRadioButton.isChecked(); } diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/MyMapsFactory.java b/MyTracks/src/com/google/android/apps/mytracks/io/MyMapsFactory.java new file mode 100644 index 000000000..8d17e1667 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/io/MyMapsFactory.java @@ -0,0 +1,68 @@ +package com.google.android.apps.mytracks.io; + +import com.google.android.apps.mytracks.io.gdata.GDataClientFactory; +import com.google.android.apps.mytracks.io.mymaps.MapsFacade; +import com.google.android.apps.mytracks.io.mymaps.MapsService; +import com.google.android.apps.mytracks.io.mymaps.MapsStringsProvider; +import com.google.android.apps.mytracks.io.mymaps.MapsFacade.AuthenticationRefresher; +import com.google.android.maps.mytracks.R; + +import android.app.Activity; +import android.content.Context; + +/** + * Factory to easily instantiate a {@link MapsFacade}. + * + * @author Rodrigo Damazio + */ +public class MyMapsFactory { + + /** + * Simple provider for reading strings from resources. + */ + private static class MyMapsStringProvider implements MapsStringsProvider { + private final Context context; + + public MyMapsStringProvider(Context context) { + this.context = context; + } + + @Override + public String getNewMapDescription() { + return context.getString(R.string.new_map_description); + } + + @Override + public String getStart() { + return context.getString(R.string.start); + } + + @Override + public String getEnd() { + return context.getString(R.string.end); + } + } + + /** + * Creates a new Maps fa�ade interface instance. + * + * @param context current context + * @param auth a valid authentication manager + * @return the fa�ade + */ + public static MapsFacade newMapsClient(Activity context, final AuthManager auth) { + MapsFacade client = MapsService.newClient(context, + GDataClientFactory.getGDataClient(context), + new MyMapsStringProvider(context), + auth.getAuthToken()); + client.setAuthenticationRefresher(new AuthenticationRefresher() { + @Override + public void invalidateAndRefresh(Runnable done) { + auth.invalidateAndRefresh(done); + } + }); + return client; + } + + private MyMapsFactory() {} +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/SendToDocs.java b/MyTracks/src/com/google/android/apps/mytracks/io/SendToDocs.java index 1bbbf8cf0..5c9254cd0 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/SendToDocs.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/SendToDocs.java @@ -28,7 +28,7 @@ import com.google.android.maps.mytracks.R; import com.google.wireless.gdata.client.GDataClient; import com.google.wireless.gdata.docs.DocumentsClient; import com.google.wireless.gdata.docs.SpreadsheetsClient; -import com.google.wireless.gdata.maps.xml.XmlMapsGDataParserFactory; +import com.google.wireless.gdata.docs.XmlDocsGDataParserFactory; import com.google.wireless.gdata.client.GDataServiceClient; import android.app.Activity; @@ -99,6 +99,7 @@ public class SendToDocs { } private void doUpload() { + // TODO statusMessage = activity.getString(R.string.error_sending_to_fusiontables); success = false; @@ -173,7 +174,7 @@ public class SendToDocs { try { androidClient = GDataClientFactory.getGDataClient(activity); SpreadsheetsClient gdataClient = new SpreadsheetsClient(androidClient, - new XmlMapsGDataParserFactory(new AndroidXmlParserFactory())); + new XmlDocsGDataParserFactory(new AndroidXmlParserFactory())); trixWrapper.setClient(gdataClient); Log.d(MyTracksConstants.TAG, "GData connection prepared: " + this.docListAuth); @@ -184,7 +185,7 @@ public class SendToDocs { } DocumentsClient docsGdataClient = new DocumentsClient(androidClient, - new XmlMapsGDataParserFactory(new AndroidXmlParserFactory())); + new XmlDocsGDataParserFactory(new AndroidXmlParserFactory())); docListWrapper.setClient(docsGdataClient); // First try to find the spreadsheet: diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/SendToMyMaps.java b/MyTracks/src/com/google/android/apps/mytracks/io/SendToMyMaps.java new file mode 100644 index 000000000..542606426 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/io/SendToMyMaps.java @@ -0,0 +1,458 @@ +/* + * Copyright 2008 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.google.android.apps.mytracks.io; + +import com.google.android.apps.mytracks.MyTracksConstants; +import com.google.android.apps.mytracks.MyTracksSettings; +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.mymaps.MapsFacade; +import com.google.android.apps.mytracks.io.mymaps.MapsFacade.WaypointData; +import com.google.android.apps.mytracks.stats.DoubleBuffer; +import com.google.android.apps.mytracks.stats.TripStatistics; +import com.google.android.apps.mytracks.util.MyTracksUtils; +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.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 MyMaps. + * 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 SendToMyMaps 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, int statusMessage); + } + + public SendToMyMaps(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(MyTracksConstants.TAG, "Sending to MyMaps: trackId = " + trackId); + doUpload(); + } + + private void doUpload() { + int statusMessageId = R.string.error_sending_to_mymap; + boolean success = true; + try { + progressIndicator.setProgressValue(1); + progressIndicator.setProgressMessage( + R.string.progress_message_reading_track); + + // Get the track meta-data + Track track = providerUtils.getTrack(trackId); + String originalDescription = track.getDescription(); + track.setDescription("

" + track.getDescription() + "

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

"); + + mapsClient = MyMapsFactory.newMapsClient(context, auth); + + // Create a new map if necessary: + boolean isNewMap = mapId.equals(NEW_MAP_ID); + if (isNewMap) { + SharedPreferences preferences = context.getSharedPreferences( + MyTracksSettings.SETTINGS_NAME, 0); + boolean mapPublic = true; + if (preferences != null) { + mapPublic = preferences.getBoolean( + context.getString(R.string.default_map_public_key), true); + } + + progressIndicator.setProgressMessage( + R.string.progress_message_creating_map); + + 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, + MyTracksConstants.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(new WaypointData( + wpt.getName(), wpt.getDescription(), wpt.getIcon(), wpt.getLocation())); + } + + success = mapsClient.uploadWaypoints(mapId, waypoints); + } + } finally { + c.close(); + } + } else { + success = false; + } + + if (!success) { + Log.w(MyTracksConstants.TAG, + "SendToMyMaps: upload waypoints failed."); + } + } + + if (success) { + statusMessageId = isNewMap + ? R.string.status_new_mymap_has_been_created + : R.string.status_tracks_have_been_uploaded; + } + Log.d(MyTracksConstants.TAG, "SendToMyMaps: Done: " + success); + progressIndicator.setProgressValue(100); + } finally { + if (mapsClient != null) { + mapsClient.cleanUp(); + } + + final boolean finalSuccess = success; + final int finalStatusMessageId = statusMessageId; + context.runOnUiThread(new Runnable() { + public void run() { + if (onCompletion != null) { + onCompletion.onSendCompleted( + mapId, finalSuccess, finalStatusMessageId); + } + } + }); + } + } + + private boolean uploadAllTrackPoints( + final Track track, String originalDescription) { + SharedPreferences preferences = context.getSharedPreferences( + MyTracksSettings.SETTINGS_NAME, 0); + 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.moveToFirst()) { + Log.w(MyTracksConstants.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(MyTracksConstants.TAG, + "Using elevation sampling factor: " + elevationSamplingFrequency + + " on " + totalLocations); + double totalDistance = 0; + + Vector distances = new Vector(); + Vector elevations = new Vector(); + DoubleBuffer elevationBuffer = + new DoubleBuffer(MyTracksConstants.ELEVATION_SMOOTHING_FACTOR); + + List locations = new ArrayList(MAX_POINTS_PER_UPLOAD); + progressIndicator.setProgressMessage( + R.string.progress_message_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 && MyTracksUtils.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 { + locationsCursor.close(); + } + } + + + private boolean prepareAndUploadPoints(Track track, List locations) { + progressIndicator.setProgressMessage( + R.string.progress_message_preparing_track); + updateProgress(); + + int numLocations = locations.size(); + if (numLocations < 2) { + Log.d(MyTracksConstants.TAG, "Not preparing/uploading too few points"); + totalLocationsUploaded += numLocations; + return true; + } + + // Prepare/pre-process the points + ArrayList splitTracks = prepareLocations(track, locations); + + // Start uploading them + progressIndicator.setProgressMessage( + R.string.progress_message_sending_mymaps); + for (Track splitTrack : splitTracks) { + if (totalSegmentsUploaded > 1) { + splitTrack.setName(splitTrack.getName() + " " + + String.format( + context.getString(R.string.part), totalSegmentsUploaded)); + } + totalSegmentsUploaded++; + Log.d(MyTracksConstants.TAG, + "SendToMyMaps: Prepared feature for upload w/ " + + splitTrack.getLocations().size() + " points."); + + // Transmit tracks via GData feed: + // ------------------------------- + Log.d(MyTracksConstants.TAG, + "SendToMyMaps: Uploading to map " + mapId + " w/ auth " + auth); + if (!mapsClient.uploadTrackPoints(mapId, splitTrack.getName(), splitTrack.getLocations())) { + Log.e(MyTracksConstants.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 buffer 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(MyTracksConstants.TAG, + "MyTracksSendToMyMaps: 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: + */ + MyTracksUtils.decimate(segment, 2.0); + + /* It the track still has > 500 points, split it in pieces: */ + if (segment.getLocations().size() > 500) { + splitTracks.addAll(MyTracksUtils.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) + / (totalLocations * 3); + totalPercentage = Math.min(99, totalPercentage); + progressIndicator.setProgressValue(totalPercentage); + } +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/TrackRecordingService.java b/MyTracks/src/com/google/android/apps/mytracks/services/TrackRecordingService.java index 576504d8a..4fc6d85eb 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/TrackRecordingService.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/TrackRecordingService.java @@ -1076,7 +1076,7 @@ public class TrackRecordingService extends Service implements LocationListener { recordingTrackId, startTime)); isRecording = true; isMoving = true; - + providerUtils.updateTrack(track); statsBuilder = new TripStatisticsBuilder(startTime); waypointStatsBuilder = new TripStatisticsBuilder(startTime); diff --git a/MyTracks/src/com/google/wireless/gdata/maps/xml/XmlMapsGDataParserFactory.java b/MyTracks/src/com/google/wireless/gdata/docs/XmlDocsGDataParserFactory.java similarity index 57% rename from MyTracks/src/com/google/wireless/gdata/maps/xml/XmlMapsGDataParserFactory.java rename to MyTracks/src/com/google/wireless/gdata/docs/XmlDocsGDataParserFactory.java index ef2f72b6b..09840754e 100644 --- a/MyTracks/src/com/google/wireless/gdata/maps/xml/XmlMapsGDataParserFactory.java +++ b/MyTracks/src/com/google/wireless/gdata/docs/XmlDocsGDataParserFactory.java @@ -13,12 +13,10 @@ * License for the specific language governing permissions and limitations under * the License. */ -package com.google.wireless.gdata.maps.xml; +package com.google.wireless.gdata.docs; import com.google.wireless.gdata.client.GDataParserFactory; import com.google.wireless.gdata.data.Entry; -import com.google.wireless.gdata.maps.MapFeatureEntry; -import com.google.wireless.gdata.maps.MapsClient; import com.google.wireless.gdata.parser.GDataParser; import com.google.wireless.gdata.parser.ParseException; import com.google.wireless.gdata.parser.xml.XmlGDataParser; @@ -26,27 +24,22 @@ import com.google.wireless.gdata.parser.xml.XmlParserFactory; import com.google.wireless.gdata.serializer.GDataSerializer; import com.google.wireless.gdata.serializer.xml.XmlEntryGDataSerializer; -import android.util.Log; +import java.io.InputStream; import org.xmlpull.v1.XmlPullParserException; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; - /** * Factory of Xml parsers for gdata maps data. */ -public class XmlMapsGDataParserFactory implements GDataParserFactory { +public class XmlDocsGDataParserFactory implements GDataParserFactory { private XmlParserFactory xmlFactory; - public XmlMapsGDataParserFactory(XmlParserFactory xmlFactory) { + public XmlDocsGDataParserFactory(XmlParserFactory xmlFactory) { this.xmlFactory = xmlFactory; } @Override public GDataParser createParser(InputStream is) throws ParseException { - is = maybeLogCommunication(is); try { return new XmlGDataParser(is, xmlFactory.createParser()); } catch (XmlPullParserException e) { @@ -59,7 +52,6 @@ public class XmlMapsGDataParserFactory implements GDataParserFactory { @Override public GDataParser createParser(Class cls, InputStream is) throws ParseException { - is = maybeLogCommunication(is); try { return createParserForClass(cls, is); } catch (XmlPullParserException e) { @@ -68,43 +60,14 @@ public class XmlMapsGDataParserFactory implements GDataParserFactory { } } - private InputStream maybeLogCommunication(InputStream is) - throws ParseException { - if (MapsClient.LOG_COMMUNICATION) { - StringBuilder builder = new StringBuilder(); - byte[] buffer = new byte[2048]; - try { - for (int n = is.read(buffer); n >= 0; n = is.read(buffer)) { - String part = new String(buffer, 0, n); - builder.append(part); - Log.d("Response part", part); - } - } catch (IOException e) { - throw new ParseException("Could not read stream", e); - } - String whole = builder.toString(); - Log.d("Response", whole); - is = new ByteArrayInputStream(whole.getBytes()); - } - return is; - } - private GDataParser createParserForClass( Class cls, InputStream is) throws ParseException, XmlPullParserException { - if (cls == MapFeatureEntry.class) { - return new XmlMapsGDataParser(is, xmlFactory.createParser()); - } else { - return new XmlGDataParser(is, xmlFactory.createParser()); - } + return new XmlGDataParser(is, xmlFactory.createParser()); } @Override public GDataSerializer createSerializer(Entry en) { - if (en.getClass() == MapFeatureEntry.class) { - return new XmlMapsGDataSerializer(xmlFactory, en); - } else { - return new XmlEntryGDataSerializer(xmlFactory, en); - } + return new XmlEntryGDataSerializer(xmlFactory, en); } -} +} \ No newline at end of file diff --git a/MyTracks/src/com/google/wireless/gdata/maps/MapFeatureEntry.java b/MyTracks/src/com/google/wireless/gdata/maps/MapFeatureEntry.java deleted file mode 100644 index 4114f86d1..000000000 --- a/MyTracks/src/com/google/wireless/gdata/maps/MapFeatureEntry.java +++ /dev/null @@ -1,50 +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.wireless.gdata.maps; - -import com.google.wireless.gdata.data.Entry; - -import java.util.HashMap; -import java.util.Map; - -/** - * GData entry for a map feature. - */ -public class MapFeatureEntry extends Entry { - - private String mPrivacy = null; - private Map mAttributes = new HashMap(); - - public void setPrivacy(String privacy) { - mPrivacy = privacy; - } - - public String getPrivacy() { - return mPrivacy; - } - - public void setAttribute(String name, String value) { - mAttributes.put(name, value); - } - - public void removeAttribute(String name) { - mAttributes.remove(name); - } - - public Map getAllAttributes() { - return mAttributes; - } -} diff --git a/MyTracks/src/com/google/wireless/gdata/maps/MapsClient.java b/MyTracks/src/com/google/wireless/gdata/maps/MapsClient.java deleted file mode 100644 index dcf0b45e2..000000000 --- a/MyTracks/src/com/google/wireless/gdata/maps/MapsClient.java +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright 2010 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ -package com.google.wireless.gdata.maps; - -import com.google.wireless.gdata.client.GDataClient; -import com.google.wireless.gdata.client.GDataParserFactory; -import com.google.wireless.gdata.client.GDataServiceClient; - -import android.util.Log; - -/** - * Client to talk to Google Maps via GData. - */ -public class MapsClient extends GDataServiceClient { - public static final String SERVICE = "local"; - - private static final boolean DEBUG = false; - public static final boolean LOG_COMMUNICATION = false; - - private static final String MAPS_BASE_FEED_URL = - "http://maps.google.com/maps/feeds/"; - private static final String MAPS_MAP_FEED_PATH = "maps/default/full"; - private static final String MAPS_FEATURE_FEED_PATH_BEFORE_MAPID = "features/"; - private static final String MAPS_FEATURE_FEED_PATH_AFTER_MAPID = "/full"; - private static final String MAPS_VERSION_FEED_PATH_FORMAT = - "%smaps/%s/versions/%s/full/%s"; - - private static final String MAP_ENTRY_ID_BEFORE_USER_ID = "maps/feeds/maps/"; - private static final String MAP_ENTRY_ID_BETWEEN_USER_ID_AND_MAP_ID = "/"; - private static final String V2_ONLY_PARAM = "?v=2.0"; - - public MapsClient(GDataClient dataClient, - GDataParserFactory dataParserFactory) { - super(dataClient, dataParserFactory); - } - - @Override - public String getServiceName() { - return SERVICE; - } - - public static String getMapsFeed() { - if (DEBUG) { - Log.d("Maps Client", "Requesting map feed:"); - } - return MAPS_BASE_FEED_URL + MAPS_MAP_FEED_PATH + V2_ONLY_PARAM; - } - - public static String getFeaturesFeed(String mapid) { - StringBuilder feed = new StringBuilder(); - feed.append(MAPS_BASE_FEED_URL); - feed.append(MAPS_FEATURE_FEED_PATH_BEFORE_MAPID); - feed.append(mapid); - feed.append(MAPS_FEATURE_FEED_PATH_AFTER_MAPID); - feed.append(V2_ONLY_PARAM); - return feed.toString(); - } - - public static String getMapIdFromMapEntryId(String entryId) { - String userId = null; - String mapId = null; - if (DEBUG) { - Log.d("Maps GData Client", "Getting mapid from entry id: " + entryId); - } - int userIdStart = - entryId.indexOf(MAP_ENTRY_ID_BEFORE_USER_ID) - + MAP_ENTRY_ID_BEFORE_USER_ID.length(); - int userIdEnd = - entryId.indexOf(MAP_ENTRY_ID_BETWEEN_USER_ID_AND_MAP_ID, userIdStart); - if (userIdStart >= 0 && userIdEnd < entryId.length() - && userIdStart <= userIdEnd) { - userId = entryId.substring(userIdStart, userIdEnd); - } - int mapIdStart = - entryId.indexOf(MAP_ENTRY_ID_BETWEEN_USER_ID_AND_MAP_ID, userIdEnd) - + MAP_ENTRY_ID_BETWEEN_USER_ID_AND_MAP_ID.length(); - if (mapIdStart >= 0 && mapIdStart < entryId.length()) { - mapId = entryId.substring(mapIdStart); - } - if (userId == null) { - userId = ""; - } - if (mapId == null) { - mapId = ""; - } - if (DEBUG) { - Log.d("Maps GData Client", "Got user id: " + userId); - Log.d("Maps GData Client", "Got map id: " + mapId); - } - return userId + "." + mapId; - } - - public static String getVersionFeed(String versionUserId, - String versionClient, String currentVersion) { - return String.format(MAPS_VERSION_FEED_PATH_FORMAT, - MAPS_BASE_FEED_URL, versionUserId, - versionClient, currentVersion); - } - -} diff --git a/MyTracks/src/com/google/wireless/gdata/maps/xml/XmlMapsGDataParser.java b/MyTracks/src/com/google/wireless/gdata/maps/xml/XmlMapsGDataParser.java deleted file mode 100644 index e7fe6e7e6..000000000 --- a/MyTracks/src/com/google/wireless/gdata/maps/xml/XmlMapsGDataParser.java +++ /dev/null @@ -1,178 +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.wireless.gdata.maps.xml; - -import com.google.wireless.gdata.data.Entry; -import com.google.wireless.gdata.data.Feed; -import com.google.wireless.gdata.data.XmlUtils; -import com.google.wireless.gdata.maps.MapFeatureEntry; -import com.google.wireless.gdata.parser.ParseException; -import com.google.wireless.gdata.parser.xml.XmlGDataParser; - -import org.xmlpull.v1.XmlPullParser; -import org.xmlpull.v1.XmlPullParserException; - -import java.io.IOException; -import java.io.InputStream; - -/** - * Parser for XML gdata maps data. - */ -public class XmlMapsGDataParser extends XmlGDataParser { - - public XmlMapsGDataParser(InputStream is, XmlPullParser xpp) - throws ParseException { - super(is, xpp); - } - - @Override - protected Feed createFeed() { - return new Feed(); - } - - @Override - protected Entry createEntry() { - return new MapFeatureEntry(); - } - - @Override - protected void handleExtraElementInFeed(Feed feed) { - // Do nothing - } - - @Override - protected void handleExtraLinkInEntry( - String rel, String type, String href, Entry entry) - throws XmlPullParserException, IOException { - if (!(entry instanceof MapFeatureEntry)) { - throw new IllegalArgumentException("Expected MapFeatureEntry!"); - } - if (rel.endsWith("#view")) { - return; - } - super.handleExtraLinkInEntry(rel, type, href, entry); - } - - /** - * Parses the current entry in the XML document. Assumes that the parser is - * currently pointing just after an <entry>. - * - * @param plainEntry The entry that will be filled. - * @throws XmlPullParserException Thrown if the XML cannot be parsed. - * @throws IOException Thrown if the underlying inputstream cannot be read. - */ - @Override - protected void handleEntry(Entry plainEntry) - throws XmlPullParserException, IOException, ParseException { - XmlPullParser parser = getParser(); - if (!(plainEntry instanceof MapFeatureEntry)) { - throw new IllegalArgumentException("Expected MapFeatureEntry!"); - } - MapFeatureEntry entry = (MapFeatureEntry) plainEntry; - - int eventType = parser.getEventType(); - entry.setPrivacy("public"); - while (eventType != XmlPullParser.END_DOCUMENT) { - switch (eventType) { - case XmlPullParser.START_TAG: - String name = parser.getName(); - if ("entry".equals(name)) { - // stop parsing here. - return; - } else if ("id".equals(name)) { - entry.setId(XmlUtils.extractChildText(parser)); - } else if ("title".equals(name)) { - entry.setTitle(XmlUtils.extractChildText(parser)); - } else if ("link".equals(name)) { - String rel = parser.getAttributeValue(null /* ns */, "rel"); - String type = parser.getAttributeValue(null /* ns */, "type"); - String href = parser.getAttributeValue(null /* ns */, "href"); - if ("edit".equals(rel)) { - entry.setEditUri(href); - } else if ("alternate".equals(rel) && "text/html".equals(type)) { - entry.setHtmlUri(href); - } else { - handleExtraLinkInEntry(rel, type, href, entry); - } - } else if ("summary".equals(name)) { - entry.setSummary(XmlUtils.extractChildText(parser)); - } else if ("content".equals(name)) { - StringBuilder contentBuilder = new StringBuilder(); - int parentDepth = parser.getDepth(); - while (parser.getEventType() != XmlPullParser.END_DOCUMENT) { - int etype = parser.next(); - switch (etype) { - case XmlPullParser.START_TAG: - contentBuilder.append('<'); - contentBuilder.append(parser.getName()); - contentBuilder.append('>'); - break; - case XmlPullParser.TEXT: - contentBuilder.append(""); - break; - case XmlPullParser.END_TAG: - if (parser.getDepth() > parentDepth) { - contentBuilder.append("'); - } - break; - } - if (etype == XmlPullParser.END_TAG - && parser.getDepth() == parentDepth) { - break; - } - } - entry.setContent(contentBuilder.toString()); - } else if ("category".equals(name)) { - String category = parser.getAttributeValue(null /* ns */, "term"); - if (category != null && category.length() > 0) { - entry.setCategory(category); - } - String categoryScheme = - parser.getAttributeValue(null /* ns */, "scheme"); - if (categoryScheme != null && category.length() > 0) { - entry.setCategoryScheme(categoryScheme); - } - } else if ("published".equals(name)) { - entry.setPublicationDate(XmlUtils.extractChildText(parser)); - } else if ("updated".equals(name)) { - entry.setUpdateDate(XmlUtils.extractChildText(parser)); - } else if ("deleted".equals(name)) { - entry.setDeleted(true); - } else if ("draft".equals(name)) { - String draft = XmlUtils.extractChildText(parser); - entry.setPrivacy("yes".equals(draft) ? "unlisted" : "public"); - } else if ("customProperty".equals(name)) { - String attrName = parser.getAttributeValue(null, "name"); - String attrValue = XmlUtils.extractChildText(parser); - entry.setAttribute(attrName, attrValue); - } else if ("deleted".equals(name)) { - entry.setDeleted(true); - } else { - handleExtraElementInEntry(entry); - } - break; - default: - break; - } - - eventType = parser.next(); - } - } -} diff --git a/MyTracks/src/com/google/wireless/gdata/maps/xml/XmlMapsGDataSerializer.java b/MyTracks/src/com/google/wireless/gdata/maps/xml/XmlMapsGDataSerializer.java deleted file mode 100644 index 7001a0f6b..000000000 --- a/MyTracks/src/com/google/wireless/gdata/maps/xml/XmlMapsGDataSerializer.java +++ /dev/null @@ -1,277 +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.wireless.gdata.maps.xml; - -import com.google.wireless.gdata.data.Entry; -import com.google.wireless.gdata.data.StringUtils; -import com.google.wireless.gdata.maps.MapFeatureEntry; -import com.google.wireless.gdata.maps.MapsClient; -import com.google.wireless.gdata.parser.ParseException; -import com.google.wireless.gdata.parser.xml.XmlGDataParser; -import com.google.wireless.gdata.parser.xml.XmlParserFactory; -import com.google.wireless.gdata.serializer.xml.XmlEntryGDataSerializer; - -import android.util.Log; - -import org.xmlpull.v1.XmlPullParserException; -import org.xmlpull.v1.XmlSerializer; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.util.Map; - -/** - * Serializer of maps data for GData. - */ -public class XmlMapsGDataSerializer extends XmlEntryGDataSerializer { - - private static final String APP_NAMESPACE = "http://www.w3.org/2007/app"; - - private MapFeatureEntry entry; - private XmlParserFactory factory; - private OutputStream stream; - - public XmlMapsGDataSerializer(XmlParserFactory factory, Entry entry) { - super(factory, entry); - - this.factory = factory; - this.entry = (MapFeatureEntry) entry; - } - - @Override - public void serialize(OutputStream out, int format) - throws IOException, ParseException { - XmlSerializer serializer = null; - try { - serializer = factory.createSerializer(); - } catch (XmlPullParserException e) { - throw new ParseException("Unable to create XmlSerializer.", e); - } - - // TODO: make the output compact - - ByteArrayOutputStream printStream; - - if (MapsClient.LOG_COMMUNICATION) { - printStream = new ByteArrayOutputStream(); - - serializer.setOutput(printStream, "UTF-8"); - } else { - serializer.setOutput(out, "UTF-8"); - } - - serializer.startDocument("UTF-8", Boolean.FALSE); - - declareEntryNamespaces(serializer); - serializer.startTag(XmlGDataParser.NAMESPACE_ATOM_URI, "entry"); - - stream = MapsClient.LOG_COMMUNICATION ? printStream : out; - serializeEntryContents(serializer, format); - - serializer.endTag(XmlGDataParser.NAMESPACE_ATOM_URI, "entry"); - serializer.endDocument(); - serializer.flush(); - - if (MapsClient.LOG_COMMUNICATION) { - Log.d("Request", printStream.toString()); - out.write(printStream.toByteArray()); - stream = out; - } - } - - private final void declareEntryNamespaces(XmlSerializer serializer) - throws IOException { - serializer.setPrefix( - "" /* default ns */, XmlGDataParser.NAMESPACE_ATOM_URI); - serializer.setPrefix( - XmlGDataParser.NAMESPACE_GD, XmlGDataParser.NAMESPACE_GD_URI); - declareExtraEntryNamespaces(serializer); - } - - private final void serializeEntryContents(XmlSerializer serializer, - int format) throws IOException { - if (format != FORMAT_CREATE) { - serializeId(serializer, entry.getId()); - } - - serializeTitle(serializer, entry.getTitle()); - - if (format != FORMAT_CREATE) { - serializeLink(serializer, - "edit" /* rel */, entry.getEditUri(), null /* type */); - serializeLink(serializer, - "alternate" /* rel */, entry.getHtmlUri(), "text/html" /* type */); - } - - serializeSummary(serializer, entry.getSummary()); - serializeContent(serializer, entry.getContent()); - serializeAuthor(serializer, entry.getAuthor(), entry.getEmail()); - serializeCategory(serializer, - entry.getCategory(), entry.getCategoryScheme()); - - if (format == FORMAT_FULL) { - serializePublicationDate(serializer, entry.getPublicationDate()); - } - - if (format != FORMAT_CREATE) { - serializeUpdateDate(serializer, entry.getUpdateDate()); - } - - serializeExtraEntryContents(serializer, format); - } - - private static void serializeId(XmlSerializer serializer, String id) - throws IOException { - if (StringUtils.isEmpty(id)) { - return; - } - serializer.startTag(null /* ns */, "id"); - serializer.text(id); - serializer.endTag(null /* ns */, "id"); - } - - private static void serializeTitle(XmlSerializer serializer, String title) - throws IOException { - if (StringUtils.isEmpty(title)) { - return; - } - serializer.startTag(null /* ns */, "title"); - serializer.text(title); - serializer.endTag(null /* ns */, "title"); - } - - public static void serializeLink(XmlSerializer serializer, String rel, - String href, String type) throws IOException { - if (StringUtils.isEmpty(href)) { - return; - } - serializer.startTag(null /* ns */, "link"); - serializer.attribute(null /* ns */, "rel", rel); - serializer.attribute(null /* ns */, "href", href); - if (!StringUtils.isEmpty(type)) { - serializer.attribute(null /* ns */, "type", type); - } - serializer.endTag(null /* ns */, "link"); - } - - private static void serializeSummary(XmlSerializer serializer, String summary) - throws IOException { - if (StringUtils.isEmpty(summary)) { - return; - } - serializer.startTag(null /* ns */, "summary"); - serializer.text(summary); - serializer.endTag(null /* ns */, "summary"); - } - - private void serializeContent(XmlSerializer serializer, String content) - throws IOException { - if (content == null) { - return; - } - serializer.startTag(null /* ns */, "content"); - if (content.contains("")) { - serializer.attribute( - null /* ns */, "type", "application/vnd.google-earth.kml+xml"); - serializer.flush(); - stream.write(content.getBytes()); - } else { - serializer.text(content); - } - serializer.endTag(null /* ns */, "content"); - } - - private static void serializeAuthor(XmlSerializer serializer, String author, - String email) throws IOException { - if (StringUtils.isEmpty(author) || StringUtils.isEmpty(email)) { - return; - } - serializer.startTag(null /* ns */, "author"); - serializer.startTag(null /* ns */, "name"); - serializer.text(author); - serializer.endTag(null /* ns */, "name"); - serializer.startTag(null /* ns */, "email"); - serializer.text(email); - serializer.endTag(null /* ns */, "email"); - serializer.endTag(null /* ns */, "author"); - } - - private static void serializeCategory(XmlSerializer serializer, - String category, String categoryScheme) throws IOException { - if (StringUtils.isEmpty(category) && StringUtils.isEmpty(categoryScheme)) { - return; - } - serializer.startTag(null /* ns */, "category"); - if (!StringUtils.isEmpty(category)) { - serializer.attribute(null /* ns */, "term", category); - } - if (!StringUtils.isEmpty(categoryScheme)) { - serializer.attribute(null /* ns */, "scheme", categoryScheme); - } - serializer.endTag(null /* ns */, "category"); - } - - private static void serializePublicationDate(XmlSerializer serializer, - String publicationDate) throws IOException { - if (StringUtils.isEmpty(publicationDate)) { - return; - } - serializer.startTag(null /* ns */, "published"); - serializer.text(publicationDate); - serializer.endTag(null /* ns */, "published"); - } - - private static void serializeUpdateDate(XmlSerializer serializer, - String updateDate) throws IOException { - if (StringUtils.isEmpty(updateDate)) { - return; - } - serializer.startTag(null /* ns */, "updated"); - serializer.text(updateDate); - serializer.endTag(null /* ns */, "updated"); - } - - @Override - protected void serializeExtraEntryContents(XmlSerializer serializer, - int format) throws IOException { - Map attrs = entry.getAllAttributes(); - for (Map.Entry attr : attrs.entrySet()) { - serializer.startTag("http://schemas.google.com/g/2005", "customProperty"); - serializer.attribute(null, "name", attr.getKey()); - serializer.text(attr.getValue()); - serializer.endTag("http://schemas.google.com/g/2005", "customProperty"); - } - String privacy = entry.getPrivacy(); - if (!StringUtils.isEmpty(privacy)) { - serializer.setPrefix("app", APP_NAMESPACE); - if ("public".equals(privacy)) { - serializer.startTag(APP_NAMESPACE, "control"); - serializer.startTag(APP_NAMESPACE, "draft"); - serializer.text("no"); - serializer.endTag(APP_NAMESPACE, "draft"); - serializer.endTag(APP_NAMESPACE, "control"); - } - if ("unlisted".equals(privacy)) { - serializer.startTag(APP_NAMESPACE, "control"); - serializer.startTag(APP_NAMESPACE, "draft"); - serializer.text("yes"); - serializer.endTag(APP_NAMESPACE, "draft"); - serializer.endTag(APP_NAMESPACE, "control"); - } - } - } -}