diff --git a/.hgignore b/.hgignore index b82e105b7..8372fdde3 100644 --- a/.hgignore +++ b/.hgignore @@ -2,3 +2,4 @@ syntax:glob */bin */gen/** +.metadata diff --git a/MyTracks/.settings/org.eclipse.jdt.core.prefs b/MyTracks/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 000000000..dec15e80b --- /dev/null +++ b/MyTracks/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,12 @@ +#Thu Aug 12 18:54:52 BRT 2010 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.compliance=1.6 +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.6 diff --git a/MyTracks/res/values-ja/arrays.xml b/MyTracks/res/values-ja/arrays.xml new file mode 100644 index 000000000..3b469da0b --- /dev/null +++ b/MyTracks/res/values-ja/arrays.xml @@ -0,0 +1,282 @@ + + + + + + 不使用 + 1 分 + 2 分 + 5 分 + 10 分 + 15 分 + 30 分 + 60 分 + + + + + 1 m + 2 m + 3 m + 4 m + 5 m (推奨) + 10 m + 100 m + + + + 3 ft + 6 ft + 10 ft + 13 ft + 16 ft (推奨) + 33 ft + 328 ft + + + + + + 50 m + 100 m + 200 m (推奨) + 500 m + 1000 m + 5000 m + + + + 164 ft + 328 ft + 656 ft (推奨) + 1640 ft + 0.6 mi + 3.1 mi + + + + + + 電源節約 (動的制御) + 高精度 (動的制御) + 最高 (推奨) + 2 秒 + 3 秒 + 4 秒 + 5 秒 + 10 秒 + 20 秒 + 30 秒 + 1 分 + 2 分 + 5 分 + 10 分 + 15 分 + 30 分 + + + + + + 10 m (GPS信号強度良好) + 20 m + 50 m + 100 m + 200 m (推奨) + 500 m + 1000 m + 2000 m + 5000 m (GPS信号強度不良) + + + + 33 ft (GPS信号強度良好) + 66 ft + 165 ft + 328 ft + 656 ft (推奨) + 1640 ft + 0.6 mi + 1.2 mi + 3.1 mi (GPS信号強度不良) + + + + + + 分割なし + 100 km + 50 km + 25 km + 10 km + 5 km + 1 km + 60 分 + 30 分 + 15 分 + 10 分 + 5 分 + 2 分 + 1 分 + + + + 分割なし + 100 mi + 50 mi + 25 mi + 10 mi + 5 mi + 1 mi + 60 分 + 30 分 + 15 分 + 10 分 + 5 分 + 2 分 + 1 分 + + + + + + 不明 + 混合タイプ + その他 + + 実行中 + 市街地ランニング + トラックランニング + 山道ランニング + 徒歩 + 早歩き + ハイキング + 山道ハイキング + 原野ハイキング + 登山 + + サイクリング + ロードバイク + マウンテンバイク + トラック自転車 + + スケート + インラインスケート + スケートボード + + スノーシュー + スキー + スノーボード + ソリ + アイスセーリング + + 帆走 + パドリング + カヤック + 漕艇 + モーターボート + フェリー + ラジコンボート + サーフィン + ウィンドサーフィン + カイトサーフィン + 水上スキー + ウェークボード + + 飛行機 + 水上機 + 商用航空機 + ヘリコプター + 熱気球 + 飛行船 + ラジコン飛行機 + ラジコンヘリコプター + スカイダイビング + グライダー + ハンググライダー + パラグライダー + + 騎乗 + 乗馬 + ロバ騎乗 + + 自動車運転 + バス運転 + 鉄道 + オートバイ + オフロードバイク + 四輪バギー + + ランドセーリング + フリスビー + アルティメットフリスビー + + + + ATM + 銀行 + 飲み屋 + バーベキュー + 建物 + バス停 + カフェ + キャンプファイアー + キャンプ場 + 教会 + コーヒーハウス + コンビニ + 水飲み場 + がけ崩れ + フェリー + 火事場 + 消防署 + 釣り場 + ガソリンスタンド + ゴルフ場 + 雑貨店 + 美容室 + 散髪屋 + ハイキング道 + ホテル + 家 + 情報 + 交差 + メールボックス + 市場 + 集合場所 + 歌劇場 + 駐車場 + 郵便局 + 洞穴 + 警察署 + 踏切 + レストラン + ショッピング + 名所 + 店 + プール + 電話 + 劇場 + トイレ + 観光案内 + トンネル + 樹木 + 滝 + 噴水 + ウェイポイント + + diff --git a/MyTracks/res/values-ja/strings.xml b/MyTracks/res/values-ja/strings.xml new file mode 100644 index 000000000..447a48540 --- /dev/null +++ b/MyTracks/res/values-ja/strings.xml @@ -0,0 +1,395 @@ + + + + + トラック記録 + 記録停止 + 衛星モード + 地図モード + 現在地 + トラック + マーカ + 全トラック消去 + 設定 + 標高 + 表示するトラックを選択して下さい + 現在のトラック + 最終セグメント + + + この端末でロケーションマネージャを発見できませんでした + この端末でGPSロケーションプロバイダを発見できました + Googleマイマップへのトラック送信でエラーが発生しました.しばらく待って試して下さい. + Google Docsへのトラック送信でエラーが発生しました.しばらく待って試して下さい. + 記録作成開始できません.もう一度試して下さい. + 記録停止できません.もう一度試して下さい. + ファイルからの読み込みができません. + ファイルは正しいGPX書式ではありませんでした. + エラーが発生しました.もう一度試して下さい. + トラックが存在しません. + 大きなファイルです(500KB未満でなければなりません).終了します. + マーカの挿入ができません.位置決定ができません. + + + GPS位置精度がよくなるまで待機しています.空が見えるところにいて下さい. + 新しいトラックを記録中です. + トラック記録を停止しました. + 新しいGoogleマイマップを maps.google.com に作成しました. + トラックを maps.google.com に送信しました. + トラックを docs.google.com の新しいスプレッドシートに送信しました. + トラックを docs.google.com に送信しました. + 新しいトラックが見えるようになるまで数分程度かかります. + ここをタップして,セキュリティ &位置情報 を開いて,「GPS機能をONにする」にチェックして下さい.バックボタンで戻ってください. + ご注意: 携帯電話中継局とWi-Fiネットワークから大まかな位置を特定します. + 現在位置決定ができません.位置取得源を増やし(セキュリティ&位置情報をご覧ください),空が見えるようにして下さい.もう一度試して下さい. + ウェイポイントマーカを挿入しました. + 統計マーカを挿入しました. + + + お待ちください... + トラックをGoogleに送信しました. + トラックを読んでいます... + トラックを準備しています... + Googleマップの認証しています... + Google Docsへの認証を行っています... + トラックをGoogle Docsに送信しています... + トラックをGoogleマップに送信しています... + 新しいマイマップを生成しています... + GPSトラックをインポートしています.若干時間がかかります. + SDカードにデータを書き込んでいます.若干時間がかかります. + トラック統計情報を更新しています... + + + よろしいですか? + 全てのトラックとマーカをこの端末から削除しました. + トラックとマーカをこの端末から完全に削除しました. + マーカをこの端末から完全に削除しました. + + + Android用My Tracksで作成 + Android用<a href=\'http://mytracks.appspot.com\'>My Tracks</a>で作成しました. + 新規マップ + + + ポイント + 記録 + 活動タイプ + マーカタイプ + (Part %d) + + + トラックを保存中... + トラック %d + + + いいえ + はい + OK + エラー + 成功 + ウェイポイント + + + (開始) + (終了) + + + 表示 + Google My Mapsへ送信 + 新規Googleマイマップへ送信 + Google Docsへ送信 + Googleへ送信... + 端末から削除... + 詳細編集 + SDへの書き込み... + GPXファイルの保存 + KMLファイルの保存 + CVSファイルの保存 + Googleマイマップへの送信 + 全てのトラックの削除 + + 全てのトラックのエクスポート + エクスポート終了 + トラックを選択します.オプションを表示するには長押しして下さい. + 全てのトラック: + このトラック + 共有... + Googleマイマップへのリンク + GPXファイル + KMLファイル + CSVファイル + マップの消去 + + + 全てのマーカの削除 + このマーカ + 削除 + 詳細編集 + マップに表示 + ウェイポイントの挿入 + 統計情報の挿入 + マーカを選択します.オプションを表示するには長押しして下さい. + + + トラック詳細 + トラック名 + トラック説明 + 活動タイプ(ハイキング, サイクリング, ランニング, 登山...) + + + マーカ詳細 + マーカ名 + マーカ説明 + マーカタイプ(建物, 洞穴, 名所...) + + + My Tracks設定 + 表示 + 記録 + 共有 + + 定期通知 + 音声通知間隔の最小時間 + お使いのAndroidの版では音声読み上げ機能がありません + + ポイント間の最短距離 + 位置記録間の最短距離 + + ポイント間の最長距離 + これより間隔が長い場合はトラックは分割されます + + ポイント間の最小時間間隔 + 位置記録の最短インターバル + + 最低精度 + この精度以上でないと記録を休止します + + メートル法を使用 + 全ての距離をメートル法で表示します + 全ての距離をヤード・ポンド法で表示します + + マップの公開 + アップロードされたマップはmaps.google.comで誰でも閲覧できます + アップロードされたマップはmaps.google.comのリストに公開されません. + + 速度表示 + 速度が表示されます + ペースが表示されます + + URLのみ共有 + 共有時にマップのURLのみ送信します + 共有時にトラックのコンテントが含まれます + + 信号サンプリング周波数 + 端末の信号強度記録の時間間隔 + + 分割頻度 + 分割マーカを挿入する時間または距離の間隔 + ペース + + + GPS衛星 + + + ご使用のアカウントからのマップ選択: + 新規マップ... + Googleマイマップへの送信 + マップの読み込み中... + マップを読み込めませんでした! + このマップを選択 + マップ詳細の編集 + マップの共有 + トラックの共有 + アカウントからマップの削除 + マップタイトル: + マップ説明: + + + このマップとコンテンツを検索結果に表示させる + このマップとコンテンツはどちらも検索結果に表示させない(直接URLで他の方が見ることは可能) + このマップとコンテンツは検索結果に表示されます + このマップとコンテンツはどちらも検索結果に表示されませんが,直接リンクで他の方が見ることができます. + 閉じる + 編集 + キャンセル + 保存 + + + マップを共有したいと思います + このマップに興味を持って頂けると思います: + + %s + トラックを共有したいと思います + このトラックに興味を持って頂けると思います. + + このトラックを閲覧するには,Google Earthまたは他のマッピングソフトウェアが必要です. + + + 公開 + リストに出さない + まだ地図がありません. + + + + 平均移動速度 + 平均移動ペース + + + 平均速度 + 平均ペース + + 標高チャート + 標高差 + 標高 + 緯度 + 経度 + + + 最小標高 + 最小勾配 + 最小ペース + + + 最大標高 + 最大勾配 + 最大速度 + + トラックの読み込み中... + + + チャート設定 + + X軸 + 距離ベース + 時間ベース + + + Y軸 + + + + 時 + 時 + 時 + 分 + 分 + 分 + 秒 + 秒 + + + m + km + キロメータ + ft + mi + マイル + + + km/h + キロメータ毎時 + min/km + 毎キロメータ + mi/h + マイル毎時 + min/mi + 毎マイル + + + 移動時間 + 合計時間 + 時間 + SD が発見できません + 速度 + 移動して下さい. + 統計 + 合計距離 + 距離 + - + 待機中... + + + エンドユーザライセンス契約書 + 同意する + 拒否する + + + このアプリケーションについて + ヘルプ + チーム: + バージョン: + サポート + サポート: + + + 送信... + Googleマイマップ + Google Docs + 今すぐ送信 + 新規マップ作成 + 既存マップ選択 + トラック統計情報送信 + 統計情報とポイント送信 + + + アカウントを発見できませんでした. + トラックをアップロードするためのアカウントを発見できませんでした. + アカウント選択 + + + 外部ストレージを発見できませんでした.SDカードが刺さっているか,端末がコンピュータにマウントされているか確認してください. + ファイルへの書き込みができません. + SDカードにディレクトリを作成できません. + SDカードへの書き込み終了. + + + アップグレードチェック + 今すぐアップデート + アップグレードせずに続行 + お使いの版では,確実には動作せず,サポートもされません.ご使用を続けるには %1$s のアップグレードしてください. + お使いの版では,確実には動作しません.すぐにアップグレードすることをお勧めします.アップグレードされない場合は問題発生の可能性があることをご認識ください. サーバ同期するような機能の中には,アップグレードされるまで機能しないものがあります. + %1$s の新しい版が出ています.現版で問題なくご使用になれますが,アップグレードするまで最新の機能をお使いになれません. + + + ようこそ + Ok + My Tracksについて + + + My Tracks 移動していません + + My Tracks %1$s %2$.2f %3$s 時間 %4$s 速度/ペース %5$s %6$s + + + 計測単位についてのご注意 + デフォルトの単位はメートルです.設定を変更しますか? + + + 誤り率: %1$d + S/N比: %1$d + Ec/Io比: %1$.1f dB + GSM 強度: %1$d + CDMA 強度: %1$d dBm + EVDO 強度: %1$d dBm + diff --git a/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java b/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java index 142c74de4..27fda84eb 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java +++ b/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java @@ -24,7 +24,7 @@ import com.google.android.apps.mytracks.content.Track; import com.google.android.apps.mytracks.content.Waypoint; import com.google.android.apps.mytracks.io.AuthManager; import com.google.android.apps.mytracks.io.AuthManagerFactory; -import com.google.android.apps.mytracks.io.GpxImport; +import com.google.android.apps.mytracks.io.GpxImporter; import com.google.android.apps.mytracks.io.SendToDocs; import com.google.android.apps.mytracks.io.SendToMyMaps; import com.google.android.apps.mytracks.io.TrackWriter; @@ -62,24 +62,25 @@ 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.view.WindowManager.BadTokenException; import android.widget.RelativeLayout; import android.widget.TabHost; import android.widget.Toast; -import org.xml.sax.SAXException; - import java.io.File; +import java.io.FileInputStream; import java.io.IOException; -import java.util.ArrayList; +import java.io.InputStream; import java.util.HashMap; import java.util.Random; import javax.xml.parsers.ParserConfigurationException; +import org.xml.sax.SAXException; + /** * The super activity that embeds our sub activities. * @@ -895,13 +896,14 @@ public class MyTracks extends TabActivity implements OnTouchListener, Thread t = new Thread() { @Override public void run() { - boolean success = false; - ArrayList tracks = new ArrayList(); int message = R.string.success; + + long[] trackIdsImported = null; + try { try { - GpxImport.importGPXFile(fileName, tracks); - success = true; + InputStream is = new FileInputStream(fileName); + trackIdsImported = GpxImporter.importGPXFile(is, providerUtils); } catch (SAXException e) { Log.e(MyTracksConstants.TAG, "Caught an unexpected exception.", e); message = R.string.error_generic; @@ -918,15 +920,11 @@ public class MyTracks extends TabActivity implements OnTouchListener, Log.e(MyTracksConstants.TAG, "Caught an unexpected exception.", e); message = R.string.error_out_of_memory; } - if (success) { - long trackId = -1; - for (Track track : tracks) { - Uri uri = providerUtils.insertTrackAndTrackPoints(track); - trackId = Long.parseLong(uri.getLastPathSegment()); - } - setSelectedTrack(trackId); + if (trackIdsImported != null && trackIdsImported.length > 0) { + // select last track from import file + setSelectedTrack(trackIdsImported[trackIdsImported.length - 1]); } else { - MyTracks.this.showMessageDialog(message, false/*success*/); + MyTracks.this.showMessageDialog(message, false/* success */); } } finally { runOnUiThread(new Runnable() { diff --git a/MyTracks/src/com/google/android/apps/mytracks/content/MyTracksProvider.java b/MyTracks/src/com/google/android/apps/mytracks/content/MyTracksProvider.java index a4b542e25..010e553f8 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/content/MyTracksProvider.java +++ b/MyTracks/src/com/google/android/apps/mytracks/content/MyTracksProvider.java @@ -167,24 +167,25 @@ public class MyTracksProvider extends ContentProvider { @Override public int delete(Uri url, String where, String[] selectionArgs) { - if (urlMatcher.match(url) == TRACKPOINTS) { - Log.w(MyTracksProvider.TAG, "provider trackpoints delete!"); - int count = db.delete(TRACKPOINTS_TABLE, where, selectionArgs); - getContext().getContentResolver().notifyChange(url, null, true); - return count; - } else if (urlMatcher.match(url) == TRACKS) { - Log.w(MyTracksProvider.TAG, "provider track delete!"); - int count = db.delete(TRACKS_TABLE, where, selectionArgs); - getContext().getContentResolver().notifyChange(url, null, true); - return count; - } else if (urlMatcher.match(url) == WAYPOINTS) { - Log.w(MyTracksProvider.TAG, "provider waypoint delete!"); - int count = db.delete(WAYPOINTS_TABLE, where, selectionArgs); - getContext().getContentResolver().notifyChange(url, null, true); - return count; - } else { - throw new IllegalArgumentException("Unknown URL " + url); + String table; + switch (urlMatcher.match(url)) { + case TRACKPOINTS: + table = TRACKPOINTS_TABLE; + break; + case TRACKS: + table = TRACKS_TABLE; + break; + case WAYPOINTS: + table = WAYPOINTS_TABLE; + break; + default: + throw new IllegalArgumentException("Unknown URL " + url); } + + Log.w(MyTracksProvider.TAG, "provider delete in " + table + "!"); + int count = db.delete(table, where, selectionArgs); + getContext().getContentResolver().notifyChange(url, null, true); + return count; } @Override @@ -216,19 +217,49 @@ public class MyTracksProvider extends ContentProvider { } else { values = new ContentValues(); } - if (urlMatcher.match(url) == TRACKPOINTS) { - return insertTrackPoint(url, values); - } else if (urlMatcher.match(url) == TRACKS) { - return insertTrack(url, values); - } else if (urlMatcher.match(url) == WAYPOINTS) { - return insertWaypoint(url, values); - } else { - throw new IllegalArgumentException("Unknown URL " + url); + + int urlMatchType = urlMatcher.match(url); + return insertType(url, urlMatchType, values); + } + + private Uri insertType(Uri url, int urlMatchType, ContentValues values) { + switch (urlMatchType) { + case TRACKPOINTS: + return insertTrackPoint(url, values); + case TRACKS: + return insertTrack(url, values); + case WAYPOINTS: + return insertWaypoint(url, values); + default: + throw new IllegalArgumentException("Unknown URL " + url); } } + @Override + public int bulkInsert(Uri url, ContentValues[] valuesBulk) { + Log.d(MyTracksProvider.TAG, "MyTracksProvider.bulkInsert"); + int numInserted = 0; + try { + // Use a transaction in order to make the insertions run as a single batch + db.beginTransaction(); + + int urlMatch = urlMatcher.match(url); + for (numInserted = 0; numInserted < valuesBulk.length; numInserted++) { + ContentValues values = valuesBulk[numInserted]; + if (values == null) { values = new ContentValues(); } + + insertType(url, urlMatch, values); + } + + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + + return numInserted; + } + private Uri insertTrackPoint(Uri url, ContentValues values) { - Log.d(MyTracksProvider.TAG, "MyTracksProvider.insertTrackPoint"); boolean hasLat = values.containsKey(TrackPointsColumns.LATITUDE); boolean hasLong = values.containsKey(TrackPointsColumns.LONGITUDE); boolean hasTime = values.containsKey(TrackPointsColumns.TIME); diff --git a/MyTracks/src/com/google/android/apps/mytracks/content/MyTracksProviderUtils.java b/MyTracks/src/com/google/android/apps/mytracks/content/MyTracksProviderUtils.java index 8f73fb05d..2c5c9e60a 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/content/MyTracksProviderUtils.java +++ b/MyTracks/src/com/google/android/apps/mytracks/content/MyTracksProviderUtils.java @@ -232,6 +232,17 @@ public interface MyTracksProviderUtils { */ Uri insertTrackPoint(Location location, long trackId); + /** + * Inserts multiple track points in a single operation. + * + * @param locations an array of locations to insert + * @param length the number of locations (from the beginning of the array) + * to actually insert, or -1 for all of them + * @param trackId the ID of the track to insert the points into + * @return the number of points inserted + */ + int bulkInsertTrackPoints(Location[] locations, int length, long trackId); + /** * Inserts a waypoint in the provider. * @@ -240,14 +251,6 @@ public interface MyTracksProviderUtils { */ Uri insertWaypoint(Waypoint waypoint); - /** - * Inserts a track including its track points in the provider. - * - * @param track the track to insert - * @return the content provider URI for the inserted track - */ - Uri insertTrackAndTrackPoints(Track track); - /** * Tests if a track with given id exists. * @@ -270,7 +273,7 @@ public interface MyTracksProviderUtils { * @param cursor a cursor pointing at a db or provider with locations * @return a new location object */ - public Location createLocation(Cursor cursor); + Location createLocation(Cursor cursor); /** * Creates a waypoint object from a given cursor. @@ -278,7 +281,7 @@ public interface MyTracksProviderUtils { * @param cursor a cursor pointing at a db or provider with waypoints. * @return a new waypoint object */ - public Waypoint createWaypoint(Cursor cursor); + Waypoint createWaypoint(Cursor cursor); /** * A factory which can produce instances of {@link MyTracksProviderUtils}, diff --git a/MyTracks/src/com/google/android/apps/mytracks/content/MyTracksProviderUtilsImpl.java b/MyTracks/src/com/google/android/apps/mytracks/content/MyTracksProviderUtilsImpl.java index 64e969a5d..8b13603a6 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/content/MyTracksProviderUtilsImpl.java +++ b/MyTracks/src/com/google/android/apps/mytracks/content/MyTracksProviderUtilsImpl.java @@ -840,8 +840,6 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { Cursor cursor = getLocationsCursor(track.getId(), startingPoint, buffer.getSize(), false); - final int idColumnIdx = - cursor.getColumnIndexOrThrow(TrackPointsColumns._ID); if (cursor == null) { Log.w(MyTracksProvider.TAG, "Cannot get a locations cursor!"); buffer.setInvalid(); @@ -860,6 +858,8 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { return; } + final int idColumnIdx = + cursor.getColumnIndexOrThrow(TrackPointsColumns._ID); do { Location location = createLocation(cursor); if (location == null) { @@ -900,6 +900,18 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { createContentValues(location, trackId)); } + @Override + public int bulkInsertTrackPoints(Location[] locations, int length, long trackId) { + if (length == -1) { length = locations.length; } + + ContentValues[] values = new ContentValues[length]; + for (int i = 0; i < length; i++) { + values[i] = createContentValues(locations[i], trackId); + } + + return context.getContentResolver().bulkInsert(TrackPointsColumns.CONTENT_URI, values); + } + @Override public Uri insertWaypoint(Waypoint waypoint) { Log.d(MyTracksProvider.TAG, "MyTracksProviderUtilsImpl.insertWaypoint"); @@ -908,31 +920,6 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { createContentValues(waypoint)); } - @Override - public Uri insertTrackAndTrackPoints(Track track) { - // Insert the track right away to have its ID - Uri trackUri = insertTrack(track); - long trackId = Long.parseLong(trackUri.getLastPathSegment()); - track.setId(trackId); - - // Insert all the points (associated with the track ID) - boolean firstPoint = true; - long pointId = -1; - for (Location location : track.getLocations()) { - Uri pointUri = insertTrackPoint(location, trackId); - pointId = Long.parseLong(pointUri.getLastPathSegment()); - if (firstPoint) { - track.setStartId(pointId); - firstPoint = false; - } - } - track.setStopId(pointId); - - // Update the track with the start and end point IDs - updateTrack(track); - return trackUri; - } - @Override public boolean trackExists(long id) { Cursor cursor = null; diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/GpxImport.java b/MyTracks/src/com/google/android/apps/mytracks/io/GpxImport.java deleted file mode 100644 index 2bb46ab57..000000000 --- a/MyTracks/src/com/google/android/apps/mytracks/io/GpxImport.java +++ /dev/null @@ -1,237 +0,0 @@ -/* - * Copyright 2008 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ -package com.google.android.apps.mytracks.io; - -import com.google.android.apps.mytracks.content.Track; -import com.google.android.apps.mytracks.stats.TripStatisticsBuilder; -import com.google.android.apps.mytracks.util.MyTracksUtils; - -import android.location.Location; - -import org.w3c.dom.Document; -import org.w3c.dom.NamedNodeMap; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; -import org.xml.sax.SAXException; - -import java.io.File; -import java.io.IOException; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.ArrayList; - -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.parsers.ParserConfigurationException; - -/** - * Helper class to parse a GPX file or string and convert it into a track - * object. - * - * TODO: See if we can use a SAX style parser as the DOM style - * parsing uses too much memory and will not allow import of very large GPX - * files (limit currently set to 500KB). - * - * @author Leif Hendrik Wilden - */ -public class GpxImport { - private static final SimpleDateFormat DATE_FORMAT1 = - new SimpleDateFormat("yyyy-MM-dd'T'hh:mm:ssZ"); - private static final SimpleDateFormat DATE_FORMAT2 = - new SimpleDateFormat("yyyy-MM-dd'T'hh:mm:ss'Z'"); - private static final SimpleDateFormat DATE_FORMAT3 = - new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ"); - - /** - * Reads GPS tracks from a GPX file and append tracks and their coordinates to - * the given list of tracks. - * - * @param tracks a list of tracks - * @param filename a file name - * @throws SAXException a parsing error - * @throws ParserConfigurationException internal error - * @throws IOException a file reading problem - */ - public static void importGPXFile( - final String filename, final ArrayList tracks) - throws SAXException, ParserConfigurationException, IOException, - OutOfMemoryError { - DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); - DocumentBuilder builder = factory.newDocumentBuilder(); - File file = new File(filename); - if (file.length() > 500 * 1024) { - // Better to throw an error now then, to let the VM do it. If the VM does - // it, it will throw another exception while throwing the OutOfMemoryError - // and that will kill out app with a nasty error message. In that way we - // can at least display a reasonable message to the user and let her know - // what the problem is. - throw new OutOfMemoryError(); - } - Document doc = builder.parse(file); - importGPXDocument(tracks, doc); - } - - /** - * This is a utility class with only static members. - */ - private GpxImport() { - } - - /** - * Reads GPS tracks from a GPX document and append then to the given list of - * tracks. - * - * TODO: Break this method down into smaller ones - * - * @param tracks an array list of tracks - * @param doc a document - */ - public static void importGPXDocument( - final ArrayList tracks, final Document doc) { - NodeList trackNodes = doc.getElementsByTagName("trk"); - for (int n = 0; n < trackNodes.getLength(); n++) { - Track track = new Track(); - tracks.add(track); - ArrayList locations = track.getLocations(); - Location lastLocation = null; - - Node trkNode = trackNodes.item(n); - NodeList segmentNodes = trkNode.getChildNodes(); - ArrayList nodes = new ArrayList(); - int nSegments = 0; - for (int k = 0; k < segmentNodes.getLength(); k++) { - Node segmentNode = segmentNodes.item(k); - if (segmentNode.getNodeName().equals("name")) { - track.setName(segmentNode.getFirstChild().getNodeValue()); - } else if (segmentNode.getNodeName().equals("description")) { - track.setDescription(segmentNode.getFirstChild().getNodeValue()); - } else if (segmentNode.getNodeName().equals("trkseg")) { - if (nSegments > 0) { - // Add a segment separator: - Location location = new Location("gps"); - location.setLatitude(100.0); - location.setLongitude(100.0); - location.setAltitude(0); - if (locations.size() > 0) { - long pointTime = locations.get(locations.size() - 1).getTime(); - location.setTime(pointTime); - } - track.addLocation(location); - lastLocation = null; - } - nSegments++; - NodeList segmentChildren = segmentNode.getChildNodes(); - nodes.clear(); - for (int j = 0; j < segmentChildren.getLength(); j++) { - Node child = segmentChildren.item(j); - if (child.getNodeName().equals("trkpt")) { - nodes.add(child); - } - } - for (int i = 0; i < nodes.size(); i++) { - NamedNodeMap namedNodes = nodes.get(i).getAttributes(); - double lat = Double.parseDouble(namedNodes.getNamedItem("lat") - .getNodeValue()); - double lon = Double.parseDouble(namedNodes.getNamedItem("lon") - .getNodeValue()); - NodeList children = nodes.get(i).getChildNodes(); - Node elementNode = null; - Node timeNode = null; - for (int j = 0; j < children.getLength(); j++) { - Node child = children.item(j); - if (child.getNodeName().equals("ele")) { - elementNode = child; - } else if (child.getNodeName().equals("time")) { - timeNode = child; - } - } - String altitudeStr = null; - if (elementNode != null) { - altitudeStr = elementNode.getFirstChild().getNodeValue(); - } else { - altitudeStr = "0"; - } - String timeContents = null; - if (timeNode != null) { - timeContents = timeNode.getFirstChild().getNodeValue(); - } else { - timeContents = ""; - } - double altitude = Double.parseDouble(altitudeStr); - long t = -1; - try { - // 1st try with time zone at end a la "+0000" - t = DATE_FORMAT1.parse(timeContents).getTime(); - } catch (ParseException e) { - // if that fails, try with a literal "Z" at the end (this is not - // according to xml standard, but some gpx files are like that): - try { - t = DATE_FORMAT2.parse(timeContents).getTime(); - } catch (ParseException ex) { - // some gpx timestamps have 3 additional digits at the end. - try { - t = DATE_FORMAT3.parse(timeContents).getTime(); - } catch (ParseException exc) { - t = 0; - } - } - } - - Location location = new Location("gps"); - location.setLatitude(lat); - location.setLongitude(lon); - location.setAltitude(altitude); - location.setTime(t); - // We don't have a speed and bearing in GPX, make something up from - // the last two points: - if (lastLocation != null) { - final long dt = location.getTime() - lastLocation.getTime(); - if (dt > 0) { - final float speed = - location.distanceTo(lastLocation) / (dt / 1000); - location.setSpeed(speed); - } - location.setBearing(lastLocation.bearingTo(location)); - } - lastLocation = location; - if (MyTracksUtils.isValidLocation(location)) { - track.addLocation(location); - } - } - - if (locations.size() > 0) { - long startTime = locations.get(0).getTime(); - - // Calculate statistics for the imported track - TripStatisticsBuilder statsBuilder = new TripStatisticsBuilder(); - statsBuilder.resumeAt(startTime); - for (Location location : locations) { - if (MyTracksUtils.isValidLocation(location)) { - /* Any time works here. The totalTime will be set by "pauseAt" later: */ - statsBuilder.addLocation(location, location.getTime()); - } - } - - long lastPointTime = locations.get(locations.size() - 1).getTime(); - statsBuilder.pauseAt(lastPointTime); - - track.setStatistics(statsBuilder.getStatistics()); - } - } - } - } - } -} diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/GpxImporter.java b/MyTracks/src/com/google/android/apps/mytracks/io/GpxImporter.java new file mode 100644 index 000000000..c16678e03 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/io/GpxImporter.java @@ -0,0 +1,574 @@ +/* + * Copyright 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.google.android.apps.mytracks.io; + +import com.google.android.apps.mytracks.MyTracksConstants; +import com.google.android.apps.mytracks.content.MyTracksProviderUtils; +import com.google.android.apps.mytracks.content.Track; +import com.google.android.apps.mytracks.stats.TripStatisticsBuilder; +import com.google.android.apps.mytracks.util.MyTracksUtils; + +import android.location.Location; +import android.location.LocationManager; +import android.net.Uri; +import android.util.Log; + +import java.io.IOException; +import java.io.InputStream; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.List; +import java.util.SimpleTimeZone; + +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; + +import org.xml.sax.Attributes; +import org.xml.sax.Locator; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.DefaultHandler; + +/** + * Imports GPX XML files to the my tracks provider. + * + * TODO: Show progress indication to the user. + * + * @author Leif Hendrik Wilden + * @author Steffen Horlacher + * @author Rodrigo Damazio + */ +public class GpxImporter extends DefaultHandler { + + /* + * Different date formats used in GPX files + */ + static final SimpleDateFormat DATE_FORMAT1 = new SimpleDateFormat( + "yyyy-MM-dd'T'HH:mm:ssZ"); + static final SimpleDateFormat DATE_FORMAT2 = new SimpleDateFormat( + "yyyy-MM-dd'T'HH:mm:ss'Z'"); + static final SimpleDateFormat DATE_FORMAT3 = new SimpleDateFormat( + "yyyy-MM-dd'T'HH:mm:ss.SSSZ"); + static final SimpleTimeZone UTC_TIMEZONE = new SimpleTimeZone(0, "UTC"); + + /* + * GPX-XML tag names and attributes. + */ + private static final String TAG_TRACK = "trk"; + private static final String TAG_TRACK_POINT = "trkpt"; + private static final Object TAG_TRACK_SEGMENT = "trkseg"; + private static final String TAG_NAME = "name"; + private static final String TAG_DESCRIPTION = "desc"; + private static final String TAG_ALTITUDE = "ele"; + private static final String TAG_TIME = "time"; + private static final String ATT_LAT = "lat"; + private static final String ATT_LON = "lon"; + + /** + * The maximum number of locations to buffer for bulk-insertion into the database. + */ + private static final int MAX_BUFFERED_LOCATIONS = 512; + + /** + * Utilities for accessing the contnet provider. + */ + private final MyTracksProviderUtils providerUtils; + + /** + * List of track ids written in the database. Only contains successfully + * written tracks. + */ + private final List tracksWritten; + + /** + * Contains the current elements content. + */ + private String content; + + /** + * Currently reading location. + */ + private Location location; + + /** + * Previous location, required for calculations. + */ + private Location lastLocation; + + /** + * Currently reading track. + */ + private Track track; + + /** + * Statistics builder for the current track. + */ + private TripStatisticsBuilder statsBuilder; + + /** + * Buffer of locations to be bulk-inserted into the database. + */ + private Location[] bufferedPointInserts = new Location[MAX_BUFFERED_LOCATIONS]; + + /** + * Number of locations buffered to be inserted into the database. + */ + private int numBufferedPointInserts = 0; + + /** + * Number of locations already processed. + */ + private int numberOfLocations; + + /** + * Number of segments already processed. + */ + private int numberOfSegments; + + /** + * Used to identify if a track was written to the database but not yet + * finished successfully. + */ + private boolean isCurrentTrackRollbackable; + + /** + * Flag to indicate if we're inside a track's xml element. + * Some sub elements like name may be used in other parts of the gpx file, + * and we use this to ignore them. + */ + private boolean isInTrackElement; + + /** + * Counter to find out which child level of track we are processing. + */ + private int trackChildDepth; + + /** + * SAX-Locator to get current line information. + */ + private Locator locator; + private Location lastSegmentLocation; + + /** + * Reads GPS tracks from a GPX file and writes tracks and their coordinates to + * the database. + * + * @param tracks a list of tracks + * @param is a input steam with gpx-xml data + * @return long[] array of track ids written in the database + * @throws SAXException a parsing error + * @throws ParserConfigurationException internal error + * @throws IOException a file reading problem + */ + public static long[] importGPXFile(final InputStream is, + final MyTracksProviderUtils providerUtils) + throws ParserConfigurationException, SAXException, IOException { + SAXParserFactory factory = SAXParserFactory.newInstance(); + GpxImporter handler = new GpxImporter(providerUtils); + SAXParser parser = factory.newSAXParser(); + long[] trackIds = null; + + try { + long start = System.currentTimeMillis(); + + parser.parse(is, handler); + + long end = System.currentTimeMillis(); + Log.d(MyTracksConstants.TAG, "Total import time: " + (end - start) + "ms"); + + trackIds = handler.getImportedTrackIds(); + } finally { + // delete track if not finished + handler.rollbackUnfinishedTracks(); + } + + return trackIds; + } + + /** + * Constructor, requires providerUtils for writing tracks the database. + */ + public GpxImporter(MyTracksProviderUtils providerUtils) { + this.providerUtils = providerUtils; + tracksWritten = new ArrayList(); + } + + @Override + public void characters(char[] ch, int start, int length) throws SAXException { + String newContent = new String(ch, start, length); + if (content == null) { + content = newContent; + } else { + // In 99% of the cases, a single call to this method will be made for each + // sequence of characters we're interested in, so we'll rarely be + // concatenating strings, thus not justifying the use of a StringBuilder. + content += newContent; + } + } + + @Override + public void startElement(String uri, String localName, String name, + Attributes attributes) throws SAXException { + if (isInTrackElement) { + trackChildDepth++; + if (localName.equals(TAG_TRACK_POINT)) { + onTrackPointElementStart(attributes); + } else if (localName.equals(TAG_TRACK_SEGMENT)) { + onTrackSegmentElementStart(); + } else if (localName.equals(TAG_TRACK)) { + String msg = createErrorMessage("Invalid GPX-XML detected"); + throw new SAXException(msg); + } + } else if (localName.equals(TAG_TRACK)) { + isInTrackElement = true; + trackChildDepth = 0; + onTrackElementStart(); + } + } + + @Override + public void endElement(String uri, String localName, String name) + throws SAXException { + if (!isInTrackElement) { return; } + + // process these elements only as sub-elements of track + if (localName.equals(TAG_TRACK_POINT)) { + onTrackPointElementEnd(); + } else if (localName.equals(TAG_ALTITUDE)) { + onAltitudeElementEnd(); + } else if (localName.equals(TAG_TIME)) { + onTimeElementEnd(); + } else if (localName.equals(TAG_NAME)) { + // we are only interested in the first level name element + if (trackChildDepth == 1) { + onNameElementEnd(); + } + } else if (localName.equals(TAG_DESCRIPTION)) { + // we are only interested in the first level description element + if (trackChildDepth == 1) { + onDescriptionElementEnd(); + } + } else if (localName.equals(TAG_TRACK_SEGMENT)) { + onTrackSegmentElementEnd(); + } else if (localName.equals(TAG_TRACK)) { + onTrackElementEnd(); + isInTrackElement = false; + trackChildDepth = 0; + } + trackChildDepth--; + + // reset element content + content = null; + } + + @Override + public void setDocumentLocator(Locator locator) { + this.locator = locator; + } + + /** + * Create a new Track object and insert empty track in database. Track will be + * updated with missing values later. + */ + private void onTrackElementStart() { + track = new Track(); + numberOfLocations = 0; + + Uri trackUri = providerUtils.insertTrack(track); + long trackId = Long.parseLong(trackUri.getLastPathSegment()); + track.setId(trackId); + isCurrentTrackRollbackable = true; + } + + private void onDescriptionElementEnd() { + track.setDescription(content.toString().trim()); + } + + private void onNameElementEnd() { + track.setName(content.toString().trim()); + } + + /** + * Track segment started. + */ + private void onTrackSegmentElementStart() { + if (numberOfSegments > 0) { + // Add a segment separator: + location = new Location(LocationManager.GPS_PROVIDER); + location.setLatitude(100.0); + location.setLongitude(100.0); + location.setAltitude(0); + if (lastLocation != null) { + location.setTime(lastLocation.getTime()); + } + insertTrackPoint(location); + lastLocation = location; + lastSegmentLocation = null; + location = null; + } + + numberOfSegments++; + } + + /** + * Reads trackpoint attributes and assigns them to the current location. + * + * @param attributes xml attributes + */ + private void onTrackPointElementStart(Attributes attributes) throws SAXException { + if (location != null) { + String errorMsg = createErrorMessage("Found a track point inside another one."); + throw new SAXException(errorMsg); + } + + location = createLocationFromAttributes(attributes); + } + + /** + * Creates and returns a location with the position parsed from the given + * attributes. + * + * @param attributes the attributes to parse + * @return the created location + * @throws SAXException if the attributes cannot be parsed + */ + private Location createLocationFromAttributes(Attributes attributes) throws SAXException { + String latitude = attributes.getValue(ATT_LAT); + String longitude = attributes.getValue(ATT_LON); + + if (latitude == null || longitude == null) { + throw new SAXException(createErrorMessage("Point with no longitude or latitude")); + } + + // create new location and set attributes + Location loc = new Location(LocationManager.GPS_PROVIDER); + loc.setLatitude(Double.parseDouble(latitude)); + loc.setLongitude(Double.parseDouble(longitude)); + return loc; + } + + /** + * Track point finished, write in database. + * + * @throws SAXException - thrown if track point is invalid + */ + private void onTrackPointElementEnd() throws SAXException { + if (MyTracksUtils.isValidLocation(location)) { + statsBuilder.addLocation(location, location.getTime()); + + // insert in db + insertTrackPoint(location); + + // first track point? + if (lastLocation == null && numberOfSegments == 1) { + track.setStartId(getLastPointId()); + } + + lastLocation = location; + lastSegmentLocation = location; + location = null; + } else { + // invalid location - abort import + String msg = createErrorMessage("Invalid location detected: " + location); + throw new SAXException(msg); + } + } + + private void insertTrackPoint(Location loc) { + bufferedPointInserts[numBufferedPointInserts] = loc; + numBufferedPointInserts++; + numberOfLocations++; + + if (numBufferedPointInserts >= MAX_BUFFERED_LOCATIONS) { + flushPointInserts(); + } + } + + private void flushPointInserts() { + if (numBufferedPointInserts <= 0) { return; } + + providerUtils.bulkInsertTrackPoints(bufferedPointInserts, numBufferedPointInserts, track.getId()); + numBufferedPointInserts = 0; + } + + /** + * Track segment finished. + */ + private void onTrackSegmentElementEnd() { + // Nothing to be done + } + + /** + * Track finished - update in database. + */ + private void onTrackElementEnd() { + if (lastLocation != null) { + flushPointInserts(); + + // Calculate statistics for the imported track and update + statsBuilder.pauseAt(lastLocation.getTime()); + track.setStopId(getLastPointId()); + track.setNumberOfPoints(numberOfLocations); + track.setStatistics(statsBuilder.getStatistics()); + providerUtils.updateTrack(track); + tracksWritten.add(track.getId()); + isCurrentTrackRollbackable = false; + lastSegmentLocation = null; + lastLocation = null; + statsBuilder = null; + } else { + // track contains no track points makes no real + // sense to import it as we have no location + // information -> roll back + rollbackUnfinishedTracks(); + } + } + + /** + * Setting time and doing additional calculations as this is the last value + * required. Also sets the start time for track and statistics as there is no + * start time in the track root element. + * + * @throws SAXException on parsing errors + */ + private void onTimeElementEnd() throws SAXException { + if (location == null) { return; } + + long time = parseTimeForAllFormats(content); + if (lastSegmentLocation != null) { + long timeDifference = time - lastSegmentLocation.getTime(); + + // check for negative time change + if (timeDifference < 0) { + String msg = createErrorMessage("Found negative time change."); + throw new SAXException(msg); + } + + // We don't have a speed and bearing in GPX, make something up from + // the last two points. + // TODO GPS points tend to have some inherent imprecision, + // speed and bearing will likely be off, so the statistics for things like + // max speed will also be off. + float speed = location.distanceTo(lastLocation) * 1000.0f / timeDifference; + location.setSpeed(speed); + location.setBearing(lastSegmentLocation.bearingTo(location)); + } + + location.setTime(time); + // initialize start time with time of first track point + if (statsBuilder == null) { + statsBuilder = new TripStatisticsBuilder(); + statsBuilder.resumeAt(time); + } + } + + private void onAltitudeElementEnd() { + if (location != null) { + location.setAltitude(Double.parseDouble(content)); + } + } + + /** + * Deletes the last track if it was not completely imported. + */ + public void rollbackUnfinishedTracks() { + if (isCurrentTrackRollbackable) { + providerUtils.deleteTrack(track.getId()); + isCurrentTrackRollbackable = false; + } + } + + /** + * Get all track ids of the tracks created by this importer run. + * + * @return array of track ids + */ + private long[] getImportedTrackIds() { + // Convert from java.lang.Long for convenience + long[] result = new long[tracksWritten.size()]; + for (int i = 0; i < result.length; i++) { + result[i] = tracksWritten.get(i); + } + return result; + } + + /** + * Parse time trying different formats used in GPX files. + * + * @param timeContents string with time information + * @return time as long + * @throws SAXException on time parsing errors + */ + private long parseTimeForAllFormats(String timeContents) throws SAXException { + long time; + timeContents = timeContents.trim(); + + // 1st try with time zone at end a la "+0000" + time = parseTime(timeContents, DATE_FORMAT1); + if (time > -1) { return time; } + + // if that fails, try with a literal "Z" at the end + // (this is not according to xml standard, but some gpx files are like + // that): + time = parseTime(timeContents, DATE_FORMAT2); + if (time > -1) { return time; } + + // some gpx timestamps have 3 additional digits at the end. + time = parseTime(timeContents, DATE_FORMAT3); + if (time > -1) { return time; } + + // everything failed - abort the import + String msg = createErrorMessage("Invalid time format: " + timeContents); + throw new SAXException(msg); + } + + private long parseTime(String timeContents, SimpleDateFormat formatParam) { + SimpleDateFormat format = formatParam; + try { + format.setTimeZone(UTC_TIMEZONE); + return format.parse(timeContents).getTime(); + } catch (ParseException ex) { + return -1; + } + } + + /** + * Returns the ID of the last point inserted into the database. + */ + private long getLastPointId() { + flushPointInserts(); + + return providerUtils.getLastLocationId(track.getId()); + } + + /** + * Builds a parsing error message with current line information. + * + * @param details details about the error, will be appended + * @return error message string with current line information + */ + private String createErrorMessage(String details) { + StringBuffer msg = new StringBuffer(); + msg.append("Parsing error at line: "); + msg.append(locator.getLineNumber()); + msg.append(" column: "); + msg.append(locator.getColumnNumber()); + msg.append(". "); + msg.append(details); + return msg.toString(); + } +} diff --git a/MyTracksTest/.classpath b/MyTracksTest/.classpath index f8a6020f2..7ec6a9bf6 100644 --- a/MyTracksTest/.classpath +++ b/MyTracksTest/.classpath @@ -4,6 +4,11 @@ - + + + + + + diff --git a/MyTracksTest/.factorypath b/MyTracksTest/.factorypath new file mode 100644 index 000000000..25bd193ed --- /dev/null +++ b/MyTracksTest/.factorypath @@ -0,0 +1,4 @@ + + + + diff --git a/MyTracksTest/.settings/org.eclipse.jdt.apt.core.prefs b/MyTracksTest/.settings/org.eclipse.jdt.apt.core.prefs new file mode 100644 index 000000000..357ffd6b8 --- /dev/null +++ b/MyTracksTest/.settings/org.eclipse.jdt.apt.core.prefs @@ -0,0 +1,7 @@ +#Thu Aug 12 19:30:48 BRT 2010 +eclipse.preferences.version=1 +org.eclipse.jdt.apt.aptEnabled=true +org.eclipse.jdt.apt.genSrcDir=.apt_generated +org.eclipse.jdt.apt.processorOptions/bin_dir=MyTracksTest/bin +org.eclipse.jdt.apt.processorOptions/logfile=MyTracksTest/annotations.log +org.eclipse.jdt.apt.reconcileEnabled=false diff --git a/MyTracksTest/.settings/org.eclipse.jdt.core.prefs b/MyTracksTest/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 000000000..1afc2a96c --- /dev/null +++ b/MyTracksTest/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,13 @@ +#Thu Aug 12 19:30:48 BRT 2010 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.compliance=1.6 +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.processAnnotations=enabled +org.eclipse.jdt.core.compiler.source=1.6 diff --git a/MyTracksTest/default.properties b/MyTracksTest/default.properties index 5b3be86a8..c4fb0851b 100644 --- a/MyTracksTest/default.properties +++ b/MyTracksTest/default.properties @@ -10,4 +10,4 @@ # Indicates whether an apk should be generated for each density. split.density=false # Project target. -target=Google Inc.:Google APIs:4 +target=Google Inc.:Google APIs:8 diff --git a/MyTracksTest/libs/AndroidMockGenerator.jar b/MyTracksTest/libs/AndroidMockGenerator.jar new file mode 100644 index 000000000..ef8b3612c Binary files /dev/null and b/MyTracksTest/libs/AndroidMockGenerator.jar differ diff --git a/MyTracksTest/libs/AndroidMockRuntime.jar b/MyTracksTest/libs/AndroidMockRuntime.jar new file mode 100644 index 000000000..40fa9e173 Binary files /dev/null and b/MyTracksTest/libs/AndroidMockRuntime.jar differ diff --git a/MyTracksTest/libs/easymock.jar b/MyTracksTest/libs/easymock.jar deleted file mode 100644 index de591680c..000000000 Binary files a/MyTracksTest/libs/easymock.jar and /dev/null differ diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/io/GpxImporterTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/io/GpxImporterTest.java new file mode 100644 index 000000000..091d2b0f0 --- /dev/null +++ b/MyTracksTest/src/com/google/android/apps/mytracks/io/GpxImporterTest.java @@ -0,0 +1,258 @@ +/* + * Copyright 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.google.android.apps.mytracks.io; + +import static com.google.android.testing.mocking.AndroidMock.eq; +import static com.google.android.testing.mocking.AndroidMock.expect; + +import com.google.android.apps.mytracks.content.MyTracksProviderUtils; +import com.google.android.apps.mytracks.content.MyTracksProviderUtils.Factory; +import com.google.android.apps.mytracks.content.Track; +import com.google.android.apps.mytracks.content.TracksColumns; +import com.google.android.apps.mytracks.testing.TestingProviderUtilsFactory; +import com.google.android.testing.mocking.AndroidMock; +import com.google.android.testing.mocking.UsesMocks; + +import android.content.ContentUris; +import android.location.Location; +import android.location.LocationManager; +import android.net.Uri; +import android.test.AndroidTestCase; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.text.SimpleDateFormat; +import java.util.Arrays; + +import javax.xml.parsers.ParserConfigurationException; + +import org.easymock.Capture; +import org.easymock.IArgumentMatcher; +import org.xml.sax.SAXException; + +/** + * Tests for the GPX importer. + * + * @author Steffen Horlacher + */ +public class GpxImporterTest extends AndroidTestCase { + + private static final String TRACK_NAME = "blablub"; + private static final String TRACK_DESC = "s'Laebe isch koi Schlotzer"; + + private static final String TRACK_LAT_1 = "48.768364"; + private static final String TRACK_LON_1 = "9.177886"; + private static final String TRACK_ELE_1 = "324.0"; + private static final String TRACK_TIME_1 = "2010-04-22T18:21:00Z"; + + private static final String TRACK_LAT_2 = "48.768374"; + private static final String TRACK_LON_2 = "9.177816"; + private static final String TRACK_ELE_2 = "333.0"; + private static final String TRACK_TIME_2 = "2010-04-22T18:21:50Z"; + + // TODO use real files from different sources with more track points + private static final String VALID_TEST_GPX = "" + "" + TRACK_ELE_1 + " +" + "" + TRACK_ELE_2 + "" + ""; + + // invalid xml + private static final String INVALID_XML_TEST_GPX = VALID_TEST_GPX.substring( + 0, VALID_TEST_GPX.length() - 50); + private static final String INVALID_LOCATION_TEST_GPX = VALID_TEST_GPX + .replaceAll(TRACK_LAT_1, "1000.0"); + private static final String INVALID_TIME_TEST_GPX = VALID_TEST_GPX + .replaceAll(TRACK_TIME_1, "invalid"); + + private static final long TRACK_ID = 1; + private static final long TRACK_POINT_ID_1 = 1; + private static final long TRACK_POINT_ID_2 = 2; + + private static final Uri TRACK_ID_URI = ContentUris.appendId( + TracksColumns.CONTENT_URI.buildUpon(), TRACK_ID).build(); + + private MyTracksProviderUtils providerUtils; + + private Factory oldProviderUtilsFactory; + + @UsesMocks(MyTracksProviderUtils.class) + @Override + protected void setUp() throws Exception { + super.setUp(); + + providerUtils = AndroidMock.createMock(MyTracksProviderUtils.class); + oldProviderUtilsFactory = + TestingProviderUtilsFactory.installWithInstance(providerUtils); + } + + @Override + protected void tearDown() throws Exception { + TestingProviderUtilsFactory.restoreOldFactory(oldProviderUtilsFactory); + super.tearDown(); + } + + /** + * Test import success + */ + public void testImportSuccess() throws Exception { + Capture trackParam = new Capture(); + + SimpleDateFormat format = GpxImporter.DATE_FORMAT2; + Location loc1 = new Location(LocationManager.GPS_PROVIDER); + loc1.setTime(format.parse(TRACK_TIME_1).getTime()); + loc1.setLatitude(Double.parseDouble(TRACK_LAT_1)); + loc1.setLongitude(Double.parseDouble(TRACK_LON_1)); + loc1.setAltitude(Double.parseDouble(TRACK_ELE_1)); + + Location loc2 = new Location(LocationManager.GPS_PROVIDER); + loc2.setTime(format.parse(TRACK_TIME_2).getTime()); + loc2.setLatitude(Double.parseDouble(TRACK_LAT_2)); + loc2.setLongitude(Double.parseDouble(TRACK_LON_2)); + loc2.setAltitude(Double.parseDouble(TRACK_ELE_2)); + + expect(providerUtils.insertTrack(AndroidMock.capture(trackParam))) + .andReturn(TRACK_ID_URI); + + expect(providerUtils.getLastLocationId(TRACK_ID)).andReturn(TRACK_POINT_ID_1).andReturn(TRACK_POINT_ID_2); + + // A flush happens after the first insertion to get the starting point ID, + // which is why we get two calls + expect(providerUtils.bulkInsertTrackPoints(LocationsMatcher.eqLoc(loc1), + eq(1), eq(TRACK_ID))).andReturn(1); + expect(providerUtils.bulkInsertTrackPoints(LocationsMatcher.eqLoc(loc2), + eq(1), eq(TRACK_ID))).andReturn(1); + + providerUtils.updateTrack(AndroidMock.capture(trackParam)); + + AndroidMock.replay(providerUtils); + + InputStream is = new ByteArrayInputStream(VALID_TEST_GPX.getBytes()); + GpxImporter.importGPXFile(is, providerUtils); + + AndroidMock.verify(providerUtils); + + // verify track parameter + Track track = trackParam.getValue(); + assertEquals(TRACK_NAME, track.getName()); + assertEquals(TRACK_DESC, track.getDescription()); + assertEquals(format.parse(TRACK_TIME_1).getTime(), track.getStatistics() + .getStartTime()); + assertNotSame(-1, track.getStartId()); + assertNotSame(-1, track.getStopId()); + } + + /** + * Test with invalid location - track should be deleted + */ + public void testImportLocationFailure() throws ParserConfigurationException, + SAXException, IOException { + testInvalidXML(INVALID_LOCATION_TEST_GPX); + } + + /** + * Test with invalid time - track should be deleted + */ + public void testImportTimeFailure() throws ParserConfigurationException, + SAXException, IOException { + testInvalidXML(INVALID_TIME_TEST_GPX); + } + + /** + * Test with invalid xml - track should be deleted + */ + public void testImportXMLFailure() throws ParserConfigurationException, + SAXException, IOException { + testInvalidXML(INVALID_XML_TEST_GPX); + } + + private void testInvalidXML(String xml) throws ParserConfigurationException, + IOException { + expect(providerUtils.insertTrack((Track) AndroidMock.anyObject())) + .andReturn(TRACK_ID_URI); + + expect(providerUtils.bulkInsertTrackPoints((Location[]) AndroidMock.anyObject(), + AndroidMock.anyInt(), AndroidMock.anyLong())).andStubReturn(1); + expect(providerUtils.getLastLocationId(TRACK_ID)).andStubReturn(TRACK_POINT_ID_1); + + providerUtils.deleteTrack(TRACK_ID); + + AndroidMock.replay(providerUtils); + + InputStream is = new ByteArrayInputStream(xml.getBytes()); + try { + GpxImporter.importGPXFile(is, providerUtils); + } catch (SAXException e) { + // expected exception + } + + AndroidMock.verify(providerUtils); + } + + /** + * Workaround because of capture bug 2617107 in easymock: + * http://sourceforge.net + * /tracker/?func=detail&aid=2617107&group_id=82958&atid=567837 + */ + private static class LocationsMatcher implements IArgumentMatcher { + private final Location[] matchLocs; + + private LocationsMatcher(Location[] expected) { + this.matchLocs = expected; + } + + public static Location[] eqLoc(Location[] expected) { + IArgumentMatcher matcher = new LocationsMatcher(expected); + AndroidMock.reportMatcher(matcher); + return null; + } + + public static Location[] eqLoc(Location expected) { + return eqLoc(new Location[] { expected}); + } + + @Override + public void appendTo(StringBuffer buf) { + buf.append("eqLoc(").append(Arrays.toString(matchLocs)).append(")"); + } + + @Override + public boolean matches(Object obj) { + if (! (obj instanceof Location[])) { return false; } + Location[] locs = (Location[]) obj; + if (locs.length < matchLocs.length) { return false; } + + // Only check the first elements (those that will be taken into account) + for (int i = 0; i < matchLocs.length; i++) { + if (!locationsMatch(locs[i], matchLocs[i])) { + return false; + } + } + + return true; + } + + private boolean locationsMatch(Location loc1, Location loc2) { + return (loc1.getTime() == loc2.getTime()) && + (loc1.getLatitude() == loc2.getLatitude()) && + (loc1.getLongitude() == loc2.getLongitude()) && + (loc1.getAltitude() == loc2.getAltitude()); + } + } +}