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 index 58bc237cc..dec15e80b 100644 --- a/MyTracks/.settings/org.eclipse.jdt.core.prefs +++ b/MyTracks/.settings/org.eclipse.jdt.core.prefs @@ -1,5 +1,12 @@ -#Fri Jun 11 16:44:12 CST 2010 -eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.5 -org.eclipse.jdt.core.compiler.compliance=1.5 -org.eclipse.jdt.core.compiler.source=1.5 +#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/AndroidManifest.xml b/MyTracks/AndroidManifest.xml index 19700daba..96cc80b4f 100755 --- a/MyTracks/AndroidManifest.xml +++ b/MyTracks/AndroidManifest.xml @@ -1,17 +1,20 @@ + android:versionCode="17" + android:versionName="1.0.16"> + android:label="@string/app_name" + android:backupAgent="com.google.android.apps.mytracks.io.backup.MyTracksBackupAgent"> + + - @@ -36,16 +39,13 @@ - + android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|orientation|fontScale" /> - + android:label="Recorded Tracks" /> - + android:label="Recorded Waypoints" /> @@ -63,13 +63,14 @@ android:label="@string/elevation_label"/> + android:label="@string/welcome"/> + android:enabled="true" + android:exported="true" /> diff --git a/MyTracks/res/layout/about.xml b/MyTracks/res/layout/about.xml index e3fe3cfb8..51f432b90 100644 --- a/MyTracks/res/layout/about.xml +++ b/MyTracks/res/layout/about.xml @@ -95,8 +95,9 @@ limitations under the License. android:textSize="20sp" /> diff --git a/MyTracks/res/menu/main.xml b/MyTracks/res/menu/main.xml new file mode 100644 index 000000000..23053e2d5 --- /dev/null +++ b/MyTracks/res/menu/main.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + 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/res/values/arrays.xml b/MyTracks/res/values/arrays.xml index d1be00be5..78075af5f 100644 --- a/MyTracks/res/values/arrays.xml +++ b/MyTracks/res/values/arrays.xml @@ -370,5 +370,6 @@ limitations under the License. GPX KML CSV + TCX diff --git a/MyTracks/res/values/keys.xml b/MyTracks/res/values/keys.xml new file mode 100644 index 000000000..30a83a613 --- /dev/null +++ b/MyTracks/res/values/keys.xml @@ -0,0 +1,48 @@ + + + + + announcementFrequency + bluetoothDevice + bluetoothPairing + bluetoothSensor + defaultMapPublic + maxRecordingDistance + metricUnits + minRecordingDistance + minRecordingInterval + minRequiredAccuracy + modularStatsDisplay + pickExistingMap + reportSpeed + sendStatsAndPoints + sendToDocs + sendToMyMaps + sensorId + sensorType + shareUrlOnly + signalSamplingFrequency + splitFrequency + + + recordingTrack + selectedTrack + + + backupToSd + restoreFromSd + \ No newline at end of file diff --git a/MyTracks/res/values/strings.xml b/MyTracks/res/values/strings.xml index 80a182bf6..dfa0b7cb3 100644 --- a/MyTracks/res/values/strings.xml +++ b/MyTracks/res/values/strings.xml @@ -122,6 +122,7 @@ limitations under the License. Save a GPX file Save a KML file Save a CSV file + Save a TCX file Send to Google My Maps Delete all tracks @@ -135,6 +136,7 @@ limitations under the License. A GPX file A KML file A CSV file + A TCX file Clear map @@ -165,9 +167,10 @@ limitations under the License. Recording Sharing + Not available in your version of Android + Announcement frequency Minimum time between two voice announcements - Text-to-speech announcements are not available in your version of Android Min distance between points Smallest distance between two recordings of your position @@ -204,6 +207,33 @@ limitations under the License. Maximum time or distance after which a new split marker will be inserted Pace + Only supported in Android 2.0 or above + Sensor Type + The type of sensor to connect to. + Sensor Id + The Id sent to sensors. + The Id must be an integer value. + Bluetooth sensor + The bluetooth sensor to connect to. + Modular Stats Display + The stats screen is split in various modes. + Display all stats on one screen + + Backup + SD Card Backup + Backup now + Write a full copy of all your tracks and settings to the SD card + Restore now + Restore all tracks and settings from the SD card + Cannot manipulate backups while recording a track. + + + Restoring a backup from the SD card will OVERWRITE all your current tracks and settings. Are you sure? + Select the backup to restore + No backups found. + Writing all tracks and settings to the SD card. This may take a while... + Reading all tracks and settings from the SD card. This may take a while... + GPS Satellites @@ -291,6 +321,7 @@ limitations under the License. minutes second seconds + ms m @@ -310,6 +341,12 @@ limitations under the License. min/mi per mile + + W + + rpm + bpm + Moving Time Total Time @@ -320,6 +357,10 @@ limitations under the License. Statistics Total Distance Distance + Power + Cadence + Heart Rate + Max Heart Rate - Waiting... @@ -337,7 +378,6 @@ limitations under the License. Support: - 1.0.14 mytracks@googlegroups.com mytracks.appspot.com Copyright 2009 Google Inc. @@ -364,8 +404,10 @@ limitations under the License. No external storage was found. Make sure that a SD card is inserted and that your phone is not mounted to a computer. Unable to write to file. + Unable to read from file. Unable to create directory on SD card. - Finished writing the track to SD card. + Finished writing to SD card. + Finished reading from SD card. Upgrade Check @@ -403,4 +445,29 @@ limitations under the License. GSM Strength: %1$d CDMA Strength: %1$d dBm EVDO Strength: %1$d dBm + + + Bluetooth + Bluetooth options + Device pairing + Open Android\'s bluetooth settings to pair a device + bluetoothSensorsEnabled + Sensor Options + + + NONE + ANT + SRM + ATHLOSOFT + + + Connected to %s + Bluetooth sensor connecting ... + Bluetooth sensor not connected + + + Sensor Data Time + Power meter enabled + Heart rate monitor enabled + diff --git a/MyTracks/res/xml/preferences.xml b/MyTracks/res/xml/preferences.xml index 77f1abcf7..ddc6b2ff9 100644 --- a/MyTracks/res/xml/preferences.xml +++ b/MyTracks/res/xml/preferences.xml @@ -18,26 +18,26 @@ limitations under the License. xmlns:android="http://schemas.android.com/apk/res/android"> - + - + - + - + - + - + + + + + + + + + diff --git a/MyTracks/src/com/google/android/apps/mymaps/MyMapsGDataWrapper.java b/MyTracks/src/com/google/android/apps/mymaps/MyMapsGDataWrapper.java index de4e0f069..365edb94f 100644 --- a/MyTracks/src/com/google/android/apps/mymaps/MyMapsGDataWrapper.java +++ b/MyTracks/src/com/google/android/apps/mymaps/MyMapsGDataWrapper.java @@ -114,7 +114,7 @@ public class MyMapsGDataWrapper { } catch (HttpException e) { Log.e(MyTracksConstants.TAG, "HttpException", e); errorMessage = e.getMessage(); - if (errorMessage.contains("401")) { + if (errorMessage != null && errorMessage.contains("401")) { errorType = ERROR_AUTH; } else { errorType = ERROR_CONNECTION; @@ -122,7 +122,7 @@ public class MyMapsGDataWrapper { } catch (IOException e) { Log.e(MyTracksConstants.TAG, "Exception", e); errorMessage = e.getMessage(); - if (errorMessage.contains("503")) { + if (errorMessage != null && errorMessage.contains("503")) { errorType = ERROR_INTERNAL; } else { errorType = ERROR_CONNECTION; diff --git a/MyTracks/src/com/google/android/apps/mymaps/VersionChecker.java b/MyTracks/src/com/google/android/apps/mymaps/VersionChecker.java index 6a47a7879..cfc1f8a3d 100644 --- a/MyTracks/src/com/google/android/apps/mymaps/VersionChecker.java +++ b/MyTracks/src/com/google/android/apps/mymaps/VersionChecker.java @@ -29,6 +29,9 @@ import android.app.Dialog; import android.app.TabActivity; import android.content.DialogInterface; import android.content.DialogInterface.OnClickListener; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; import android.content.Intent; import android.net.Uri; import android.util.Log; @@ -80,7 +83,17 @@ public class VersionChecker { final boolean newVersion; // Returned new version available final boolean newVersionRecommended; // Returned new version recommended final boolean newVersionRequired; // Returned new version required - String currentVersion = parentActivity.getString(R.string.version); + String currentVersion = ""; + try { + PackageInfo pi = + parentActivity.getPackageManager().getPackageInfo( + "com.google.android.maps.mytracks", + PackageManager.GET_META_DATA); + currentVersion = pi.versionName; + } catch (NameNotFoundException e) { + Log.w(MyTracksConstants.TAG, "Failed to get version info.", e); + return; + } GDataParser parser = client.getParserForFeed( Entry.class, diff --git a/MyTracks/src/com/google/android/apps/mytracks/ChartActivity.java b/MyTracks/src/com/google/android/apps/mytracks/ChartActivity.java index f31539bee..f665d1b5e 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/ChartActivity.java +++ b/MyTracks/src/com/google/android/apps/mytracks/ChartActivity.java @@ -1,12 +1,12 @@ /* * 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 @@ -35,6 +35,7 @@ import android.location.Location; import android.os.Bundle; import android.os.Handler; import android.os.HandlerThread; +import android.speech.tts.TextToSpeech; import android.util.Log; import android.view.Menu; import android.view.MenuItem; @@ -49,7 +50,7 @@ import java.util.ArrayList; /** * An activity that displays a chart from the track point provider. - * + * * @author Sandor Dornbush */ public class ChartActivity extends Activity implements @@ -204,23 +205,27 @@ public class ChartActivity extends Activity implements public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { if (key != null) { - if (key.equals(MyTracksSettings.SELECTED_TRACK)) { + if (key.equals(getString(R.string.selected_track_key))) { selectedTrackId = - sharedPreferences.getLong(MyTracksSettings.SELECTED_TRACK, -1); + sharedPreferences.getLong(getString(R.string.selected_track_key), + -1); readProfileAsync(); - } else if (key.equals(MyTracksSettings.METRIC_UNITS)) { + } else if (key.equals(getString(R.string.metric_units_key))) { metricUnits = - sharedPreferences.getBoolean(MyTracksSettings.METRIC_UNITS, true); + sharedPreferences.getBoolean(getString(R.string.metric_units_key), + true); cv.setMetricUnits(metricUnits); readProfileAsync(); - } else if (key.equals(MyTracksSettings.REPORT_SPEED)) { + } else if (key.equals(getString(R.string.report_speed_key))) { reportSpeed = - sharedPreferences.getBoolean(MyTracksSettings.REPORT_SPEED, true); + sharedPreferences.getBoolean(getString(R.string.report_speed_key), + true); cv.setReportSpeed(reportSpeed, this); readProfileAsync(); - } else if (key.equals(MyTracksSettings.RECORDING_TRACK)) { + } else if (key.equals(getString(R.string.recording_track_key))) { recordingTrackId = - sharedPreferences.getLong(MyTracksSettings.RECORDING_TRACK, -1); + sharedPreferences.getLong(getString(R.string.recording_track_key), + -1); runOnUiThread(updateChart); } } @@ -233,6 +238,9 @@ public class ChartActivity extends Activity implements MyTracks.getInstance().setChartActivity(this); providerUtils = MyTracksProviderUtils.Factory.get(this); + // The volume we want to control is the Text-To-Speech volume + setVolumeControlStream(TextToSpeech.Engine.DEFAULT_STREAM); + requestWindowFeature(Window.FEATURE_NO_TITLE); setContentView(R.layout.mytracks_elevation); ViewGroup layout = (ViewGroup) findViewById(R.id.elevation_chart); @@ -246,12 +254,14 @@ public class ChartActivity extends Activity implements getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0); if (preferences != null) { selectedTrackId = - preferences.getLong(MyTracksSettings.SELECTED_TRACK, -1); + preferences.getLong(getString(R.string.selected_track_key), -1); recordingTrackId = - preferences.getLong(MyTracksSettings.RECORDING_TRACK, -1); - metricUnits = preferences.getBoolean(MyTracksSettings.METRIC_UNITS, true); + preferences.getLong(getString(R.string.recording_track_key), -1); + metricUnits = preferences.getBoolean(getString(R.string.metric_units_key), + true); cv.setMetricUnits(metricUnits); - reportSpeed = preferences.getBoolean(MyTracksSettings.REPORT_SPEED, true); + reportSpeed = preferences.getBoolean(getString(R.string.report_speed_key), + true); cv.setReportSpeed(reportSpeed, this); preferences.registerOnSharedPreferenceChangeListener(this); } @@ -450,7 +460,7 @@ public class ChartActivity extends Activity implements * data[0] = the time or distance * data[1] = the elevation * data[2] = the speed - * + * * @param location a location * @return the data point */ diff --git a/MyTracks/src/com/google/android/apps/mytracks/ChartSettingsDialog.java b/MyTracks/src/com/google/android/apps/mytracks/ChartSettingsDialog.java index b0aa5a14f..0610e0671 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/ChartSettingsDialog.java +++ b/MyTracks/src/com/google/android/apps/mytracks/ChartSettingsDialog.java @@ -66,6 +66,10 @@ public class ChartSettingsDialog extends Dialog { } public void setup(ChartActivity chart) { + if (chart == null) { + return; + } + RadioGroup rd = (RadioGroup) findViewById(R.id.chart_settings_x); rd.check(chart.getMode() == Mode.BY_DISTANCE ? R.id.chart_settings_by_distance diff --git a/MyTracks/src/com/google/android/apps/mytracks/ChartView.java b/MyTracks/src/com/google/android/apps/mytracks/ChartView.java index 905374150..4ff195a76 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/ChartView.java +++ b/MyTracks/src/com/google/android/apps/mytracks/ChartView.java @@ -511,8 +511,8 @@ public class ChartView extends View { // Dimensions have changed (for example due to orientation change): w = c.getWidth(); h = c.getHeight(); - effectiveWidth = w - leftBorder - RIGHT_BORDER; - effectiveHeight = h - TOP_BORDER - BOTTOM_BORDER; + effectiveWidth = Math.max(0, w - leftBorder - RIGHT_BORDER); + effectiveHeight = Math.max(0, h - TOP_BORDER - BOTTOM_BORDER); setupPath(); } c.save(); diff --git a/MyTracks/src/com/google/android/apps/mytracks/ExportAllTracks.java b/MyTracks/src/com/google/android/apps/mytracks/ExportAllTracks.java index 07311fca2..bd1b3fd31 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/ExportAllTracks.java +++ b/MyTracks/src/com/google/android/apps/mytracks/ExportAllTracks.java @@ -17,6 +17,7 @@ package com.google.android.apps.mytracks; import com.google.android.apps.mytracks.content.MyTracksProviderUtils; import com.google.android.apps.mytracks.content.TracksColumns; +import com.google.android.apps.mytracks.io.TrackWriter; import com.google.android.apps.mytracks.io.TrackWriterFactory; import com.google.android.apps.mytracks.io.TrackWriterFactory.TrackFileFormat; import com.google.android.maps.mytracks.R; @@ -109,7 +110,8 @@ public class ExportAllTracks { context.getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0); long recordingTrackId = -1; if (prefs != null) { - recordingTrackId = prefs.getLong(MyTracksSettings.RECORDING_TRACK, -1); + recordingTrackId = + prefs.getLong(context.getString(R.string.recording_track_key), -1); } if (recordingTrackId != -1) { acquireWakeLock(); @@ -153,6 +155,7 @@ public class ExportAllTracks { if (cursor == null) { return; } + final int trackCount = cursor.getCount(); Log.i(MyTracksConstants.TAG, "ExportAllTracks: Exporting: " + cursor.getCount() + " tracks."); @@ -160,7 +163,9 @@ public class ExportAllTracks { MyTracks.getInstance().runOnUiThread(new Runnable() { public void run() { makeProgressDialog(trackCount); - }}); + } + }); + for (int i = 0; cursor.moveToNext(); i++) { final int status = i; MyTracks.getInstance().runOnUiThread(new Runnable() { @@ -176,16 +181,27 @@ public class ExportAllTracks { long id = cursor.getLong(idxTrackId); Log.i(MyTracksConstants.TAG, "ExportAllTracks: exporting: " + id); - TrackWriterFactory.newWriter(context, providerUtils, id, format) - .writeTrack(); + TrackWriter writer = + TrackWriterFactory.newWriter(context, providerUtils, id, format); + writer.writeTrack(); + + if (!writer.wasSuccess()) { + // Abort the whole export on the first error. + int error = writer.getErrorMessage(); + Toast.makeText(context, error, Toast.LENGTH_LONG).show(); + return; + } } } finally { if (cursor != null) { cursor.close(); } - } - if (progress != null) { - progress.dismiss(); + if (progress != null) { + synchronized (this) { + progress.dismiss(); + progress = null; + } + } } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/MenuManager.java b/MyTracks/src/com/google/android/apps/mytracks/MenuManager.java new file mode 100644 index 000000000..e09011b86 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/MenuManager.java @@ -0,0 +1,93 @@ +/* + * Copyright 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.google.android.apps.mytracks; + +import com.google.android.maps.mytracks.R; + +import android.content.Intent; +import android.view.Menu; +import android.view.MenuItem; + +/** + * Manage the application menus. + * + * @author Sandor Dornbush + */ +class MenuManager { + + private MyTracks activity; + + public MenuManager(MyTracks activity) { + this.activity = activity; + } + + public boolean onCreateOptionsMenu(Menu menu) { + activity.getMenuInflater().inflate(R.menu.main, menu); + return true; + } + + public void onPrepareOptionsMenu(Menu menu, boolean hasRecorded, + boolean isRecording, boolean hasSelectedTrack) { + menu.findItem(R.id.menu_list_tracks).setEnabled(hasRecorded); + menu.findItem(R.id.menu_list_markers) + .setEnabled(hasRecorded && hasSelectedTrack); + MenuItem startRecording = menu.findItem(R.id.menu_start_recording); + startRecording.setEnabled(!isRecording); + startRecording.setVisible(!isRecording); + MenuItem stopRecording = menu.findItem(R.id.menu_stop_recording); + stopRecording.setEnabled(isRecording); + stopRecording.setVisible(isRecording); + } + + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.menu_start_recording: { + activity.startRecording(); + return true; + } + case R.id.menu_stop_recording: { + activity.stopRecording(); + return true; + } + case R.id.menu_list_tracks: { + Intent startIntent = new Intent(activity, MyTracksList.class); + activity.startActivityForResult(startIntent, MyTracksConstants.SHOW_TRACK); + return true; + } + case R.id.menu_list_markers: { + Intent startIntent = new Intent(activity, MyTracksWaypointsList.class); + startIntent.putExtra("trackid", activity.getSelectedTrack()); + activity.startActivityForResult(startIntent, MyTracksConstants.SHOW_WAYPOINT); + return true; + } + case R.id.menu_settings: { + Intent startIntent = new Intent(activity, MyTracksSettings.class); + activity.startActivity(startIntent); + return true; + } + case R.id.menu_help: { + Intent startIntent = new Intent(activity, WelcomeActivity.class); + activity.startActivity(startIntent); + return true; + } + case MyTracksConstants.MENU_CLEAR_MAP: { + activity.setSelectedTrack(-1); + return true; + } + } + return false; + } +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java b/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java index 59fe72732..9a3455172 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; @@ -32,6 +32,7 @@ import com.google.android.apps.mytracks.io.TrackWriterFactory; import com.google.android.apps.mytracks.io.TrackWriterFactory.TrackFileFormat; import com.google.android.apps.mytracks.services.ITrackRecordingService; import com.google.android.apps.mytracks.services.TrackRecordingService; +import com.google.android.apps.mytracks.util.FileUtils; import com.google.android.maps.mytracks.R; import android.app.Activity; @@ -70,16 +71,17 @@ 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. * @@ -133,15 +135,9 @@ public class MyTracks extends TabActivity implements OnTouchListener, private ChartSettingsDialog chartSettingsDialog; /* - * Menu items: + * Menu manager. */ - - private MenuItem startRecording; - private MenuItem stopRecording; - private MenuItem listTracks; - private MenuItem listMarkers; - private MenuItem settings; - private MenuItem help; + private MenuManager menuManager; /* * Information on upload success to MyMaps/Docs. @@ -282,6 +278,7 @@ public class MyTracks extends TabActivity implements OnTouchListener, super.onCreate(savedInstanceState); instance = this; providerUtils = MyTracksProviderUtils.Factory.get(this); + menuManager = new MenuManager(this); // The volume we want to control is the Text-To-Speech volume setVolumeControlStream(TextToSpeech.Engine.DEFAULT_STREAM); @@ -320,8 +317,10 @@ public class MyTracks extends TabActivity implements OnTouchListener, SharedPreferences prefs = getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0); if (prefs != null) { - selectedTrackId = prefs.getLong(MyTracksSettings.SELECTED_TRACK, -1); - recordingTrackId = prefs.getLong(MyTracksSettings.RECORDING_TRACK, -1); + selectedTrackId = + prefs.getLong(getString(R.string.selected_track_key), -1); + recordingTrackId = + prefs.getLong(getString(R.string.recording_track_key), -1); prefs.registerOnSharedPreferenceChangeListener(this); } @@ -415,81 +414,21 @@ public class MyTracks extends TabActivity implements OnTouchListener, @Override public boolean onCreateOptionsMenu(Menu menu) { super.onCreateOptionsMenu(menu); - startRecording = menu.add(0, MyTracksConstants.MENU_START_RECORDING, 0, - R.string.start_recording); - startRecording.setIcon(R.drawable.menu_record); - stopRecording = menu.add(0, MyTracksConstants.MENU_STOP_RECORDING, 0, - R.string.stop_recording); - stopRecording.setIcon(R.drawable.menu_stop); - listTracks = menu.add(0, MyTracksConstants.MENU_LIST_TRACKS, 0, - R.string.list_tracks); - listTracks.setIcon(R.drawable.menu_list_tracks); - listMarkers = menu.add(0, MyTracksConstants.MENU_LIST_MARKERS, 0, - R.string.list_markers); - listMarkers.setIcon(R.drawable.menu_marker); - settings = menu.add(0, MyTracksConstants.MENU_SETTINGS, 10001, - R.string.settings); - settings.setIcon(android.R.drawable.ic_menu_preferences); - help = menu.add(0, MyTracksConstants.MENU_HELP, 10002, R.string.help); - help.setIcon(android.R.drawable.ic_menu_info_details); - return true; + return menuManager.onCreateOptionsMenu(menu); } @Override public boolean onPrepareOptionsMenu(Menu menu) { - boolean hasRecorded = providerUtils.getLastTrack() != null; - boolean isRecording = isRecording(); - listTracks = menu.findItem(MyTracksConstants.MENU_LIST_TRACKS); - listTracks.setEnabled(hasRecorded); - listMarkers = menu.findItem(MyTracksConstants.MENU_LIST_MARKERS); - listMarkers.setEnabled(hasRecorded && selectedTrackId >= 0); - startRecording = menu.findItem(MyTracksConstants.MENU_START_RECORDING); - startRecording.setEnabled(!isRecording); - startRecording.setVisible(!isRecording); - stopRecording = menu.findItem(MyTracksConstants.MENU_STOP_RECORDING); - stopRecording.setEnabled(isRecording); - stopRecording.setVisible(isRecording); + menuManager.onPrepareOptionsMenu(menu, providerUtils.getLastTrack() != null, + isRecording(), selectedTrackId >= 0); return super.onPrepareOptionsMenu(menu); } @Override public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case MyTracksConstants.MENU_START_RECORDING: { - startRecording(); - return true; - } - case MyTracksConstants.MENU_STOP_RECORDING: { - stopRecording(); - return true; - } - case MyTracksConstants.MENU_LIST_TRACKS: { - Intent startIntent = new Intent(this, MyTracksList.class); - startActivityForResult(startIntent, MyTracksConstants.SHOW_TRACK); - return true; - } - case MyTracksConstants.MENU_LIST_MARKERS: { - Intent startIntent = new Intent(this, MyTracksWaypointsList.class); - startIntent.putExtra("trackid", selectedTrackId); - startActivityForResult(startIntent, MyTracksConstants.SHOW_WAYPOINT); - return true; - } - case MyTracksConstants.MENU_SETTINGS: { - Intent startIntent = new Intent(this, MyTracksSettings.class); - startActivity(startIntent); - return true; - } - case MyTracksConstants.MENU_HELP: { - Intent startIntent = new Intent(this, WelcomeActivity.class); - startActivity(startIntent); - return true; - } - case MyTracksConstants.MENU_CLEAR_MAP: { - setSelectedTrack(-1); - return true; - } - } - return super.onOptionsItemSelected(item); + return (menuManager.onOptionsItemSelected(item) + ? true + : super.onOptionsItemSelected(item)); } /* @@ -786,8 +725,11 @@ public class MyTracks extends TabActivity implements OnTouchListener, case MyTracksConstants.SAVE_KML_FILE: if (exportFormat == null) { exportFormat = TrackFileFormat.KML; } //$FALL-THROUGH$ - case MyTracksConstants.SAVE_CSV_FILE: { + case MyTracksConstants.SAVE_CSV_FILE: if (exportFormat == null) { exportFormat = TrackFileFormat.CSV; } + //$FALL-THROUGH$ + case MyTracksConstants.SAVE_TCX_FILE: + if (exportFormat == null) { exportFormat = TrackFileFormat.TCX; } if (results != null && resultCode == Activity.RESULT_OK) { final long trackId = results.getLongExtra("trackid", selectedTrackId); @@ -796,7 +738,6 @@ public class MyTracks extends TabActivity implements OnTouchListener, } } break; - } case MyTracksConstants.SHARE_LINK: { Track selectedTrack = providerUtils.getTrack(selectedTrackId); if (selectedTrack != null) { @@ -810,15 +751,21 @@ public class MyTracks extends TabActivity implements OnTouchListener, break; } case MyTracksConstants.SHARE_GPX_FILE: - case MyTracksConstants.SHARE_KML_FILE: { - if (results != null && resultCode == RESULT_OK) { - long trackId = results.getLongExtra("trackid", selectedTrackId); + if (exportFormat == null) { exportFormat = TrackFileFormat.GPX; } + //$FALL-THROUGH$ + case MyTracksConstants.SHARE_KML_FILE: + if (exportFormat == null) { exportFormat = TrackFileFormat.KML; } + //$FALL-THROUGH$ + case MyTracksConstants.SHARE_CSV_FILE: + if (exportFormat == null) { exportFormat = TrackFileFormat.CSV; } + //$FALL-THROUGH$ + case MyTracksConstants.SHARE_TCX_FILE: { + if (exportFormat == null) { exportFormat = TrackFileFormat.TCX; } + + if (results != null && resultCode == Activity.RESULT_OK) { + final long trackId = results.getLongExtra("trackid", selectedTrackId); if (trackId >= 0) { - TrackFileFormat format = - (requestCode == MyTracksConstants.SHARE_GPX_FILE) - ? TrackFileFormat.GPX - : TrackFileFormat.KML; - sendTrack(trackId, format); + sendTrack(trackId, exportFormat); } } break; @@ -861,9 +808,9 @@ public class MyTracks extends TabActivity implements OnTouchListener, + "to shared preferences: ", e); } } - if (key != null && key.equals(MyTracksSettings.SELECTED_TRACK)) { + if (key != null && key.equals(getString(R.string.selected_track_key))) { selectedTrackId = - sharedPreferences.getLong(MyTracksSettings.SELECTED_TRACK, -1); + sharedPreferences.getLong(getString(R.string.selected_track_key), -1); } } @@ -950,13 +897,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; @@ -973,15 +921,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() { @@ -1071,7 +1015,8 @@ public class MyTracks extends TabActivity implements OnTouchListener, getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0); boolean shareUrlOnly = true; if (prefs != null) { - shareUrlOnly = prefs.getBoolean(MyTracksSettings.SHARE_URL_ONLY, false); + shareUrlOnly = + prefs.getBoolean(getString(R.string.share_url_only_key), false); } String url = MyMapsConstants.MAPSHOP_BASE_URL + "?msa=0&msid=" + mapId; @@ -1294,7 +1239,7 @@ public class MyTracks extends TabActivity implements OnTouchListener, * Starts the track recording service (if not already running) and binds to * it. Starts recording a new track. */ - private void startRecording() { + public void startRecording() { if (trackRecordingService == null) { startNewTrackRequested = true; Intent startIntent = new Intent(this, TrackRecordingService.class); @@ -1320,7 +1265,7 @@ public class MyTracks extends TabActivity implements OnTouchListener, * Stops the track recording service and unbinds from it. Will display a toast * "Stopped recording" and pop up the Track Details activity. */ - private void stopRecording() { + public void stopRecording() { if (trackRecordingService != null) { try { trackRecordingService.endCurrentTrack(); @@ -1379,18 +1324,22 @@ public class MyTracks extends TabActivity implements OnTouchListener, * * @param trackId the id of the track */ - private void setSelectedTrack(final long trackId) { + public void setSelectedTrack(final long trackId) { runOnUiThread(new Runnable() { public void run() { SharedPreferences prefs = getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0); SharedPreferences.Editor editor = prefs.edit(); - editor.putLong(MyTracksSettings.SELECTED_TRACK, trackId); + editor.putLong(getString(R.string.selected_track_key), trackId); editor.commit(); } }); } + protected long getSelectedTrack() { + return selectedTrackId; + } + /** * Writes the recording track id to the shared preferences. * Executed on the UI thread. @@ -1403,7 +1352,7 @@ public class MyTracks extends TabActivity implements OnTouchListener, SharedPreferences prefs = getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0); SharedPreferences.Editor editor = prefs.edit(); - editor.putLong(MyTracksSettings.RECORDING_TRACK, trackId); + editor.putLong(getString(R.string.recording_track_key), trackId); editor.commit(); } }); @@ -1421,8 +1370,8 @@ public class MyTracks extends TabActivity implements OnTouchListener, getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0); if (prefs != null) { SharedPreferences.Editor editor = prefs.edit(); - editor.putLong(MyTracksSettings.SELECTED_TRACK, theSelectedTrackId); - editor.putLong(MyTracksSettings.RECORDING_TRACK, theRecordingTrackId); + editor.putLong(getString(R.string.selected_track_key), theSelectedTrackId); + editor.putLong(getString(R.string.recording_track_key), theRecordingTrackId); editor.commit(); } } @@ -1507,10 +1456,12 @@ public class MyTracks extends TabActivity implements OnTouchListener, showDialogSafely(DIALOG_WRITE_PROGRESS); final TrackWriter writer = TrackWriterFactory.newWriter(this, providerUtils, trackId, format); - String sep = System.getProperty("file.separator"); + + FileUtils fileUtils = new FileUtils(); String extension = format.getExtension(); - File dir = new File(Environment.getExternalStorageDirectory() - + sep + extension + sep + "tmp"); + String dirName = fileUtils.buildExternalDirectoryPath(extension, "tmp"); + + File dir = new File(dirName); writer.setDirectory(dir); writer.setOnCompletion(new Runnable() { public void run() { diff --git a/MyTracks/src/com/google/android/apps/mytracks/MyTracksConstants.java b/MyTracks/src/com/google/android/apps/mytracks/MyTracksConstants.java index 226bd2ddb..78f3e05d6 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/MyTracksConstants.java +++ b/MyTracks/src/com/google/android/apps/mytracks/MyTracksConstants.java @@ -15,6 +15,8 @@ */ package com.google.android.apps.mytracks; +import android.os.Build; + /** * Constants used by the MyTracks application. * @@ -32,6 +34,17 @@ public abstract class MyTracksConstants { */ public static final String GPS_PROVIDER = "gps"; + /** + * Name of the top-level directory inside the SD card where our files will + * be read from/written to. + */ + public static final Object SDCARD_TOP_DIR = "MyTracks"; + + /** + * The API level of the Android version we're being run under. + */ + public static final int ANDROID_API_LEVEL = Integer.parseInt(Build.VERSION.SDK); + /* * onActivityResult request codes: */ @@ -54,29 +67,25 @@ public abstract class MyTracksConstants { 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 EDIT_DETAILS = 18; - public static final int SAVE_GPX_FILE = 19; - public static final int SAVE_KML_FILE = 20; - public static final int SAVE_CSV_FILE = 21; - public static final int CLEAR_MAP = 22; - public static final int SHOW_WAYPOINT = 23; - public static final int EDIT_WAYPOINT = 24; - public static final int WELCOME = 25; + 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; /* * Menu ids: */ - public static final int MENU_START_RECORDING = 1; - public static final int MENU_STOP_RECORDING = 2; - public static final int MENU_LIST_TRACKS = 3; - public static final int MENU_LIST_MARKERS = 4; - public static final int MENU_SETTINGS = 5; - public static final int MENU_MY_LOCATION = 6; - public static final int MENU_TOGGLE_LAYERS = 7; - public static final int MENU_CHART_SETTINGS = 8; - public static final int MENU_HELP = 9; - public static final int MENU_CURRENT_SEGMENT = 10; + public static final int MENU_MY_LOCATION = 1; + public static final int MENU_TOGGLE_LAYERS = 2; + public static final int MENU_CHART_SETTINGS = 3; + public static final int MENU_CURRENT_SEGMENT = 4; /* * Context menu ids: @@ -91,11 +100,13 @@ public abstract class MyTracksConstants { public static final int MENU_SHARE_GPX_FILE = 201; public static final int MENU_SHARE_KML_FILE = 202; public static final int MENU_SHARE_CSV_FILE = 203; - public static final int MENU_WRITE_TO_SD_CARD = 204; - public static final int MENU_SAVE_GPX_FILE = 205; - public static final int MENU_SAVE_KML_FILE = 206; - public static final int MENU_SAVE_CSV_FILE = 207; - public static final int MENU_CLEAR_MAP = 208; + public static final int MENU_SHARE_TCX_FILE = 204; + public static final int MENU_WRITE_TO_SD_CARD = 205; + public static final int MENU_SAVE_GPX_FILE = 206; + public static final int MENU_SAVE_KML_FILE = 207; + public static final int MENU_SAVE_CSV_FILE = 208; + public static final int MENU_SAVE_TCX_FILE = 209; + public static final int MENU_CLEAR_MAP = 210; /** * The number of distance readings to smooth to get a stable signal. @@ -123,6 +134,12 @@ public abstract class MyTracksConstants { */ public static final int MAX_DISPLAYED_TRACK_POINTS = 10000; + /** + * Target number of track points displayed by the map overlay. + * We may display more than this number of points. + */ + public static final int TARGET_DISPLAYED_TRACK_POINTS = 5000; + /** * Maximum number of track points ever loaded at once from the provider into * memory. @@ -179,12 +196,16 @@ public abstract class MyTracksConstants { return MyTracksConstants.SHARE_GPX_FILE; case MyTracksConstants.MENU_SHARE_CSV_FILE: return MyTracksConstants.SHARE_CSV_FILE; + case MyTracksConstants.MENU_SHARE_TCX_FILE: + return MyTracksConstants.SHARE_TCX_FILE; case MyTracksConstants.MENU_SAVE_GPX_FILE: return MyTracksConstants.SAVE_GPX_FILE; case MyTracksConstants.MENU_SAVE_KML_FILE: return MyTracksConstants.SAVE_KML_FILE; case MyTracksConstants.MENU_SAVE_CSV_FILE: return MyTracksConstants.SAVE_CSV_FILE; + case MyTracksConstants.MENU_SAVE_TCX_FILE: + return MyTracksConstants.SAVE_TCX_FILE; case MyTracksConstants.MENU_CLEAR_MAP: return MyTracksConstants.CLEAR_MAP; default: diff --git a/MyTracks/src/com/google/android/apps/mytracks/MyTracksDeleteAllTracks.java b/MyTracks/src/com/google/android/apps/mytracks/MyTracksDeleteAllTracks.java index 4762c840e..8a5f255f1 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/MyTracksDeleteAllTracks.java +++ b/MyTracks/src/com/google/android/apps/mytracks/MyTracksDeleteAllTracks.java @@ -62,7 +62,7 @@ public class MyTracksDeleteAllTracks extends Handler { SharedPreferences prefs = context.getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0); SharedPreferences.Editor editor = prefs.edit(); - editor.putLong(MyTracksSettings.SELECTED_TRACK, -1); + editor.putLong(context.getString(R.string.selected_track_key), -1); editor.commit(); if (done != null) { Handler h = new Handler(); diff --git a/MyTracks/src/com/google/android/apps/mytracks/MyTracksList.java b/MyTracks/src/com/google/android/apps/mytracks/MyTracksList.java index c2be23dce..f88421868 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/MyTracksList.java +++ b/MyTracks/src/com/google/android/apps/mytracks/MyTracksList.java @@ -90,6 +90,8 @@ public class MyTracksList extends ListActivity R.string.tracklist_share_kml_file); share.add(0, MyTracksConstants.MENU_SHARE_CSV_FILE, 0, R.string.tracklist_share_csv_file); + share.add(0, MyTracksConstants.MENU_SHARE_TCX_FILE, 0, + R.string.tracklist_share_tcx_file); SubMenu save = menu.addSubMenu(0, MyTracksConstants.MENU_WRITE_TO_SD_CARD, 0, R.string.tracklist_write_to_sd); @@ -99,6 +101,8 @@ public class MyTracksList extends ListActivity R.string.tracklist_save_as_kml); save.add(0, MyTracksConstants.MENU_SAVE_CSV_FILE, 0, R.string.tracklist_save_as_csv); + save.add(0, MyTracksConstants.MENU_SAVE_TCX_FILE, 0, + R.string.tracklist_save_as_tcx); menu.add(0, MyTracksConstants.MENU_DELETE, 0, R.string.tracklist_delete_track); } @@ -111,14 +115,16 @@ public class MyTracksList extends ListActivity if (key == null) { return; } - if (key.equals(MyTracksSettings.METRIC_UNITS)) { - metricUnits = sharedPreferences.getBoolean(MyTracksSettings.METRIC_UNITS, true); + if (key.equals(getString(R.string.metric_units_key))) { + metricUnits = sharedPreferences.getBoolean( + getString(R.string.metric_units_key), true); if (tracksCursor != null && !tracksCursor.isClosed()) { tracksCursor.requery(); } } - if (key.equals(MyTracksSettings.RECORDING_TRACK)) { - recordingTrackId = sharedPreferences.getLong(MyTracksSettings.RECORDING_TRACK, -1); + if (key.equals(getString(R.string.recording_track_key))) { + recordingTrackId = sharedPreferences.getLong( + getString(R.string.recording_track_key), -1); } } @@ -193,9 +199,10 @@ public class MyTracksList extends ListActivity SharedPreferences preferences = getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0); preferences.registerOnSharedPreferenceChangeListener(this); - metricUnits = preferences.getBoolean(MyTracksSettings.METRIC_UNITS, true); + metricUnits = + preferences.getBoolean(getString(R.string.metric_units_key), true); recordingTrackId = - preferences.getLong(MyTracksSettings.RECORDING_TRACK, -1); + preferences.getLong(getString(R.string.recording_track_key), -1); tracksCursor = getContentResolver().query( TracksColumns.CONTENT_URI, null, null, null, "_id DESC"); diff --git a/MyTracks/src/com/google/android/apps/mytracks/MyTracksMap.java b/MyTracks/src/com/google/android/apps/mytracks/MyTracksMap.java index c7b73f7d7..394471e43 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/MyTracksMap.java +++ b/MyTracks/src/com/google/android/apps/mytracks/MyTracksMap.java @@ -47,6 +47,7 @@ import android.os.Bundle; import android.os.Handler; import android.os.HandlerThread; import android.provider.Settings; +import android.speech.tts.TextToSpeech; import android.util.Log; import android.view.ContextMenu; import android.view.ContextMenu.ContextMenuInfo; @@ -136,6 +137,11 @@ public class MyTracksMap extends MapActivity private Handler updateTrackHandler; private MyTracksProviderUtils providerUtils; + + /** + * This value is used to decide how many points to display. + */ + private int samplingFrequency = 1; /** * A runnable that updates the track from the provider (looking for points @@ -147,35 +153,7 @@ public class MyTracksMap extends MapActivity if (selectedTrack == null) { return; } - Cursor cursor = null; - try { - cursor = providerUtils.getLocationsCursor( - recordingTrackId, - lastSeenLocationId + 1, - MyTracksConstants.MAX_DISPLAYED_TRACK_POINTS - - selectedTrack.getLocations().size(), - true); - if (cursor != null) { - if (cursor.moveToLast()) { - final int idColumnIdx = - cursor.getColumnIndexOrThrow(TrackPointsColumns._ID); - do { - lastSeenLocationId = cursor.getLong(idColumnIdx); - Location location = providerUtils.createLocation(cursor); - if (location != null) { - selectedTrack.addLocation(location); - } - } while (cursor.moveToPrevious()); - } - } - } catch (RuntimeException e) { - Log.w(MyTracksConstants.TAG, "Caught an unexpected exception: ", e); - } finally { - if (cursor != null) { - cursor.close(); - } - mapView.postInvalidate(); - } + readAllNewTrackPoints(); } }; @@ -188,9 +166,9 @@ public class MyTracksMap extends MapActivity if (selectedTrack == null) { return; } - lastSeenLocationId = providerUtils.getTrackPoints(selectedTrack, - MyTracksConstants.MAX_DISPLAYED_TRACK_POINTS); - mapView.postInvalidate(); + mapOverlay.clearPoints(); + lastSeenLocationId = selectedTrack.getStartId(); + readAllNewTrackPoints(); } }; @@ -302,6 +280,9 @@ public class MyTracksMap extends MapActivity Log.d(MyTracksConstants.TAG, "MyTracksMap.onCreate"); super.onCreate(bundle); + // The volume we want to control is the Text-To-Speech volume + setVolumeControlStream(TextToSpeech.Engine.DEFAULT_STREAM); + providerUtils = MyTracksProviderUtils.Factory.get(this); // We don't need a window title bar: @@ -380,12 +361,12 @@ public class MyTracksMap extends MapActivity getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0); if (preferences != null) { minRequiredAccuracy = preferences.getInt( - MyTracksSettings.MIN_REQUIRED_ACCURACY, + getString(R.string.min_required_accuracy_key), MyTracksSettings.DEFAULT_MIN_REQUIRED_ACCURACY); recordingTrackId = - preferences.getLong(MyTracksSettings.RECORDING_TRACK, -1); + preferences.getLong(getString(R.string.recording_track_key), -1); long selectedTrackId = - preferences.getLong(MyTracksSettings.SELECTED_TRACK, -1); + preferences.getLong(getString(R.string.selected_track_key), -1); if (selectedTrackId >= 0) { setSelectedTrack(selectedTrackId); } @@ -783,6 +764,8 @@ public class MyTracksMap extends MapActivity R.string.tracklist_share_kml_file); share.add(0, MyTracksConstants.MENU_SHARE_CSV_FILE, 0, R.string.tracklist_share_csv_file); + share.add(0, MyTracksConstants.MENU_SHARE_TCX_FILE, 0, + R.string.tracklist_share_tcx_file); SubMenu save = menu.addSubMenu(0, MyTracksConstants.MENU_WRITE_TO_SD_CARD, 0, R.string.tracklist_write_to_sd); @@ -792,6 +775,8 @@ public class MyTracksMap extends MapActivity R.string.tracklist_save_as_kml); save.add(0, MyTracksConstants.MENU_SAVE_CSV_FILE, 0, R.string.tracklist_save_as_csv); + save.add(0, MyTracksConstants.MENU_SAVE_TCX_FILE, 0, + R.string.tracklist_save_as_tcx); menu.add(0, MyTracksConstants.MENU_CLEAR_MAP, 0, R.string.tracklist_clear_map); menu.add(0, MyTracksConstants.MENU_DELETE, 0, @@ -885,26 +870,36 @@ public class MyTracksMap extends MapActivity } @Override - public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, - String key) { + public void onSharedPreferenceChanged( + final SharedPreferences sharedPreferences, + final String key) { if (key != null) { - if (key.equals(MyTracksSettings.MIN_REQUIRED_ACCURACY)) { - minRequiredAccuracy = sharedPreferences.getInt( - MyTracksSettings.MIN_REQUIRED_ACCURACY, - MyTracksSettings.DEFAULT_MIN_REQUIRED_ACCURACY); - } else if (key.equals(MyTracksSettings.SELECTED_TRACK)) { - long selectedTrackId = - sharedPreferences.getLong(MyTracksSettings.SELECTED_TRACK, -1); - setSelectedTrack(selectedTrackId); - } else if (key.equals(MyTracksSettings.RECORDING_TRACK)) { - recordingTrackId = - sharedPreferences.getLong(MyTracksSettings.RECORDING_TRACK, -1); - if (selectedTrack != null) { - mapOverlay.setShowEndMarker( - selectedTrack.getId() != recordingTrackId); - mapView.postInvalidate(); + uiHandler.post(new Runnable() { + @Override + public void run() { + if (key.equals(getString(R.string.min_required_accuracy_key))) { + minRequiredAccuracy = sharedPreferences.getInt( + getString(R.string.min_required_accuracy_key), + MyTracksSettings.DEFAULT_MIN_REQUIRED_ACCURACY); + } else if (key.equals(getString(R.string.selected_track_key))) { + long selectedTrackId = + sharedPreferences.getLong( + getString(R.string.selected_track_key), + -1); + setSelectedTrack(selectedTrackId); + } else if (key.equals(getString(R.string.recording_track_key))) { + recordingTrackId = + sharedPreferences.getLong( + getString(R.string.recording_track_key), + -1); + if (selectedTrack != null) { + mapOverlay.setShowEndMarker( + selectedTrack.getId() != recordingTrackId); + mapView.postInvalidate(); + } + } } - } + }); } } @@ -977,4 +972,63 @@ public class MyTracksMap extends MapActivity public void onAccuracyChanged(Sensor s, int accuracy) { // do nothing } + + /** + * Set the sampling frequency from the total number of points in the track. + * + * @param track The track to read the total number of points from. + */ + private void setSamplingFrequency(Track track) { + long totalLocations = track.getStopId() - track.getStartId(); + + // Limit the number of map points readings. + samplingFrequency = + (int) (1 + + totalLocations / MyTracksConstants.TARGET_DISPLAYED_TRACK_POINTS); + Log.i(MyTracksConstants.TAG, "Sampling locations: " + samplingFrequency); + } + + private void readAllNewTrackPoints() { + Cursor cursor = null; + // Refetch the track to get the latest StopId + selectedTrack = providerUtils.getTrack(selectedTrack.getId()); + long totalLocations = selectedTrack.getStopId() - + selectedTrack.getStartId(); + + setSamplingFrequency(selectedTrack); + int bufferSize = 1024; + int points = 0; + while (lastSeenLocationId < selectedTrack.getStopId()) { + cursor = providerUtils.getLocationsCursor( + selectedTrack.getId(), lastSeenLocationId, bufferSize, false); + if (cursor != null && cursor.moveToFirst()) { + final int idColumnIdx = cursor.getColumnIndexOrThrow( + TrackPointsColumns._ID); + while (cursor.moveToNext()) { + points++; + Location location = providerUtils.createLocation(cursor); + lastSeenLocationId = cursor.getLong(idColumnIdx); + // Include a point if it fits one of the following criteria: + // - Has the mod for the sampling frequency. + // - Is the first point. + // - Is the last point and we are not recording this track. + if (!MyTracksUtils.isValidLocation(location) || + points % samplingFrequency == 0 || + points == 0 || + (recordingTrackId != selectedTrack.getId() && + points == (totalLocations - 1))) { + mapOverlay.addLocation(location); + } + } + } else { + lastSeenLocationId += bufferSize; + } + cursor.close(); + cursor = null; + } + if (cursor != null) { + cursor.close(); + } + mapView.postInvalidate(); + } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/MyTracksOverlay.java b/MyTracks/src/com/google/android/apps/mytracks/MyTracksOverlay.java index fabd7555f..8441dabc7 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/MyTracksOverlay.java +++ b/MyTracks/src/com/google/android/apps/mytracks/MyTracksOverlay.java @@ -1,12 +1,12 @@ /* * 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 @@ -55,6 +55,7 @@ public class MyTracksOverlay extends Overlay { private final Paint errorCirclePaint; private final Context context; private final ArrayList waypoints = new ArrayList(); + private final ArrayList points = new ArrayList(1024); private Track selectedTrack; private int lastHeading = 0; @@ -128,6 +129,17 @@ public class MyTracksOverlay extends Overlay { return selectedTrack; } + /** + * Add a location to the map overlay. + * + * NOTE: this method takes ownership of this location and may change it. + * + * @param l the location to add + */ + public void addLocation(Location l) { + points.add(l); + } + public void addWaypoint(Waypoint wpt) { waypoints.add(wpt); } @@ -136,6 +148,10 @@ public class MyTracksOverlay extends Overlay { waypoints.clear(); } + public void clearPoints() { + points.clear(); + } + public void setShowEndMarker(boolean showEndMarker) { this.showEndMarker = showEndMarker; } @@ -153,7 +169,11 @@ public class MyTracksOverlay extends Overlay { // Draw the waypoints: ArrayList currentWaypoints = waypoints; for (int i = 1; i < currentWaypoints.size(); i++) { - Location loc = currentWaypoints.get(i).getLocation(); + Waypoint wpt = currentWaypoints.get(i); + if (wpt == null) { + continue; + } + Location loc = wpt.getLocation(); if (loc == null) { continue; } @@ -162,7 +182,7 @@ public class MyTracksOverlay extends Overlay { mapView.getProjection().toPixels(geoPoint, pt); canvas.save(); canvas.translate(pt.x - (markerWidth / 2) + 3, pt.y - (markerHeight)); - if (currentWaypoints.get(i).getType() == Waypoint.TYPE_STATISTICS) { + if (wpt.getType() == Waypoint.TYPE_STATISTICS) { statsMarker.draw(canvas); } else { waypointMarker.draw(canvas); @@ -196,7 +216,6 @@ public class MyTracksOverlay extends Overlay { if (track == null) { return; } - ArrayList points = track.getLocations(); if (points.size() < 2) { return; } @@ -206,7 +225,7 @@ public class MyTracksOverlay extends Overlay { int h = mapView.getLatitudeSpan(); int cx = mapView.getMapCenter().getLongitudeE6(); int cy = mapView.getMapCenter().getLatitudeE6(); - Rect rect = new Rect(cx - w / 2, cy - h / 2, cx + w / 2, cy + h / 2); + Rect rect = new Rect(cx - w, cy - h, cx + w, cy + h); Point pt = new Point(); GeoPoint geoPoint; @@ -222,14 +241,6 @@ public class MyTracksOverlay extends Overlay { int lastLocLon = (int) (points.get(0).getLongitude() * 1E6); int lastLocLat = (int) (points.get(0).getLatitude() * 1E6); - // Line decimation for dummies: - // Skip di additional points, where di depends on zoom level: - int di = 0; - int dl = 17 - mapView.getZoomLevel(); - if (dl > 0) { - di += (dl * dl); - } - // Loop over track points: path = new Path(); for (int i = 1; i < points.size(); i++) { @@ -241,23 +252,6 @@ public class MyTracksOverlay extends Overlay { locLon = (int) (loc.getLongitude() * 1E6); locLat = (int) (loc.getLatitude() * 1E6); - // Skip to every n-th point (depends on zoom level, see above): - for (int j = 0; j < di && i < points.size() - 1; j++) { - // TODO Check the thread synchronization. - // There is no reason that points.get(i + 1) should be null but users - // have reported it causing null pointer exceptions. - if ((locLat > 90E6) || - (lastLocLat > 90E6) || - (points.get(i + 1) == null) || - (points.get(i + 1).getLatitude() > 90)) { - break; - } - i++; - loc = points.get(i); - locLon = (int) (loc.getLongitude() * 1E6); - locLat = (int) (loc.getLatitude() * 1E6); - } - // Draw a line segment if it's inside the viewing window: if (locLat < 90E6 && lastLocLat < 90E6) { lastValidLocation = loc; diff --git a/MyTracks/src/com/google/android/apps/mytracks/MyTracksSettings.java b/MyTracks/src/com/google/android/apps/mytracks/MyTracksSettings.java index 776b5612f..93373380f 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/MyTracksSettings.java +++ b/MyTracks/src/com/google/android/apps/mytracks/MyTracksSettings.java @@ -1,12 +1,12 @@ /* * 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 @@ -15,16 +15,21 @@ */ package com.google.android.apps.mytracks; +import com.google.android.apps.mytracks.io.backup.BackupActivityHelper; +import com.google.android.apps.mytracks.io.backup.BackupPreferencesListener; import com.google.android.apps.mytracks.services.SafeStatusAnnouncerTask; import com.google.android.maps.mytracks.R; +import android.content.SharedPreferences; import android.os.Bundle; import android.preference.CheckBoxPreference; import android.preference.ListPreference; import android.preference.Preference; import android.preference.Preference.OnPreferenceChangeListener; +import android.preference.Preference.OnPreferenceClickListener; import android.preference.PreferenceActivity; import android.preference.PreferenceManager; +import android.speech.tts.TextToSpeech; import android.util.Log; /** @@ -37,26 +42,6 @@ public class MyTracksSettings extends PreferenceActivity { public static final String SETTINGS_NAME = "MyTracksSettings"; - public static final String ANNOUNCEMENT_FREQUENCY = "announcementFrequency"; - public static final String DEFAULT_MAP_PUBLIC = "defaultMapPublic"; - public static final String MAX_RECORDING_DISTANCE = "maxRecordingDistance"; - public static final String METRIC_UNITS = "metricUnits"; - public static final String MIN_RECORDING_DISTANCE = "minRecordingDistance"; - public static final String MIN_RECORDING_INTERVAL = "minRecordingInterval"; - public static final String MIN_REQUIRED_ACCURACY = "minRequiredAccuracy"; - public static final String PICK_EXISTING_MAP = "pickExistingMap"; - public static final String RECORDING_TRACK = "recordingTrack"; - public static final String REPORT_SPEED = "reportSpeed"; - public static final String SELECTED_TRACK = "selectedTrack"; - public static final String SEND_STATS_AND_POINTS = "sendStatsAndPoints"; - public static final String SEND_TO_DOCS = "sendToDocs"; - public static final String SEND_TO_MYMAPS = "sendToMyMaps"; - public static final String SHARE_URL_ONLY = "shareUrlOnly"; - public static final String SIGNAL_SAMPLING_FREQUENCY = - "signalSamplingFrequency"; - public static final String SPLIT_FREQUENCY = "splitFrequency"; - public static final String WRITE_TO_SD_CARD = "writeToSdCard"; - /* * Default values - keep in sync with those in preferences.xml. */ @@ -68,6 +53,9 @@ public class MyTracksSettings extends PreferenceActivity { public static final int DEFAULT_SPLIT_FREQUENCY = 0; private static boolean mTTSAvailable; + private BackupPreferencesListener backupListener; + + private SharedPreferences preferences; /* establish whether the tts class is available to us */ static { @@ -85,18 +73,27 @@ public class MyTracksSettings extends PreferenceActivity { public void onCreate(Bundle icicle) { super.onCreate(icicle); + // The volume we want to control is the Text-To-Speech volume + setVolumeControlStream(TextToSpeech.Engine.DEFAULT_STREAM); + // Tell it where to read/write preferences PreferenceManager preferenceManager = getPreferenceManager(); preferenceManager.setSharedPreferencesName(SETTINGS_NAME); preferenceManager.setSharedPreferencesMode(0); + // Set up automatic preferences backup + backupListener = BackupPreferencesListener.create(this); + preferences = preferenceManager.getSharedPreferences(); + preferences.registerOnSharedPreferenceChangeListener(backupListener); + // Load the preferences to be displayed addPreferencesFromResource(R.xml.preferences); // Hook up switching of displayed list entries between metric and imperial // units CheckBoxPreference metricUnitsPreference = - (CheckBoxPreference) findPreference(METRIC_UNITS); + (CheckBoxPreference) findPreference( + getString(R.string.metric_units_key)); metricUnitsPreference.setOnPreferenceChangeListener( new OnPreferenceChangeListener() { @Override @@ -112,7 +109,8 @@ public class MyTracksSettings extends PreferenceActivity { // Disable TTS announcement preference if not available if (!mTTSAvailable) { IntegerListPreference announcementFrequency = - (IntegerListPreference) findPreference(ANNOUNCEMENT_FREQUENCY); + (IntegerListPreference) findPreference( + getString(R.string.announcement_frequency_key)); announcementFrequency.setEnabled(false); announcementFrequency.setValue("-1"); announcementFrequency.setSummary( @@ -120,6 +118,59 @@ public class MyTracksSettings extends PreferenceActivity { } } + @Override + protected void onResume() { + super.onResume(); + + Preference backupNowPreference = + findPreference(getString(R.string.backup_to_sd_key)); + Preference restoreNowPreference = + findPreference(getString(R.string.restore_from_sd_key)); + + // If recording, disable backup/restore + // (we don't want to get to inconsistent states) + boolean recording = + preferences.getLong(getString(R.string.recording_track_key), -1) != -1; + backupNowPreference.setEnabled(!recording); + restoreNowPreference.setEnabled(!recording); + backupNowPreference.setSummary( + recording ? R.string.settings_no_backup_while_recording + : R.string.settings_backup_to_sd_summary); + restoreNowPreference.setSummary( + recording ? R.string.settings_no_backup_while_recording + : R.string.settings_restore_from_sd_summary); + + // Add actions to the backup preferences + backupNowPreference.setOnPreferenceClickListener( + new OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + BackupActivityHelper backupHelper = + new BackupActivityHelper(MyTracksSettings.this); + backupHelper.writeBackup(); + return true; + } + }); + restoreNowPreference.setOnPreferenceClickListener( + new OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + BackupActivityHelper backupHelper = + new BackupActivityHelper(MyTracksSettings.this); + backupHelper.restoreBackup(); + return true; + } + }); + } + + @Override + protected void onDestroy() { + getPreferenceManager().getSharedPreferences() + .unregisterOnSharedPreferenceChangeListener(backupListener); + + super.onPause(); + } + /** * Updates all the preferences which give options with distance units to use * the proper unit the user has selected. @@ -128,13 +179,17 @@ public class MyTracksSettings extends PreferenceActivity { */ private void updatePreferenceUnits(boolean isMetric) { final ListPreference minRecordingDistance = - (ListPreference) findPreference(MIN_RECORDING_DISTANCE); + (ListPreference) findPreference( + getString(R.string.min_recording_distance_key)); final ListPreference maxRecordingDistance = - (ListPreference) findPreference(MAX_RECORDING_DISTANCE); + (ListPreference) findPreference( + getString(R.string.max_recording_distance_key)); final ListPreference minRequiredAccuracy = - (ListPreference) findPreference(MIN_REQUIRED_ACCURACY); + (ListPreference) findPreference( + getString(R.string.min_required_accuracy_key)); final ListPreference splitFrequency = - (ListPreference) findPreference(SPLIT_FREQUENCY); + (ListPreference) findPreference( + getString(R.string.split_frequency_key)); minRecordingDistance.setEntries(isMetric ? R.array.min_recording_distance_options diff --git a/MyTracks/src/com/google/android/apps/mytracks/MyTracksWaypointDetails.java b/MyTracks/src/com/google/android/apps/mytracks/MyTracksWaypointDetails.java index 33b911762..82604320b 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/MyTracksWaypointDetails.java +++ b/MyTracks/src/com/google/android/apps/mytracks/MyTracksWaypointDetails.java @@ -66,11 +66,11 @@ public class MyTracksWaypointDetails extends Activity getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0); if (preferences != null) { boolean useMetric = - preferences.getBoolean(MyTracksSettings.METRIC_UNITS, true); + preferences.getBoolean(getString(R.string.metric_units_key), true); utils.setMetricUnits(useMetric); boolean displaySpeed = - preferences.getBoolean(MyTracksSettings.REPORT_SPEED, true); + preferences.getBoolean(getString(R.string.report_speed_key), true); utils.setReportSpeed(displaySpeed); utils.updateWaypointUnits(); diff --git a/MyTracks/src/com/google/android/apps/mytracks/MyTracksWaypointsList.java b/MyTracks/src/com/google/android/apps/mytracks/MyTracksWaypointsList.java index 0e9d6040d..594c9fb98 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/MyTracksWaypointsList.java +++ b/MyTracks/src/com/google/android/apps/mytracks/MyTracksWaypointsList.java @@ -139,9 +139,9 @@ public class MyTracksWaypointsList extends ListActivity getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0); if (preferences != null) { recordingTrackId = - preferences.getLong(MyTracksSettings.RECORDING_TRACK, -1); + preferences.getLong(getString(R.string.recording_track_key), -1); selectedTrackId = - preferences.getLong(MyTracksSettings.SELECTED_TRACK, -1); + preferences.getLong(getString(R.string.selected_track_key), -1); } boolean selectedRecording = selectedTrackId > 0 && selectedTrackId == recordingTrackId; diff --git a/MyTracks/src/com/google/android/apps/mytracks/SendToGoogleDialog.java b/MyTracks/src/com/google/android/apps/mytracks/SendToGoogleDialog.java index 8e7847cb0..3f6ef6da9 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/SendToGoogleDialog.java +++ b/MyTracks/src/com/google/android/apps/mytracks/SendToGoogleDialog.java @@ -105,17 +105,25 @@ public class SendToGoogleDialog extends Dialog { getContext().getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0); if (prefs != null) { sendToMyMapsCheckBox.setChecked( - prefs.getBoolean(MyTracksSettings.SEND_TO_MYMAPS, true)); + prefs.getBoolean( + getContext().getString(R.string.send_to_my_maps_key), true)); sendToDocsCheckBox.setChecked( - prefs.getBoolean(MyTracksSettings.SEND_TO_DOCS, true)); + prefs.getBoolean( + getContext().getString(R.string.send_to_docs_key), true)); pickMapRadioButton.setChecked( - prefs.getBoolean(MyTracksSettings.PICK_EXISTING_MAP, false)); + prefs.getBoolean( + getContext().getString(R.string.pick_existing_map_key), false)); createNewMapRadioButton.setChecked( - !prefs.getBoolean(MyTracksSettings.PICK_EXISTING_MAP, false)); + !prefs.getBoolean( + getContext().getString(R.string.pick_existing_map_key), false)); sendStatsAndPointsRadioButton.setChecked( - prefs.getBoolean(MyTracksSettings.SEND_STATS_AND_POINTS, false)); + prefs.getBoolean( + getContext().getString(R.string.send_stats_and_points_key), + false)); sendStatsRadioButton.setChecked( - !prefs.getBoolean(MyTracksSettings.SEND_STATS_AND_POINTS, false)); + !prefs.getBoolean( + getContext().getString(R.string.send_stats_and_points_key), + false)); } } @@ -127,13 +135,16 @@ public class SendToGoogleDialog extends Dialog { Editor editor = prefs.edit(); if (editor != null) { editor.putBoolean( - MyTracksSettings.SEND_TO_MYMAPS, sendToMyMapsCheckBox.isChecked()); + getContext().getString(R.string.send_to_my_maps_key), + sendToMyMapsCheckBox.isChecked()); editor.putBoolean( - MyTracksSettings.SEND_TO_DOCS, sendToDocsCheckBox.isChecked()); + getContext().getString(R.string.send_to_docs_key), + sendToDocsCheckBox.isChecked()); editor.putBoolean( - MyTracksSettings.PICK_EXISTING_MAP, pickMapRadioButton.isChecked()); + getContext().getString(R.string.pick_existing_map_key), + pickMapRadioButton.isChecked()); editor.putBoolean( - MyTracksSettings.SEND_STATS_AND_POINTS, + getContext().getString(R.string.send_stats_and_points_key), sendStatsAndPointsRadioButton.isChecked()); editor.commit(); } diff --git a/MyTracks/src/com/google/android/apps/mytracks/StatsActivity.java b/MyTracks/src/com/google/android/apps/mytracks/StatsActivity.java index c16bb69fa..1432888f5 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/StatsActivity.java +++ b/MyTracks/src/com/google/android/apps/mytracks/StatsActivity.java @@ -1,12 +1,12 @@ /* * 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 @@ -15,6 +15,9 @@ */ package com.google.android.apps.mytracks; +import com.google.android.apps.mytracks.content.MyTracksProviderUtils; +import com.google.android.apps.mytracks.content.MyTracksProviderUtilsImpl; +import com.google.android.apps.mytracks.content.Track; import com.google.android.apps.mytracks.content.TracksColumns; import com.google.android.apps.mytracks.content.WaypointsColumns; import com.google.android.maps.mytracks.R; @@ -24,13 +27,13 @@ import android.content.Context; import android.content.SharedPreferences; import android.content.SharedPreferences.OnSharedPreferenceChangeListener; import android.database.ContentObserver; -import android.database.Cursor; import android.location.Location; import android.location.LocationListener; import android.location.LocationManager; import android.location.LocationProvider; import android.os.Bundle; import android.os.Handler; +import android.speech.tts.TextToSpeech; import android.util.DisplayMetrics; import android.util.Log; import android.view.Menu; @@ -48,23 +51,6 @@ import android.widget.Toast; public class StatsActivity extends Activity implements OnSharedPreferenceChangeListener, LocationListener { - /** - * Columns we need from the provider - */ - private static final String[] PROJECTION_COLUMNS = { - TracksColumns._ID, - TracksColumns.STARTTIME, - TracksColumns.TOTALTIME, - TracksColumns.MOVINGTIME, - TracksColumns.TOTALDISTANCE, - TracksColumns.AVGSPEED, - TracksColumns.AVGMOVINGSPEED, - TracksColumns.MAXSPEED, - TracksColumns.MINELEVATION, - TracksColumns.MAXELEVATION, - TracksColumns.ELEVATIONGAIN, - TracksColumns.MINGRADE, - TracksColumns.MAXGRADE }; private final StatsUtilities utils; private UIUpdateThread thread; @@ -109,6 +95,9 @@ public class StatsActivity extends Activity */ private boolean showCurrentSegment = false; + private final MyTracksProviderUtils providerUtils; + private Track track = null; + /** * A runnable for posting to the UI thread. Will update the total time field. */ @@ -147,7 +136,8 @@ public class StatsActivity extends Activity } public StatsActivity() { - this.utils = new StatsUtilities(this); + utils = new StatsUtilities(this); + providerUtils = new MyTracksProviderUtilsImpl(this); } /** Called when the activity is first created. */ @@ -155,6 +145,9 @@ public class StatsActivity extends Activity public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + // The volume we want to control is the Text-To-Speech volume + setVolumeControlStream(TextToSpeech.Engine.DEFAULT_STREAM); + // We don't need a window title bar: requestWindowFeature(Window.FEATURE_NO_TITLE); @@ -176,20 +169,20 @@ public class StatsActivity extends Activity SharedPreferences preferences = getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0); if (preferences != null) { - selectedTrackId = preferences.getLong(MyTracksSettings.SELECTED_TRACK, - -1); - recordingTrackId = preferences.getLong(MyTracksSettings.RECORDING_TRACK, - -1); - metricUnits = preferences.getBoolean(MyTracksSettings.METRIC_UNITS, - true); + selectedTrackId = preferences.getLong( + getString(R.string.selected_track_key), -1); + recordingTrackId = preferences.getLong( + getString(R.string.recording_track_key), -1); + metricUnits = preferences.getBoolean( + getString(R.string.metric_units_key), true); displaySpeed = - preferences.getBoolean(MyTracksSettings.REPORT_SPEED, true); + preferences.getBoolean(getString(R.string.report_speed_key), true); checkLiveTrack(); restoreStats(); updateLocation(null); preferences.registerOnSharedPreferenceChangeListener(this); } - locationManager = + locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE); utils.setMetricUnits(metricUnits); utils.setReportSpeed(displaySpeed); @@ -227,39 +220,51 @@ public class StatsActivity extends Activity @Override public void onSharedPreferenceChanged( - SharedPreferences sharedPreferences, String key) { + final SharedPreferences sharedPreferences, final String key) { Log.d(MyTracksConstants.TAG, "StatsActivity: onSharedPreferences changed " + key); if (key != null) { - if (key.equals(MyTracksSettings.SELECTED_TRACK)) { - selectedTrackId = - sharedPreferences.getLong(MyTracksSettings.SELECTED_TRACK, -1); - checkLiveTrack(); - restoreStats(); - updateLocation(null); - } else if (key.equals(MyTracksSettings.RECORDING_TRACK)) { - recordingTrackId = - sharedPreferences.getLong(MyTracksSettings.RECORDING_TRACK, -1); - checkLiveTrack(); - restoreStats(); - updateLocation(null); - } else if (key.equals(MyTracksSettings.METRIC_UNITS)) { - metricUnits = - sharedPreferences.getBoolean(MyTracksSettings.METRIC_UNITS, true); - utils.setMetricUnits(metricUnits); - utils.updateUnits(); - restoreStats(); - } else if (key.equals(MyTracksSettings.REPORT_SPEED)) { - displaySpeed = - sharedPreferences.getBoolean(MyTracksSettings.REPORT_SPEED, true); - utils.setReportSpeed(displaySpeed); - utils.updateUnits(); - utils.setSpeedLabel( - R.id.speed_label, R.string.speed, R.string.pace_label); - Log.w(MyTracksConstants.TAG, "Setting speed labels"); - utils.setSpeedLabels(); - restoreStats(); - } + runOnUiThread(new Runnable() { + @Override + public void run() { + if (key.equals(getString(R.string.selected_track_key))) { + selectedTrackId = + sharedPreferences.getLong( + getString(R.string.selected_track_key), + -1); + checkLiveTrack(); + restoreStats(); + updateLocation(null); + } else if (key.equals(getString(R.string.recording_track_key))) { + recordingTrackId = + sharedPreferences.getLong( + getString(R.string.recording_track_key), + -1); + checkLiveTrack(); + restoreStats(); + updateLocation(null); + } else if (key.equals(getString(R.string.metric_units_key))) { + metricUnits = + sharedPreferences.getBoolean( + getString(R.string.metric_units_key), true); + utils.setMetricUnits(metricUnits); + utils.updateUnits(); + restoreStats(); + } else if (key.equals(getString(R.string.report_speed_key))) { + displaySpeed = + sharedPreferences.getBoolean( + getString(R.string.report_speed_key), + true); + utils.setReportSpeed(displaySpeed); + utils.updateUnits(); + utils.setSpeedLabel( + R.id.speed_label, R.string.speed, R.string.pace_label); + Log.w(MyTracksConstants.TAG, "Setting speed labels"); + utils.setSpeedLabels(); + restoreStats(); + } + } + }); } } @@ -359,60 +364,18 @@ public class StatsActivity extends Activity return; } - Cursor cursor = null; - try { - cursor = getContentResolver().query( - showCurrentSegment - ? WaypointsColumns.CONTENT_URI - : TracksColumns.CONTENT_URI, - PROJECTION_COLUMNS, - showCurrentSegment - ? WaypointsColumns.TRACKID + "=" + selectedTrackId - : "_id=" + selectedTrackId, - null, - "_id LIMIT 1"); - - if (cursor != null && cursor.moveToFirst()) { - final int idxStartTime = - cursor.getColumnIndexOrThrow(TracksColumns.STARTTIME); - final int idxTotalTime = - cursor.getColumnIndexOrThrow(TracksColumns.TOTALTIME); - final int idxMovingTime = - cursor.getColumnIndexOrThrow(TracksColumns.MOVINGTIME); - final int idxTotalDistance = - cursor.getColumnIndexOrThrow(TracksColumns.TOTALDISTANCE); - final int idxAverageSpeed = - cursor.getColumnIndexOrThrow(TracksColumns.AVGSPEED); - final int idxAverageMovingSpeed = - cursor.getColumnIndexOrThrow(TracksColumns.AVGMOVINGSPEED); - final int idxMaxSpeed = - cursor.getColumnIndexOrThrow(TracksColumns.MAXSPEED); - final int idxMinElevation = - cursor.getColumnIndexOrThrow(TracksColumns.MINELEVATION); - final int idxMaxElevation = - cursor.getColumnIndexOrThrow(TracksColumns.MAXELEVATION); - final int idxElevationGain = - cursor.getColumnIndexOrThrow(TracksColumns.ELEVATIONGAIN); - final int idxMinGrade = - cursor.getColumnIndexOrThrow(TracksColumns.MINGRADE); - final int idxMaxGrade = - cursor.getColumnIndexOrThrow(TracksColumns.MAXGRADE); - startTime = cursor.getLong(idxStartTime); - if (!selectedTrackIsRecording()) { - utils.setTime(R.id.total_time_register, cursor.getLong(idxTotalTime)); - } - utils.setAllStats(cursor.getLong(idxMovingTime), - cursor.getFloat(idxTotalDistance), cursor.getFloat(idxAverageSpeed), - cursor.getFloat(idxAverageMovingSpeed), - cursor.getFloat(idxMaxSpeed), cursor.getFloat(idxMinElevation), - cursor.getFloat(idxMaxElevation), cursor.getFloat(idxElevationGain), - cursor.getFloat(idxMinGrade), cursor.getFloat(idxMaxGrade)); - } - } finally { - if (cursor != null) { - cursor.close(); - } + track = providerUtils.getTrack(selectedTrackId); + if (track == null || track.getStatistics() == null) { + utils.setAllToUnknown(); + return; } + + startTime = track.getStatistics().getStartTime(); + if (!selectedTrackIsRecording()) { + utils.setTime(R.id.total_time_register, + track.getStatistics().getTotalTime()); + } + utils.setAllStats(track.getStatistics()); } /** diff --git a/MyTracks/src/com/google/android/apps/mytracks/WelcomeActivity.java b/MyTracks/src/com/google/android/apps/mytracks/WelcomeActivity.java index 4b717cc9a..42edd66f6 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/WelcomeActivity.java +++ b/MyTracks/src/com/google/android/apps/mytracks/WelcomeActivity.java @@ -19,12 +19,17 @@ import com.google.android.maps.mytracks.R; import android.app.Activity; import android.app.AlertDialog; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; import android.os.Bundle; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.View.OnClickListener; import android.view.Window; import android.webkit.WebView; +import android.widget.TextView; /** * An activity that displays a welcome screen. @@ -72,5 +77,16 @@ public class WelcomeActivity extends Activity { builder.setIcon(R.drawable.arrow_icon); AlertDialog dialog = builder.create(); dialog.show(); + + // Get the version from the manifest. + try { + PackageInfo pi = + getPackageManager().getPackageInfo("com.google.android.maps.mytracks", + PackageManager.GET_META_DATA); + ((TextView) dialog.findViewById(R.id.about_version_register)). + setText(pi.versionName); + } catch (NameNotFoundException e) { + Log.w(MyTracksConstants.TAG, "Failed to get version info.", e); + } } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/content/ContentTypeIds.java b/MyTracks/src/com/google/android/apps/mytracks/content/ContentTypeIds.java new file mode 100644 index 000000000..d1db14815 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/content/ContentTypeIds.java @@ -0,0 +1,32 @@ +/* + * 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.content; + +/** + * Utilities for serializing primitive types. + * + * @author Rodrigo Damazio + */ +public class ContentTypeIds { + public static final byte BOOLEAN_TYPE_ID = 0; + public static final byte LONG_TYPE_ID = 1; + public static final byte INT_TYPE_ID = 2; + public static final byte FLOAT_TYPE_ID = 3; + public static final byte DOUBLE_TYPE_ID = 4; + public static final byte STRING_TYPE_ID = 5; + + private ContentTypeIds() { /* Not instantiable */ } +} 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..b71e8440a 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/content/MyTracksProviderUtils.java +++ b/MyTracks/src/com/google/android/apps/mytracks/content/MyTracksProviderUtils.java @@ -15,6 +15,7 @@ */ package com.google.android.apps.mytracks.content; +import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.location.Location; @@ -232,6 +233,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 +252,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. * @@ -264,13 +268,31 @@ public interface MyTracksProviderUtils { */ void updateTrack(Track track); + /** + * Creates a Track object from a given cursor. + * + * @param cursor a cursor pointing at a db or provider with tracks + * @return a new Track object + */ + Track createTrack(Cursor cursor); + + /** + * Creates the ContentValues for a given Track object. + * + * Note: If the track has an id<0 the id column will not be filled. + * + * @param track a given track object + * @return a filled in ContentValues object + */ + ContentValues createContentValues(Track track); + /** * Creates a location object from a given cursor. * * @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 +300,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 42595ee16..e0e940bbe 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/content/MyTracksProviderUtilsImpl.java +++ b/MyTracks/src/com/google/android/apps/mytracks/content/MyTracksProviderUtilsImpl.java @@ -74,15 +74,8 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { return values; } - /** - * Creates the ContentValues for a given Track object. - * - * Note: If the track has an id<0 the id column will not be filled. - * - * @param track a given track object - * @return a filled in ContentValues object - */ - private static ContentValues createContentValues(Track track) { + @Override + public ContentValues createContentValues(Track track) { ContentValues values = new ContentValues(); TripStatistics stats = track.getStatistics(); @@ -209,13 +202,8 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { return location; } - /** - * Creates a Track object from a given cursor. - * - * @param cursor a cursor pointing at a db or provider with tracks - * @return a new Track object - */ - private static Track createTrack(Cursor cursor) { + @Override + public Track createTrack(Cursor cursor) { int idxId = cursor.getColumnIndexOrThrow(TracksColumns._ID); int idxName = cursor.getColumnIndexOrThrow(TracksColumns.NAME); int idxDescription = @@ -840,8 +828,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,13 +846,16 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { return; } - while (cursor.moveToNext()) { + final int idColumnIdx = + cursor.getColumnIndexOrThrow(TrackPointsColumns._ID); + do { Location location = createLocation(cursor); if (location == null) { continue; } buffer.add(location, cursor.getLong(idColumnIdx)); - } + } while (cursor.moveToNext()); + if (buffer.getLocationsLoaded() == 0) { Log.w(MyTracksProvider.TAG, "No locations read."); buffer.resetAt(startingPoint + buffer.getSize()); @@ -899,6 +888,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"); @@ -907,31 +908,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/content/TrackPointsColumns.java b/MyTracks/src/com/google/android/apps/mytracks/content/TrackPointsColumns.java index 206fd0eb8..36701db40 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/content/TrackPointsColumns.java +++ b/MyTracks/src/com/google/android/apps/mytracks/content/TrackPointsColumns.java @@ -15,6 +15,8 @@ */ package com.google.android.apps.mytracks.content; +import static com.google.android.apps.mytracks.content.ContentTypeIds.*; + import android.net.Uri; import android.provider.BaseColumns; @@ -42,4 +44,14 @@ public interface TrackPointsColumns extends BaseColumns { public static final String TIME = "time"; public static final String ACCURACY = "accuracy"; public static final String SPEED = "speed"; + + /** Columns that go into the backup. */ + public static final String[] BACKUP_COLUMNS = + { _ID, TRACKID, LATITUDE, LONGITUDE, ALTITUDE, BEARING, TIME, + ACCURACY, SPEED }; + + /** Types of the columns that go into the backup. */ + public static final byte[] BACKUP_COLUMN_TYPES = + { LONG_TYPE_ID, LONG_TYPE_ID, INT_TYPE_ID, INT_TYPE_ID, FLOAT_TYPE_ID, + FLOAT_TYPE_ID, LONG_TYPE_ID, FLOAT_TYPE_ID, FLOAT_TYPE_ID }; } diff --git a/MyTracks/src/com/google/android/apps/mytracks/content/TracksColumns.java b/MyTracks/src/com/google/android/apps/mytracks/content/TracksColumns.java index 6fb122b89..5e22926c9 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/content/TracksColumns.java +++ b/MyTracks/src/com/google/android/apps/mytracks/content/TracksColumns.java @@ -15,6 +15,8 @@ */ package com.google.android.apps.mytracks.content; +import static com.google.android.apps.mytracks.content.ContentTypeIds.*; + import android.net.Uri; import android.provider.BaseColumns; @@ -59,4 +61,16 @@ public interface TracksColumns extends BaseColumns { public static final String MINLON = "minlon"; public static final String MAXLON = "maxlon"; public static final String MAPID = "mapid"; + + public static final String[] BACKUP_COLUMNS = { + _ID, NAME, DESCRIPTION, CATEGORY, STARTID, STOPID, STARTTIME, STOPTIME, + NUMPOINTS, TOTALDISTANCE, TOTALTIME, MOVINGTIME, AVGSPEED, AVGMOVINGSPEED, + MAXSPEED, MINELEVATION, MAXELEVATION, ELEVATIONGAIN, MINGRADE, MAXGRADE, + MINLAT, MAXLAT, MINLON, MAXLON }; + public static final byte[] BACKUP_COLUMN_TYPES = { + LONG_TYPE_ID, STRING_TYPE_ID, STRING_TYPE_ID, STRING_TYPE_ID, + LONG_TYPE_ID, LONG_TYPE_ID, LONG_TYPE_ID, LONG_TYPE_ID, INT_TYPE_ID, + FLOAT_TYPE_ID, LONG_TYPE_ID, LONG_TYPE_ID, FLOAT_TYPE_ID, FLOAT_TYPE_ID, + FLOAT_TYPE_ID, FLOAT_TYPE_ID, FLOAT_TYPE_ID, FLOAT_TYPE_ID, FLOAT_TYPE_ID, + FLOAT_TYPE_ID, INT_TYPE_ID, INT_TYPE_ID, INT_TYPE_ID, INT_TYPE_ID }; } diff --git a/MyTracks/src/com/google/android/apps/mytracks/content/WaypointsColumns.java b/MyTracks/src/com/google/android/apps/mytracks/content/WaypointsColumns.java index 8184bd583..08c57c179 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/content/WaypointsColumns.java +++ b/MyTracks/src/com/google/android/apps/mytracks/content/WaypointsColumns.java @@ -15,6 +15,8 @@ */ package com.google.android.apps.mytracks.content; +import static com.google.android.apps.mytracks.content.ContentTypeIds.*; + import android.net.Uri; import android.provider.BaseColumns; @@ -66,4 +68,19 @@ public interface WaypointsColumns extends BaseColumns { public static final String ELEVATIONGAIN = "elevationgain"; public static final String MINGRADE = "mingrade"; public static final String MAXGRADE = "maxgrade"; + + public static final String[] BACKUP_COLUMNS = { + _ID, TRACKID, NAME, DESCRIPTION, CATEGORY, ICON, TYPE, LENGTH, DURATION, + STARTTIME, STARTID, STOPID, LATITUDE, LONGITUDE, ALTITUDE, BEARING, TIME, + ACCURACY, SPEED, TOTALDISTANCE, TOTALTIME, MOVINGTIME, AVGSPEED, + AVGMOVINGSPEED, MAXSPEED, MINELEVATION, MAXELEVATION, ELEVATIONGAIN, + MINGRADE, MAXGRADE }; + public static final byte[] BACKUP_COLUMN_TYPES = { + LONG_TYPE_ID, LONG_TYPE_ID, STRING_TYPE_ID, STRING_TYPE_ID, + STRING_TYPE_ID, STRING_TYPE_ID, INT_TYPE_ID, FLOAT_TYPE_ID, LONG_TYPE_ID, + LONG_TYPE_ID, LONG_TYPE_ID, LONG_TYPE_ID, INT_TYPE_ID, INT_TYPE_ID, + FLOAT_TYPE_ID, FLOAT_TYPE_ID, LONG_TYPE_ID, FLOAT_TYPE_ID, FLOAT_TYPE_ID, + FLOAT_TYPE_ID, LONG_TYPE_ID, LONG_TYPE_ID, FLOAT_TYPE_ID, FLOAT_TYPE_ID, + FLOAT_TYPE_ID, FLOAT_TYPE_ID, FLOAT_TYPE_ID, FLOAT_TYPE_ID, FLOAT_TYPE_ID, + FLOAT_TYPE_ID }; } diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/CsvTrackWriter.java b/MyTracks/src/com/google/android/apps/mytracks/io/CsvTrackWriter.java index 0356442ad..22797b4af 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/CsvTrackWriter.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/CsvTrackWriter.java @@ -20,7 +20,6 @@ import com.google.android.apps.mytracks.content.Waypoint; import com.google.android.apps.mytracks.io.TrackWriterFactory.TrackFileFormat; import android.location.Location; -import android.os.Environment; import java.io.OutputStream; import java.io.PrintWriter; @@ -47,7 +46,7 @@ import java.util.TimeZone; public class CsvTrackWriter implements TrackFormatWriter { static final SimpleDateFormat TIMESTAMP_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); - { + static { TIMESTAMP_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC")); } @@ -56,13 +55,6 @@ public class CsvTrackWriter implements TrackFormatWriter { private PrintWriter pw; private Track track; - @Override - public String getDefaultDirectory() { - return Environment.getExternalStorageDirectory() - + System.getProperty("file.separator") - + getExtension(); - } - @Override public String getExtension() { return TrackFileFormat.CSV.getExtension(); 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..4fbba6150 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/io/GpxImporter.java @@ -0,0 +1,577 @@ +/* + * 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"); + static { + DATE_FORMAT1.setTimeZone(UTC_TIMEZONE); + DATE_FORMAT2.setTimeZone(UTC_TIMEZONE); + DATE_FORMAT3.setTimeZone(UTC_TIMEZONE); + } + + /* + * 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 format) { + try { + 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/MyTracks/src/com/google/android/apps/mytracks/io/GpxTrackWriter.java b/MyTracks/src/com/google/android/apps/mytracks/io/GpxTrackWriter.java index 323edf90b..6054dc9a5 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/GpxTrackWriter.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/GpxTrackWriter.java @@ -21,7 +21,6 @@ import com.google.android.apps.mytracks.io.TrackWriterFactory.TrackFileFormat; import com.google.android.apps.mytracks.util.StringUtils; import android.location.Location; -import android.os.Environment; import java.io.OutputStream; import java.io.PrintWriter; @@ -38,7 +37,7 @@ public class GpxTrackWriter implements TrackFormatWriter { static final SimpleDateFormat TIMESTAMP_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); - { + static { TIMESTAMP_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC")); } @@ -51,13 +50,6 @@ public class GpxTrackWriter implements TrackFormatWriter { this.pw = new PrintWriter(out); } - @Override - public String getDefaultDirectory() { - return Environment.getExternalStorageDirectory() - + System.getProperty("file.separator") - + getExtension(); - } - @Override public String getExtension() { return TrackFileFormat.GPX.getExtension(); @@ -145,6 +137,7 @@ public class GpxTrackWriter implements TrackFormatWriter { @Override public void writeWaypoint(Waypoint waypoint) { if (pw != null) { + // TODO: The gpx spec says waypoints should come *before* tracks Location l = waypoint.getLocation(); if (l != null) { pw.println(""); + pw.print(""); + pw.println(); + } + + @Override + public void writeBeginTrack(Location firstPoint) { + if (pw == null) { + return; + } + pw.println(" "); + pw.print(" "); + pw.print(" "); + pw.print(TIMESTAMP_FORMAT.format(track.getStatistics().getStartTime())); + pw.println(""); + pw.println(" "); + pw.print(" "); + pw.print(track.getStatistics().getTotalTime() / 1000); + pw.println(""); + pw.print(" "); + pw.print(track.getStatistics().getTotalDistance()); + pw.println(""); + // TODO max speed etc. + // Calories are a required element just put in 0. + pw.print("0"); + pw.println("Active"); + pw.println("Manual)"); + } + + @Override + public void writeOpenSegment() { + if (pw != null) { + pw.println(" "); + } + } + + @Override + public void writeLocation(Location location) { + if (pw == null) { + return; + } + pw.println(" "); + Date d = new Date(location.getTime()); + pw.println(" "); + pw.println(" "); + + pw.print(" "); + pw.print(location.getLatitude()); + pw.println(""); + + pw.print(" "); + pw.print(location.getLongitude()); + pw.println(""); + + pw.println(" "); + pw.print(" "); + pw.print(location.getAltitude()); + pw.println(""); + + pw.println(" "); + } + + @Override + public void writeCloseSegment() { + if (pw != null) { + pw.println(" "); + } + } + + @Override + public void writeEndTrack(Location lastPoint) { + if (pw == null) { + return; + } + pw.println(" "); + pw.print(" "); + pw.print(""); + pw.print(Build.MODEL); + pw.print(""); + pw.println(")"); + pw.println(" "); + pw.println(" "); + } + + @Override + public void writeFooter() { + if (pw == null) { + return; + } + pw.print(" "); + pw.print("My Tracks by Google"); + pw.println(""); + pw.println(""); + } + + @Override + public void writeWaypoint(Waypoint waypoint) { + // TODO Write out the waypoints somewhere. + } +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/TrackFormatWriter.java b/MyTracks/src/com/google/android/apps/mytracks/io/TrackFormatWriter.java index 60f462e75..c5ac02514 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/TrackFormatWriter.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/TrackFormatWriter.java @@ -53,14 +53,6 @@ public interface TrackFormatWriter { */ void prepare(Track track, OutputStream out); - /** - * Determines the default location that will be written to (if no other is - * specified). - * - * @return The absolute path for the the file created, or null on failure - */ - String getDefaultDirectory(); - /** * @return The file extentsion (i.e. gpx, kml, ...) */ diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/TrackWriter.java b/MyTracks/src/com/google/android/apps/mytracks/io/TrackWriter.java index 46362928b..0554c2c02 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/TrackWriter.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/TrackWriter.java @@ -20,6 +20,7 @@ import com.google.android.apps.mytracks.content.MyTracksProviderUtils; import com.google.android.apps.mytracks.content.Track; import com.google.android.apps.mytracks.content.TrackBuffer; import com.google.android.apps.mytracks.content.Waypoint; +import com.google.android.apps.mytracks.util.FileUtils; import com.google.android.apps.mytracks.util.MyTracksUtils; import com.google.android.maps.mytracks.R; @@ -27,14 +28,12 @@ import android.app.Activity; import android.content.Context; import android.database.Cursor; import android.location.Location; -import android.os.Environment; import android.util.Log; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.OutputStream; -import java.util.HashSet; /** * The class which exports tracks to the SD card. @@ -51,39 +50,20 @@ public class TrackWriter { private final MyTracksProviderUtils providerUtils; private final Track track; private final TrackFormatWriter writer; + private final FileUtils fileUtils; private Runnable onCompletion = null; private boolean success = false; private int errorMessage = -1; private File directory = null; private File file = null; - /** - * A set of characters that are prohibited from being in file names. - */ - private static final HashSet PROHIBITED_CHARACTERS = - new HashSet(); - - static { - for (int i = 0; i < 48; i++) { - PROHIBITED_CHARACTERS.add(new Character((char) i)); - } - for (int i = 58; i < 65; i++) { - PROHIBITED_CHARACTERS.add(new Character((char) i)); - } - for (int i = 91; i < 97; i++) { - PROHIBITED_CHARACTERS.add(new Character((char) i)); - } - for (int i = 123; i < 128; i++) { - PROHIBITED_CHARACTERS.add(new Character((char) i)); - } - } - public TrackWriter(Context context, MyTracksProviderUtils providerUtils, Track track, TrackFormatWriter writer) { this.context = context; this.providerUtils = providerUtils; this.track = track; this.writer = writer; + this.fileUtils = new FileUtils(); } /** @@ -149,26 +129,6 @@ public class TrackWriter { * =============== */ - /** - * Makes sure the given directory exists, creating it if necessary. - * - * @param dir the directory to ensure exists - * @return True on success - */ - private static boolean ensureExists(File dir) { - if (dir.exists()) { - return true; - } - File parent = dir.getParentFile(); - if (parent == null) { - return true; - } - if (!ensureExists(parent)) { - return false; - } - return dir.mkdir(); - } - private void finished() { if (onCompletion != null) { runOnUiThread(onCompletion); @@ -195,9 +155,15 @@ public class TrackWriter { return false; } - // Make sure the name will work on FAT - String fileName = - sanitizeName(track.getName()) + "." + writer.getExtension(); + // Make sure the file doesn't exist yet (possibly by changing the filename) + String fileName = fileUtils.buildUniqueFileName( + directory, track.getName(), writer.getExtension()); + if (fileName == null) { + Log.e(MyTracksConstants.TAG, + "Unable to get a unique filename for " + fileName); + return false; + } + Log.i(MyTracksConstants.TAG, "Writing track to: " + fileName); try { writer.prepare(track, newOutputStream(fileName)); @@ -214,17 +180,18 @@ public class TrackWriter { */ protected boolean canWriteFile() { if (directory == null) { - directory = newFile(writer.getDefaultDirectory()); + String dirName = + fileUtils.buildExternalDirectoryPath(writer.getExtension()); + directory = newFile(dirName); } - if (!Environment.getExternalStorageState() - .equals(Environment.MEDIA_MOUNTED)) { + if (!fileUtils.isSdCardAvailable()) { Log.i(MyTracksConstants.TAG, "Could not find SD card."); errorMessage = R.string.io_no_external_storage_found; return false; } - if (!ensureExists(directory)) { - Log.i(MyTracksConstants.TAG, "Could not create GPX directory."); + if (!fileUtils.ensureDirectoryExists(directory)) { + Log.i(MyTracksConstants.TAG, "Could not create export directory."); errorMessage = R.string.io_create_dir_failed; return false; } @@ -250,25 +217,6 @@ public class TrackWriter { return new File(path); } - /** - * Normalizes the input string and make sure it is a valid fat32 file name. - */ - static String sanitizeName(String name) { - StringBuilder cleaned = new StringBuilder(); - for (int i = 0; i < name.length(); i++) { - char c = name.charAt(i); - if (!PROHIBITED_CHARACTERS.contains(c)) { - cleaned.append(c); - } - } - - // Max = 260 - // Boiler plate /[gpx|kml]/ .[gpx|kml] = 8 - return (cleaned.length() > 252) - ? cleaned.substring(0, 252) - : cleaned.toString(); - } - /** * Writes the waypoints for the given track. * diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/TrackWriterFactory.java b/MyTracks/src/com/google/android/apps/mytracks/io/TrackWriterFactory.java index 98e7c177b..bc3c15ddd 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/TrackWriterFactory.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/TrackWriterFactory.java @@ -49,6 +49,12 @@ public class TrackWriterFactory { public TrackFormatWriter newFormatWriter(Context context) { return new CsvTrackWriter(); } + }, + TCX { + @Override + public TrackFormatWriter newFormatWriter(Context context) { + return new TcxTrackWriter(); + } }; /** diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/backup/BackupActivityHelper.java b/MyTracks/src/com/google/android/apps/mytracks/io/backup/BackupActivityHelper.java new file mode 100644 index 000000000..e66cd29d8 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/io/backup/BackupActivityHelper.java @@ -0,0 +1,247 @@ +/* + * 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.backup; + +import com.google.android.apps.mytracks.MyTracks; +import com.google.android.apps.mytracks.MyTracksConstants; +import com.google.android.apps.mytracks.util.FileUtils; +import com.google.android.maps.mytracks.R; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.AlertDialog.Builder; +import android.app.Dialog; +import android.app.ProgressDialog; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.content.Intent; +import android.util.Log; +import android.widget.Toast; + +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.Comparator; +import java.util.Date; + +/** + * Helper which shows a UI for writing or restoring a backup, + * and calls the appropriate handler for actually executing those + * operations. + * + * @author Rodrigo Damazio + */ +public class BackupActivityHelper { + + // Since the user sees this format, we use the local timezone + private static final SimpleDateFormat DISPLAY_BACKUP_FORMAT = + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + + private static final Comparator REVERSE_DATE_ORDER = + new Comparator() { + @Override + public int compare(Date s1, Date s2) { + return s2.compareTo(s1); + } + }; + + private final FileUtils fileUtils; + private final ExternalFileBackup backup; + private final Activity activity; + + public BackupActivityHelper(Activity activity) { + this.activity = activity; + this.fileUtils = new FileUtils(); + this.backup = new ExternalFileBackup(activity, fileUtils); + } + + /** + * Writes a full backup to the default file. + * This shows the results to the user. + */ + public void writeBackup() { + if (!fileUtils.isSdCardAvailable()) { + showToast(R.string.io_no_external_storage_found); + return; + } + + if (!backup.isBackupsDirectoryAvailable(true)) { + showToast(R.string.io_create_dir_failed); + return; + } + + final ProgressDialog progressDialog = ProgressDialog.show( + activity, + activity.getString(R.string.progress_title), + activity.getString(R.string.backup_write_progress_message), + true); + + // Do the writing in another thread + new Thread() { + @Override + public void run() { + try { + backup.writeToDefaultFile(); + showToast(R.string.io_write_finished); + } catch (IOException e) { + Log.e(MyTracksConstants.TAG, "Failed to write backup", e); + showToast(R.string.io_write_failed); + } finally { + dismissDialog(progressDialog); + } + } + }.start(); + } + + /** + * Restores a full backup from the SD card. + * The user will be given a choice of which backup to restore as well as a + * confirmation dialog. + */ + public void restoreBackup() { + // Get the list of existing backups + if (!fileUtils.isSdCardAvailable()) { + showToast(R.string.io_no_external_storage_found); + return; + } + + if (!backup.isBackupsDirectoryAvailable(false)) { + showToast(R.string.no_backups); + return; + } + + final Date[] backupDates = backup.getAvailableBackups(); + if (backupDates == null || backupDates.length == 0) { + showToast(R.string.no_backups); + return; + } + Arrays.sort(backupDates, REVERSE_DATE_ORDER); + + // Show a confirmation dialog + Builder confirmationDialogBuilder = new AlertDialog.Builder(activity); + confirmationDialogBuilder.setMessage(R.string.restore_overwrites_warning); + confirmationDialogBuilder.setCancelable(true); + confirmationDialogBuilder.setPositiveButton(android.R.string.yes, + new OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + pickBackupForRestore(backupDates); + } + }); + confirmationDialogBuilder.setNegativeButton(android.R.string.no, null); + confirmationDialogBuilder.create().show(); + } + + /** + * Shows a backup list for the user to pick, then restores it. + * + * @param backupDir the backup directory + * @param backupDates the list of available backup files + */ + private void pickBackupForRestore(final Date[] backupDates) { + if (backupDates.length == 1) { + // Only one choice, don't bother showing the list + restoreFromDateAsync(backupDates[0]); + return; + } + + // Make a user-visible version of the backup filenames + final String backupDateStrs[] = new String[backupDates.length]; + for (int i = 0; i < backupDates.length; i++) { + backupDateStrs[i] = DISPLAY_BACKUP_FORMAT.format(backupDates[i]); + } + + // Show a dialog for the user to pick which backup to restore + Builder dialogBuilder = new AlertDialog.Builder(activity); + dialogBuilder.setCancelable(true); + dialogBuilder.setTitle(R.string.select_backup_to_restore); + dialogBuilder.setItems(backupDateStrs, new OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + // User picked to restore this one + restoreFromDateAsync(backupDates[which]); + } + }); + dialogBuilder.create().show(); + } + + /** + * Shows a progress dialog, then starts restoring the backup osynchronously. + * + * @param inputFile the file to restore from + */ + private void restoreFromDateAsync(final Date date) { + // Show a progress dialog + final ProgressDialog progressDialog = ProgressDialog.show( + activity, + activity.getString(R.string.progress_title), + activity.getString(R.string.backup_import_progress_message), + true); + + // Do the actual importing in another thread (don't block the UI) + new Thread() { + @Override + public void run() { + try { + backup.restoreFromDate(date); + showToast(R.string.io_read_finished); + } catch (IOException e) { + Log.e(MyTracksConstants.TAG, "Failed to restore backup", e); + showToast(R.string.io_read_failed); + } finally { + dismissDialog(progressDialog); + + // Data may have been restored, "reboot" the app to catch it + restartApplication(); + } + } + }.start(); + } + + /** + * Restarts My Tracks completely. + * This forces any modified data to be re-read. + */ + private void restartApplication() { + Intent intent = new Intent(activity, MyTracks.class); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + activity.startActivity(intent); + } + + /** + * Shows a toast with the given contents. + */ + private void showToast(final int resId) { + activity.runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(activity, resId, Toast.LENGTH_LONG).show(); + } + }); + } + + /** + * Safely dismisses the given dialog. + */ + private void dismissDialog(final Dialog dialog) { + activity.runOnUiThread(new Runnable() { + @Override + public void run() { + dialog.dismiss(); + } + }); + } +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/backup/BackupPreferencesListener.java b/MyTracks/src/com/google/android/apps/mytracks/io/backup/BackupPreferencesListener.java new file mode 100644 index 000000000..b6caaeab9 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/io/backup/BackupPreferencesListener.java @@ -0,0 +1,76 @@ +/* + * 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.backup; + +import com.google.android.apps.mytracks.MyTracksConstants; + +import android.app.backup.BackupManager; +import android.content.Context; +import android.content.SharedPreferences; +import android.content.SharedPreferences.OnSharedPreferenceChangeListener; + +/** + * Shared preferences listener which notifies the backup system about new data + * being available for backup. + * This class is API-version-safe and will provide a dummy implementation if + * the device doesn't support backup services. + * + * @author Rodrigo Damazio + */ +public abstract class BackupPreferencesListener + implements OnSharedPreferenceChangeListener { + + /** + * Real implementation of the listener, which calls the {@link BackupManager}. + */ + private static class BackupPreferencesListenerImpl + extends BackupPreferencesListener { + private final BackupManager backupManager; + + public BackupPreferencesListenerImpl(Context context) { + this.backupManager = new BackupManager(context); + } + + @Override + public void onSharedPreferenceChanged( + SharedPreferences sharedPreferences, String key) { + backupManager.dataChanged(); + } + } + + /** + * Dummy implementation of the listener which does nothing. + */ + private static class DummyBackupPreferencesListener + extends BackupPreferencesListener { + @Override + public void onSharedPreferenceChanged( + SharedPreferences sharedPreferences, String key) { + // Do nothing + } + } + + /** + * Creates and returns a proper instance of the listener for this device. + */ + public static BackupPreferencesListener create(Context context) { + if (MyTracksConstants.ANDROID_API_LEVEL >= 8) { + return new BackupPreferencesListenerImpl(context); + } else { + return new DummyBackupPreferencesListener(); + } + } +} \ No newline at end of file diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/backup/DatabaseDumper.java b/MyTracks/src/com/google/android/apps/mytracks/io/backup/DatabaseDumper.java new file mode 100644 index 000000000..310a16824 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/io/backup/DatabaseDumper.java @@ -0,0 +1,236 @@ +/* + * 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.backup; + +import com.google.android.apps.mytracks.content.ContentTypeIds; + +import android.database.Cursor; +import android.database.MergeCursor; + +import java.io.DataOutputStream; +import java.io.IOException; + +/** + * Database dumper which is able to write only part of the database + * according to some query. + * + * This dumper is symmetrical to {@link DatabaseImporter}. + * + * @author Rodrigo Damazio + */ +class DatabaseDumper { + + /** The names of the columns being dumped. */ + private final String[] columnNames; + /** The types of the columns being dumped. */ + private final byte[] columnTypes; + /** Whether to output null fields. */ + private final boolean outputNullFields; + + // Temporary state + private int[] columnIndices; + private boolean[] hasFields; + + public DatabaseDumper(String[] columnNames, byte[] columnTypes, + boolean outputNullFields) { + if (columnNames.length != columnTypes.length) { + throw new IllegalArgumentException("Names don't match types"); + } + + this.columnNames = columnNames; + this.columnTypes = columnTypes; + this.outputNullFields = outputNullFields; + } + + /** + * Writes the header plus all rows that can be read from the given cursor. + * This assumes the cursor will have the same column and column indices on + * every row (and thus may not work with a {@link MergeCursor}). + */ + public void writeAllRows(Cursor cursor, DataOutputStream writer) + throws IOException { + writeHeaders(cursor, cursor.getCount(), writer); + + if (!cursor.moveToFirst()) { + return; + } + + do { + writeOneRow(cursor, writer); + } while (cursor.moveToNext()); + } + + /** + * Writes just the headers for the data that will come from the given cursor. + * The headers include column information and the number of rows that will be + * written. + * + * @param cursor the cursor to get columns from + * @param numRows the number of rows that will be later written + * @throws IOException if there are errors while writing + */ + public void writeHeaders(Cursor cursor, int numRows, DataOutputStream writer) + throws IOException { + initializeCachedValues(cursor); + writeQueryMetadata(cursor, numRows, writer); + } + + /** + * Writes the current row from the cursor. The cursor is not advanced. + * This must be called after {@link #writeHeaders}. + * + * @param cursor the cursor to write data from + * @throws IOException if there are any errors while writing + */ + public void writeOneRow(Cursor cursor, DataOutputStream writer) + throws IOException { + if (columnIndices == null) { + throw new IllegalStateException( + "Cannot write rows before writing the header"); + } + + if (columnIndices.length > Long.SIZE) { + throw new IllegalArgumentException("Too many fields"); + } + + // Build a bitmap of which fields are present + long fields = 0; + for (int i = 0; i < columnIndices.length; i++) { + hasFields[i] = !cursor.isNull(columnIndices[i]); + fields |= (hasFields[i] ? 1 : 0) << i; + } + writer.writeLong(fields); + + // Actually write the present fields + for (int i = 0; i < columnIndices.length; i++) { + if (hasFields[i]) { + writeCell(columnIndices[i], columnTypes[i], cursor, writer); + } else if (outputNullFields) { + writeDummyCell(columnTypes[i], writer); + } + } + } + + /** + * Initializes the column indices and other temporary state for reading from + * the given cursor. + */ + private void initializeCachedValues(Cursor cursor) { + // These indices are constant for every row (unless we're fed a MergeCursor) + if (cursor instanceof MergeCursor) { + throw new IllegalArgumentException("Cannot use a MergeCursor"); + } + + columnIndices = new int[columnNames.length]; + for (int i = 0; i < columnNames.length; i++) { + String columnName = columnNames[i]; + columnIndices[i] = cursor.getColumnIndexOrThrow(columnName); + } + + hasFields = new boolean[columnIndices.length]; + } + + /** + * Writes metadata about the query to be dumped. + * + * @param cursor the cursor that the data will come from + * @param numRows the number of rows that will be dumped + * @throws IOException if there are any errors while writing + */ + private void writeQueryMetadata( + Cursor cursor, int numRows, DataOutputStream writer) throws IOException { + // Write column data + writer.writeInt(columnNames.length); + for (int i = 0; i < columnNames.length; i++) { + String columnName = columnNames[i]; + byte columnType = columnTypes[i]; + writer.writeUTF(columnName); + writer.writeByte(columnType); + } + + // Write the number of rows + writer.writeInt(numRows); + } + + /** + * Writes a single cell of the database to the output. + * + * @param columnIdx the column index to read from + * @param columnType the type of the column to be read + * @param cursor the cursor to read from + * @throws IOException if there are any errors while writing + */ + private void writeCell( + int columnIdx, byte columnTypeId, Cursor cursor, DataOutputStream writer) + throws IOException { + switch (columnTypeId) { + case ContentTypeIds.LONG_TYPE_ID: + writer.writeLong(cursor.getLong(columnIdx)); + return; + case ContentTypeIds.DOUBLE_TYPE_ID: + writer.writeDouble(cursor.getDouble(columnIdx)); + return; + case ContentTypeIds.FLOAT_TYPE_ID: + writer.writeFloat(cursor.getFloat(columnIdx)); + return; + case ContentTypeIds.BOOLEAN_TYPE_ID: + writer.writeBoolean(cursor.getInt(columnIdx) != 0); + return; + case ContentTypeIds.INT_TYPE_ID: + writer.writeInt(cursor.getInt(columnIdx)); + return; + case ContentTypeIds.STRING_TYPE_ID: + writer.writeUTF(cursor.getString(columnIdx)); + return; + default: + throw new IllegalArgumentException( + "Type " + columnTypeId + " not supported"); + } + } + + /** + * Writes a dummy cell value to the output. + * + * @param columnType the type of the value to write + * @throws IOException if there are any errors while writing + */ + private void writeDummyCell(byte columnTypeId, DataOutputStream writer) + throws IOException { + switch (columnTypeId) { + case ContentTypeIds.LONG_TYPE_ID: + writer.writeLong(0L); + return; + case ContentTypeIds.DOUBLE_TYPE_ID: + writer.writeDouble(0.0); + return; + case ContentTypeIds.FLOAT_TYPE_ID: + writer.writeFloat(0.0f); + return; + case ContentTypeIds.BOOLEAN_TYPE_ID: + writer.writeBoolean(false); + return; + case ContentTypeIds.INT_TYPE_ID: + writer.writeInt(0); + return; + case ContentTypeIds.STRING_TYPE_ID: + writer.writeUTF(""); + return; + default: + throw new IllegalArgumentException( + "Type " + columnTypeId + " not supported"); + } + } +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/backup/DatabaseImporter.java b/MyTracks/src/com/google/android/apps/mytracks/io/backup/DatabaseImporter.java new file mode 100644 index 000000000..c57ce175c --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/io/backup/DatabaseImporter.java @@ -0,0 +1,180 @@ +/* + * 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.backup; + +import com.google.android.apps.mytracks.content.ContentTypeIds; + +import android.content.ContentResolver; +import android.content.ContentValues; +import android.net.Uri; + +import java.io.DataInputStream; +import java.io.IOException; + +/** + * Database importer which reads values written by {@link DatabaseDumper}. + * + * @author Rodrigo Damazio + */ +public class DatabaseImporter { + + /** Maximum number of entries in a bulk insertion */ + private static final int DEFAULT_BULK_SIZE = 1024; + + private final Uri destinationUri; + private final ContentResolver resolver; + private final boolean readNullFields; + private final int bulkSize; + + // Metadata read from the reader + private String[] columnNames; + private byte[] columnTypes; + + public DatabaseImporter(Uri destinationUri, ContentResolver resolver, + boolean readNullFields) { + this(destinationUri, resolver, readNullFields, DEFAULT_BULK_SIZE); + } + + protected DatabaseImporter(Uri destinationUri, ContentResolver resolver, + boolean readNullFields, int bulkSize) { + this.destinationUri = destinationUri; + this.resolver = resolver; + this.readNullFields = readNullFields; + this.bulkSize = bulkSize; + } + + /** + * Reads the header which includes metadata about the table being imported. + * + * @throws IOException if there are any problems while reading + */ + private void readHeaders(DataInputStream reader) throws IOException { + int numColumns = reader.readInt(); + columnNames = new String[numColumns]; + columnTypes = new byte[numColumns]; + for (int i = 0; i < numColumns; i++) { + columnNames[i] = reader.readUTF(); + columnTypes[i] = reader.readByte(); + } + } + + /** + * Imports all rows from the reader into the database. + * Insertion is done in bulks for efficiency. + * + * @throws IOException if there are any errors while reading + */ + public void importAllRows(DataInputStream reader) throws IOException { + readHeaders(reader); + + ContentValues[] valueBulk = new ContentValues[bulkSize]; + int numValues = 0; + + int numRows = reader.readInt(); + int numColumns = columnNames.length; + + // For each row + for (int r = 0; r < numRows; r++) { + if (valueBulk[numValues] == null) { + valueBulk[numValues] = new ContentValues(numColumns); + } else { + // Reuse values objects + valueBulk[numValues].clear(); + } + + // Read the fields bitmap + long fields = reader.readLong(); + for (int c = 0; c < numColumns; c++) { + if ((fields & 1) == 1) { + // Field is present, read into values + readOneCell(columnNames[c], columnTypes[c], valueBulk[numValues], + reader); + } else if (readNullFields) { + // Field not present but still written, read and discard + readOneCell(columnNames[c], columnTypes[c], null, reader); + } + + fields >>= 1; + } + + numValues++; + + // If we have enough values, flush them as a bulk insertion + if (numValues >= bulkSize) { + doBulkInsert(valueBulk); + numValues = 0; + } + } + + // Do a final bulk insert with the leftovers + if (numValues > 0) { + ContentValues[] leftovers = new ContentValues[numValues]; + System.arraycopy(valueBulk, 0, leftovers, 0, numValues); + doBulkInsert(leftovers); + } + } + + protected void doBulkInsert(ContentValues[] values) { + resolver.bulkInsert(destinationUri, values); + } + + /** + * Reads a single cell from the reader. + * + * @param name the name of the column to be read + * @param typeId the type ID of the column to be read + * @param values the {@link ContentValues} object to put the read cell value + * in - if null, the value is just discarded + * @throws IOException if there are any problems while reading + */ + private void readOneCell(String name, byte typeId, ContentValues values, + DataInputStream reader) throws IOException { + switch (typeId) { + case ContentTypeIds.BOOLEAN_TYPE_ID: { + boolean value = reader.readBoolean(); + if (values != null) { values.put(name, value); } + return; + } + case ContentTypeIds.LONG_TYPE_ID: { + long value = reader.readLong(); + if (values != null) { values.put(name, value); } + return; + } + case ContentTypeIds.DOUBLE_TYPE_ID: { + double value = reader.readDouble(); + if (values != null) { values.put(name, value); } + return; + } + case ContentTypeIds.FLOAT_TYPE_ID: { + Float value = reader.readFloat(); + if (values != null) { values.put(name, value); } + return; + } + case ContentTypeIds.INT_TYPE_ID: { + int value = reader.readInt(); + if (values != null) { values.put(name, value); } + return; + } + case ContentTypeIds.STRING_TYPE_ID: { + String value = reader.readUTF(); + if (values != null) { values.put(name, value); } + return; + } + default: + throw new IOException("Read unknown type " + typeId); + } + } +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/backup/ExternalFileBackup.java b/MyTracks/src/com/google/android/apps/mytracks/io/backup/ExternalFileBackup.java new file mode 100644 index 000000000..48155fe78 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/io/backup/ExternalFileBackup.java @@ -0,0 +1,263 @@ +/* + * 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.backup; + +import com.google.android.apps.mytracks.MyTracksConstants; +import com.google.android.apps.mytracks.MyTracksSettings; +import com.google.android.apps.mytracks.content.TrackPointsColumns; +import com.google.android.apps.mytracks.content.TracksColumns; +import com.google.android.apps.mytracks.content.WaypointsColumns; +import com.google.android.apps.mytracks.util.FileUtils; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.SharedPreferences; +import android.database.Cursor; +import android.util.Log; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.TimeZone; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import java.util.zip.ZipOutputStream; + +/** + * Handler for writing or reading single-file backups. + * + * @author Rodrigo Damazio + */ +class ExternalFileBackup { + // Filename format - in UTC + private static final SimpleDateFormat BACKUP_FILENAME_FORMAT = + new SimpleDateFormat("'backup-'yyyy-MM-dd_HH-mm-ss'.zip'"); + static { + BACKUP_FILENAME_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC")); + } + + private static final String BACKUPS_SUBDIR = "backups"; + private static final int BACKUP_FORMAT_VERSION = 1; + private static final String ZIP_ENTRY_NAME = + "backup.mytracks.v" + BACKUP_FORMAT_VERSION; + private static final int COMPRESSION_LEVEL = 8; + + private final Context context; + private final FileUtils fileUtils; + + public ExternalFileBackup(Context context, FileUtils fileUtils) { + this.context = context; + this.fileUtils = fileUtils; + } + + /** + * Returns whether the backups directory is (or can be made) available. + * + * @param create whether to try creating the directory if it doesn't exist + */ + public boolean isBackupsDirectoryAvailable(boolean create) { + return getBackupsDirectory(create) != null; + } + + /** + * Returns the backup directory, or null if not available. + * + * @param create whether to try creating the directory if it doesn't exist + */ + private File getBackupsDirectory(boolean create) { + String dirName = fileUtils.buildExternalDirectoryPath(BACKUPS_SUBDIR); + final File dir = new File(dirName); + Log.d(MyTracksConstants.TAG, "Dir: " + dir.getAbsolutePath()); + if (create) { + // Try to create - if that fails, return null + return fileUtils.ensureDirectoryExists(dir) ? dir : null; + } else { + // Return it if it already exists, otherwise return null + return dir.isDirectory() ? dir : null; + } + } + + /** + * Returns a list of available backups to be restored. + */ + public Date[] getAvailableBackups() { + File dir = getBackupsDirectory(false); + if (dir == null) { return null; } + String[] fileNames = dir.list(); + + List backupDates = new ArrayList(fileNames.length); + for (int i = 0; i < fileNames.length; i++) { + String fileName = fileNames[i]; + try { + backupDates.add(BACKUP_FILENAME_FORMAT.parse(fileName)); + } catch (ParseException e) { + // Not a backup file, ignore + } + } + + return backupDates.toArray(new Date[backupDates.size()]); + } + + /** + * Writes the backup to the default file. + */ + public void writeToDefaultFile() throws IOException { + writeToFile(getFileForDate(new Date())); + } + + /** + * Restores the backup from the given date. + */ + public void restoreFromDate(Date when) throws IOException { + restoreFromFile(getFileForDate(when)); + } + + /** + * Produces the proper file descriptor for the given backup date. + */ + private File getFileForDate(Date when) { + File dir = getBackupsDirectory(false); + String fileName = BACKUP_FILENAME_FORMAT.format(when); + File file = new File(dir, fileName); + return file; + } + + /** + * Synchronously writes a backup to the given file. + */ + private void writeToFile(File outputFile) throws IOException { + Log.d(MyTracksConstants.TAG, + "Writing backup to file " + outputFile.getAbsolutePath()); + + // Create all the auxiliary classes that will do the writing + PreferenceBackupHelper preferencesHelper = new PreferenceBackupHelper(); + DatabaseDumper trackDumper = new DatabaseDumper( + TracksColumns.BACKUP_COLUMNS, + TracksColumns.BACKUP_COLUMN_TYPES, + false); + DatabaseDumper waypointDumper = new DatabaseDumper( + WaypointsColumns.BACKUP_COLUMNS, + WaypointsColumns.BACKUP_COLUMN_TYPES, + false); + DatabaseDumper pointDumper = new DatabaseDumper( + TrackPointsColumns.BACKUP_COLUMNS, + TrackPointsColumns.BACKUP_COLUMN_TYPES, + false); + + // Open the target for writing + FileOutputStream outputStream = new FileOutputStream(outputFile); + ZipOutputStream compressedStream = new ZipOutputStream(outputStream); + compressedStream.setLevel(COMPRESSION_LEVEL); + compressedStream.putNextEntry(new ZipEntry(ZIP_ENTRY_NAME)); + DataOutputStream outWriter = new DataOutputStream(compressedStream); + + try { + // Dump the entire contents of each table + ContentResolver contentResolver = context.getContentResolver(); + Cursor tracksCursor = contentResolver.query( + TracksColumns.CONTENT_URI, null, null, null, null); + try { + trackDumper.writeAllRows(tracksCursor, outWriter); + } finally { + tracksCursor.close(); + } + + Cursor waypointsCursor = contentResolver.query( + WaypointsColumns.CONTENT_URI, null, null, null, null); + try { + waypointDumper.writeAllRows(waypointsCursor, outWriter); + } finally { + waypointsCursor.close(); + } + + Cursor pointsCursor = contentResolver.query( + TrackPointsColumns.CONTENT_URI, null, null, null, null); + try { + pointDumper.writeAllRows(pointsCursor, outWriter); + } finally { + pointsCursor.close(); + } + + // Dump preferences + SharedPreferences preferences = + context.getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0); + preferencesHelper.exportPreferences(preferences, outWriter); + } catch (IOException e) { + // We tried to delete the partially created file, but do nothing + // if that also fails. + outputFile.delete(); + + throw e; + } finally { + compressedStream.closeEntry(); + compressedStream.close(); + } + } + + /** + * Synchronously restores the backup from the given file. + */ + private void restoreFromFile(File inputFile) throws IOException { + Log.d(MyTracksConstants.TAG, + "Restoring from file " + inputFile.getAbsolutePath()); + + PreferenceBackupHelper preferencesHelper = new PreferenceBackupHelper(); + ContentResolver resolver = context.getContentResolver(); + DatabaseImporter trackImporter = + new DatabaseImporter(TracksColumns.CONTENT_URI, resolver, false); + DatabaseImporter waypointImporter = + new DatabaseImporter(WaypointsColumns.CONTENT_URI, resolver, false); + DatabaseImporter pointImporter = + new DatabaseImporter(TrackPointsColumns.CONTENT_URI, resolver, false); + + ZipFile zipFile = new ZipFile(inputFile, ZipFile.OPEN_READ); + ZipEntry zipEntry = zipFile.getEntry(ZIP_ENTRY_NAME); + if (zipEntry == null) { + throw new IOException("Invalid backup ZIP file"); + } + + InputStream compressedStream = zipFile.getInputStream(zipEntry); + DataInputStream reader = new DataInputStream(compressedStream); + + try { + // Delete all previous contents of the tables and preferences. + resolver.delete(TracksColumns.CONTENT_URI, null, null); + resolver.delete(TrackPointsColumns.CONTENT_URI, null, null); + resolver.delete(WaypointsColumns.CONTENT_URI, null, null); + + // Import the new contents of each table + trackImporter.importAllRows(reader); + waypointImporter.importAllRows(reader); + pointImporter.importAllRows(reader); + + // Restore preferences + SharedPreferences preferences = + context.getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0); + preferencesHelper.importPreferences(reader, preferences); + } finally { + compressedStream.close(); + zipFile.close(); + } + } +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/backup/MyTracksBackupAgent.java b/MyTracks/src/com/google/android/apps/mytracks/io/backup/MyTracksBackupAgent.java new file mode 100644 index 000000000..f9bb2f352 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/io/backup/MyTracksBackupAgent.java @@ -0,0 +1,99 @@ +/* + * 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.backup; + +import com.google.android.apps.mytracks.MyTracksConstants; +import com.google.android.apps.mytracks.MyTracksSettings; + +import android.app.backup.BackupAgent; +import android.app.backup.BackupDataInput; +import android.app.backup.BackupDataOutput; +import android.content.SharedPreferences; +import android.os.ParcelFileDescriptor; +import android.util.Log; + +import java.io.IOException; + +/** + * Backup agent used to backup and restore all preferences. + * We use a regular {@link BackupAgent} instead of the convenient helpers in + * order to be future-proof (assuming we'll want to back up tracks later). + * + * @author Rodrigo Damazio + */ +public class MyTracksBackupAgent extends BackupAgent { + private static final String PREFERENCES_ENTITY = "prefs"; + + @Override + public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data, + ParcelFileDescriptor newState) throws IOException { + Log.i(MyTracksConstants.TAG, "Performing backup"); + SharedPreferences preferences = this.getSharedPreferences( + MyTracksSettings.SETTINGS_NAME, 0); + + backupPreferences(data, preferences); + Log.i(MyTracksConstants.TAG, "Backup complete"); + } + + private void backupPreferences(BackupDataOutput data, + SharedPreferences preferences) throws IOException { + PreferenceBackupHelper preferenceDumper = createPreferenceBackupHelper(); + byte[] dumpedContents = preferenceDumper.exportPreferences(preferences); + data.writeEntityHeader(PREFERENCES_ENTITY, dumpedContents.length); + data.writeEntityData(dumpedContents, dumpedContents.length); + } + + protected PreferenceBackupHelper createPreferenceBackupHelper() { + return new PreferenceBackupHelper(); + } + + @Override + public void onRestore(BackupDataInput data, int appVersionCode, + ParcelFileDescriptor newState) throws IOException { + Log.i(MyTracksConstants.TAG, "Restoring from backup"); + while (data.readNextHeader()) { + String key = data.getKey(); + Log.d(MyTracksConstants.TAG, "Restoring entity " + key); + if (key.equals(PREFERENCES_ENTITY)) { + restorePreferences(data); + } else { + Log.e(MyTracksConstants.TAG, "Found unknown backup entity: " + key); + data.skipEntityData(); + } + } + Log.i(MyTracksConstants.TAG, "Done restoring from backup"); + } + + /** + * Restores all preferences from the backup. + * + * @param data the backup data to read from + * @throws IOException if there are any errors while reading + */ + private void restorePreferences(BackupDataInput data) throws IOException { + int dataSize = data.getDataSize(); + byte[] dataBuffer = new byte[dataSize]; + int read = data.readEntityData(dataBuffer, 0, dataSize); + if (read != dataSize) { + throw new IOException("Failed to read all the preferences data"); + } + + SharedPreferences preferences = this.getSharedPreferences( + MyTracksSettings.SETTINGS_NAME, 0); + PreferenceBackupHelper importer = createPreferenceBackupHelper(); + importer.importPreferences(dataBuffer, preferences); + } +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/backup/PreferenceBackupHelper.java b/MyTracks/src/com/google/android/apps/mytracks/io/backup/PreferenceBackupHelper.java new file mode 100644 index 000000000..3987ada9d --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/io/backup/PreferenceBackupHelper.java @@ -0,0 +1,176 @@ +/* + * 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.backup; + +import com.google.android.apps.mytracks.content.ContentTypeIds; + +import android.content.SharedPreferences; +import android.content.SharedPreferences.Editor; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.Map; + +/** + * Helper for backing up and restoring shared preferences. + * + * @author Rodrigo Damazio + */ +class PreferenceBackupHelper { + + private static final int BUFFER_SIZE = 2048; + + /** + * Exports all shared preferences from the given object as a byte array. + * + * @param preferences the preferences to export + * @return the corresponding byte array + * @throws IOException if there are any errors while writing to the byte array + */ + public byte[] exportPreferences(SharedPreferences preferences) + throws IOException { + ByteArrayOutputStream bufStream = new ByteArrayOutputStream(BUFFER_SIZE); + DataOutputStream outWriter = new DataOutputStream(bufStream); + exportPreferences(preferences, outWriter); + + return bufStream.toByteArray(); + } + + /** + * Exports all shared preferences from the given object into the given output + * stream. + * + * @param preferences the preferences to export + * @param bufStream the stream to write them to + * @throws IOException if there are any errors while writing the output + */ + public void exportPreferences( + SharedPreferences preferences, + DataOutputStream outWriter) throws IOException { + Map values = preferences.getAll(); + + outWriter.writeInt(values.size()); + for (Map.Entry entry : values.entrySet()) { + writePreference(entry.getKey(), entry.getValue(), outWriter); + } + outWriter.flush(); + } + + /** + * Imports all preferences from the given byte array. + * + * @param data the byte array to read preferences from + * @param preferences the shared preferences to edit + * @return whether the preference change was successful + * @throws IOException if there are any errors while reading + */ + public boolean importPreferences(byte[] data, SharedPreferences preferences) + throws IOException { + ByteArrayInputStream bufStream = new ByteArrayInputStream(data); + DataInputStream reader = new DataInputStream(bufStream); + + return importPreferences(reader, preferences); + } + + /** + * Imports all preferences from the given stream. + * + * @param reader the stream to read from + * @param preferences the shared preferences to edit + * @return whether the preference change was successful + * @throws IOException if there are any errors while reading + */ + public boolean importPreferences(DataInputStream reader, + SharedPreferences preferences) throws IOException { + Editor editor = preferences.edit(); + editor.clear(); + + int numPreferences = reader.readInt(); + for (int i = 0; i < numPreferences; i++) { + String name = reader.readUTF(); + byte typeId = reader.readByte(); + readAndSetPreference(name, typeId, reader, editor); + } + + return editor.commit(); + } + + /** + * Reads a single preference and sets it into the given editor. + * + * @param name the name of the preference to read + * @param typeId the type ID of the preference to read + * @param reader the reader to read from + * @param editor the editor to set the preference in + * @throws IOException if there are errors while reading + */ + private void readAndSetPreference(String name, byte typeId, + DataInputStream reader, Editor editor) throws IOException { + switch (typeId) { + case ContentTypeIds.BOOLEAN_TYPE_ID: + editor.putBoolean(name, reader.readBoolean()); + return; + case ContentTypeIds.LONG_TYPE_ID: + editor.putLong(name, reader.readLong()); + return; + case ContentTypeIds.FLOAT_TYPE_ID: + editor.putFloat(name, reader.readFloat()); + return; + case ContentTypeIds.INT_TYPE_ID: + editor.putInt(name, reader.readInt()); + return; + case ContentTypeIds.STRING_TYPE_ID: + editor.putString(name, reader.readUTF()); + return; + } + } + + /** + * Writes a single preference. + * + * @param name the name of the preference to write + * @param value the correctly-typed value of the preference + * @param writer the writer to write to + * @throws IOException if there are errors while writing + */ + private void writePreference(String name, Object value, DataOutputStream writer) + throws IOException { + writer.writeUTF(name); + + if (value instanceof Boolean) { + writer.writeByte(ContentTypeIds.BOOLEAN_TYPE_ID); + writer.writeBoolean((Boolean) value); + } else if (value instanceof Integer) { + writer.writeByte(ContentTypeIds.INT_TYPE_ID); + writer.writeInt((Integer) value); + } else if (value instanceof Long) { + writer.writeByte(ContentTypeIds.LONG_TYPE_ID); + writer.writeLong((Long) value); + } else if (value instanceof Float) { + writer.writeByte(ContentTypeIds.FLOAT_TYPE_ID); + writer.writeFloat((Float) value); + } else if (value instanceof String) { + writer.writeByte(ContentTypeIds.STRING_TYPE_ID); + writer.writeUTF((String) value); + } else { + throw new IllegalArgumentException( + "Type " + value.getClass().getName() + " not supported"); + } + } +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/PreferenceManager.java b/MyTracks/src/com/google/android/apps/mytracks/services/PreferenceManager.java new file mode 100644 index 000000000..5128ceca0 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/services/PreferenceManager.java @@ -0,0 +1,142 @@ +/* + * 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.services; + +import android.content.SharedPreferences; +import android.util.Log; + +import com.google.android.apps.mytracks.MyTracksConstants; +import com.google.android.apps.mytracks.MyTracksSettings; +import com.google.android.maps.mytracks.R; + +/** + * A class that manages reading the shared preferences for the service. + * + * @author Sandor Dornbush + */ +public class PreferenceManager { + private TrackRecordingService service; + private String announcement_frequency_key; + private String max_recording_distance_key; + private String metric_units_key; + private String min_recording_distance_key; + private String min_recording_interval_key; + private String min_required_accuracy_key; + private String signal_sampling_frequency_key; + private String split_frequency_key; + + public PreferenceManager(TrackRecordingService service) { + this.service = service; + announcement_frequency_key = + service.getString(R.string.announcement_frequency_key); + max_recording_distance_key = + service.getString(R.string.max_recording_distance_key); + metric_units_key = + service.getString(R.string.metric_units_key); + min_recording_distance_key = + service.getString(R.string.min_recording_distance_key); + min_recording_interval_key = + service.getString(R.string.min_recording_interval_key); + min_required_accuracy_key = + service.getString(R.string.min_required_accuracy_key); + split_frequency_key = + service.getString(R.string.split_frequency_key); + signal_sampling_frequency_key = + service.getString(R.string.signal_sampling_frequency_key); + } + + /** + * Notifies that preferences have changed. + * Call this with key == null to update all preferences in one call. + * + * @param key the key that changed (may be null to update all preferences) + */ + public void onSharedPreferenceChanged(String key) { + SharedPreferences sharedPreferences = + service.getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0); + if (sharedPreferences == null) { + Log.w(MyTracksConstants.TAG, + "TrackRecordingService: Couldn't get shared preferences."); + return; + } + + if (key == null || key.equals(min_recording_distance_key)) { + service.setMinRecordingDistance( + sharedPreferences.getInt( + min_recording_distance_key, + MyTracksSettings.DEFAULT_MIN_RECORDING_DISTANCE)); + Log.d(MyTracksConstants.TAG, + "TrackRecordingService: minRecordingDistance = " + + service.getMinRecordingDistance()); + } + if (key == null || key.equals(max_recording_distance_key)) { + service.setMaxRecordingDistance( + sharedPreferences.getInt( + max_recording_distance_key, + MyTracksSettings.DEFAULT_MAX_RECORDING_DISTANCE)); + } + if (key == null || key.equals(min_recording_interval_key)) { + int minRecordingInterval = sharedPreferences.getInt( + min_recording_interval_key, + MyTracksSettings.DEFAULT_MIN_RECORDING_INTERVAL); + switch (minRecordingInterval) { + case -2: + // Battery Miser + // min: 30 seconds + // max: 5 minutes + // minDist: 5 meters Choose battery life over moving time accuracy. + service.setLocationListenerPolicy( + new AdaptiveLocationListenerPolicy(30000, 300000, 5)); + break; + case -1: + // High Accuracy + // min: 1 second + // max: 30 seconds + // minDist: 0 meters get all updates to properly measure moving time. + service.setLocationListenerPolicy( + new AdaptiveLocationListenerPolicy(1000, 30000, 0)); + break; + default: + service.setLocationListenerPolicy( + new AbsoluteLocationListenerPolicy(minRecordingInterval * 1000)); + } + } + if (key == null || key.equals(min_required_accuracy_key)) { + service.setMinRequiredAccuracy( + sharedPreferences.getInt( + min_required_accuracy_key, + MyTracksSettings.DEFAULT_MIN_REQUIRED_ACCURACY)); + } + if (key == null || key.equals(announcement_frequency_key)) { + service.setAnnouncementFrequency( + sharedPreferences.getInt(announcement_frequency_key, + -1)); + } + if (key == null || key.equals(split_frequency_key)) { + service.getSplitManager().setSplitFrequency( + sharedPreferences.getInt(split_frequency_key, 0)); + } + if (key == null || key.equals(signal_sampling_frequency_key)) { + service.getSignalManager().setFrequency( + sharedPreferences.getInt( + signal_sampling_frequency_key, -1), service); + } + if (key == null || key.equals(metric_units_key)) { + service.getSplitManager().setMetricUnits(sharedPreferences.getBoolean( + metric_units_key, true)); + } + } +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/StatusAnnouncerTask.java b/MyTracks/src/com/google/android/apps/mytracks/services/StatusAnnouncerTask.java index d0ec9a41e..ba0f0cf52 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/StatusAnnouncerTask.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/StatusAnnouncerTask.java @@ -116,10 +116,12 @@ public class StatusAnnouncerTask boolean metricUnits = true; boolean reportSpeed = true; if (preferences != null) { - metricUnits = preferences.getBoolean(MyTracksSettings.METRIC_UNITS, - true); - reportSpeed = preferences.getBoolean(MyTracksSettings.REPORT_SPEED, - true); + metricUnits = + preferences.getBoolean(context.getString(R.string.metric_units_key), + true); + reportSpeed = + preferences.getBoolean(context.getString(R.string.report_speed_key), + true); } double d = stats.getTotalDistance() / 1000; 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 fc952387d..026cd6b56 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/TrackRecordingService.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/TrackRecordingService.java @@ -1,12 +1,12 @@ /* * 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 @@ -36,7 +36,6 @@ import android.app.Service; import android.content.ContentValues; import android.content.Context; import android.content.Intent; -import android.content.SharedPreferences; import android.database.Cursor; import android.database.sqlite.SQLiteException; import android.location.Location; @@ -56,7 +55,7 @@ import java.util.TimerTask; /** * A background service that registers a location listener and records track * points. Track points are saved to the MyTracksProvider. - * + * * @author Leif Hendrik Wilden */ public class TrackRecordingService extends Service implements LocationListener { @@ -68,8 +67,6 @@ public class TrackRecordingService extends Service implements LocationListener { private LocationManager locationManager; private WakeLock wakeLock; - private int minRecordingInterval = - MyTracksSettings.DEFAULT_MIN_RECORDING_INTERVAL; private int minRecordingDistance = MyTracksSettings.DEFAULT_MIN_RECORDING_DISTANCE; private int maxRecordingDistance = @@ -110,8 +107,10 @@ public class TrackRecordingService extends Service implements LocationListener { private TaskExecuterManager signalManager; private SplitManager splitManager; + private PreferenceManager prefManager; + /** - * The interval in milliseconds that we have requested to be notfied of gps + * The interval in milliseconds that we have requested to be notified of gps * readings. */ private long currentRecordingInterval = 0; @@ -126,7 +125,7 @@ public class TrackRecordingService extends Service implements LocationListener { * Task invoked by a timer periodically to make sure the location listener is * still registered. */ - private TimerTask checkLocationListener = new TimerTask() { + private final TimerTask checkLocationListener = new TimerTask() { @Override public void run() { if (!onCreateWasCalled) { @@ -198,7 +197,7 @@ public class TrackRecordingService extends Service implements LocationListener { /** * Inserts a new location in the track points db and updates the corresponding * track in the track db. - * + * * @param recordingTrack the track that is currently being recorded * @param location the location to be inserted * @param lastRecordedLocation the last recorded location before this one (or @@ -475,8 +474,7 @@ public class TrackRecordingService extends Service implements LocationListener { } // This should never happen, but just in case (we really don't want the - // service - // to crash): + // service to crash): if (location == null) { Log.w(MyTracksConstants.TAG, "Location changed, but location is null."); @@ -559,8 +557,7 @@ public class TrackRecordingService extends Service implements LocationListener { } // If separation from last recorded point is too large insert a - // separator - // to indicate end of a segment: + // separator to indicate end of a segment: boolean startNewSegment = lastRecordedLocation != null && lastRecordedLocation.getLatitude() < 90 @@ -620,7 +617,7 @@ public class TrackRecordingService extends Service implements LocationListener { */ /** - * Notifies that preferences have changed. + * Notifies that preferences have changed. * Call this with key == null to update all preferences in one call. * * @param key the key that changed (may be null to update all preferences) @@ -628,93 +625,7 @@ public class TrackRecordingService extends Service implements LocationListener { public void onSharedPreferenceChanged(String key) { Log.d(MyTracksConstants.TAG, "TrackRecordingService.onSharedPreferenceChanged"); - SharedPreferences sharedPreferences = - getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0); - if (sharedPreferences == null) { - Log.w(MyTracksConstants.TAG, - "TrackRecordingService: Couldn't get shared preferences."); - return; - } - - if (key == null || key.equals(MyTracksSettings.MIN_RECORDING_DISTANCE)) { - minRecordingDistance = sharedPreferences.getInt( - MyTracksSettings.MIN_RECORDING_DISTANCE, - MyTracksSettings.DEFAULT_MIN_RECORDING_DISTANCE); - Log.d(MyTracksConstants.TAG, - "TrackRecordingService: minRecordingDistance = " - + minRecordingDistance); - } - if (key == null || key.equals(MyTracksSettings.MAX_RECORDING_DISTANCE)) { - maxRecordingDistance = sharedPreferences.getInt( - MyTracksSettings.MAX_RECORDING_DISTANCE, - MyTracksSettings.DEFAULT_MAX_RECORDING_DISTANCE); - } - if (key == null || key.equals(MyTracksSettings.MIN_RECORDING_INTERVAL)) { - minRecordingInterval = sharedPreferences.getInt( - MyTracksSettings.MIN_RECORDING_INTERVAL, - MyTracksSettings.DEFAULT_MIN_RECORDING_INTERVAL); - switch (minRecordingInterval) { - case -2: - // Battery Miser - // min: 30 seconds - // max: 5 minutes - // minDist: 5 meters Choose battery life over moving time accuracy. - locationListenerPolicy = - new AdaptiveLocationListenerPolicy(30000, 300000, 5); - break; - case -1: - // High Accuracy - // min: 1 second - // max: 30 seconds - // minDist: 0 meters get all updates to properly measure moving time. - locationListenerPolicy = - new AdaptiveLocationListenerPolicy(1000, 30000, 0); - break; - default: - locationListenerPolicy = - new AbsoluteLocationListenerPolicy(minRecordingInterval * 1000); - } - } - if (key == null || key.equals(MyTracksSettings.MIN_REQUIRED_ACCURACY)) { - minRequiredAccuracy = sharedPreferences.getInt( - MyTracksSettings.MIN_REQUIRED_ACCURACY, - MyTracksSettings.DEFAULT_MIN_REQUIRED_ACCURACY); - } - if (key == null || key.equals(MyTracksSettings.RECORDING_TRACK)) { - recordingTrackId = - sharedPreferences.getLong(MyTracksSettings.RECORDING_TRACK, -1); - } - if (key == null || key.equals(MyTracksSettings.ANNOUNCEMENT_FREQUENCY)) { - announcementFrequency = - sharedPreferences.getInt(MyTracksSettings.ANNOUNCEMENT_FREQUENCY, -1); - if (mTTSAvailable) { - if (announcementFrequency == -1) { - if (executer != null) { - executer.shutdown(); - executer = null; - } - } else { - if (executer == null) { - SafeStatusAnnouncerTask announcer = - new SafeStatusAnnouncerTask(this); - executer = new PeriodicTaskExecuter(announcer, this); - } - executer.scheduleTask(announcementFrequency * 60000); - } - } - } - if (key == null || key.equals(MyTracksSettings.SPLIT_FREQUENCY)) { - splitManager.setSplitFrequency( - sharedPreferences.getInt(MyTracksSettings.SPLIT_FREQUENCY, 0)); - } - if (key == null || key.equals(MyTracksSettings.SIGNAL_SAMPLING_FREQUENCY)) { - signalManager.setFrequency(sharedPreferences.getInt( - MyTracksSettings.SIGNAL_SAMPLING_FREQUENCY, -1), this); - } - if (key == null || key.equals(MyTracksSettings.METRIC_UNITS)) { - splitManager.setMetricUnits( - sharedPreferences.getBoolean(MyTracksSettings.METRIC_UNITS, true)); - } + prefManager.onSharedPreferenceChanged(key); if (isRecording) { registerLocationListener(); @@ -730,7 +641,6 @@ public class TrackRecordingService extends Service implements LocationListener { Log.d(MyTracksConstants.TAG, "TrackRecordingService.onCreate"); super.onCreate(); onCreateWasCalled = true; - setForeground(true); providerUtils = MyTracksProviderUtils.Factory.get(this); notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); @@ -748,6 +658,7 @@ public class TrackRecordingService extends Service implements LocationListener { signalManager = new TaskExecuterManager(-1, new SignalStrengthTask(this), this); } + prefManager = new PreferenceManager(this); onSharedPreferenceChanged(null); restoreStats(); registerLocationListener(); @@ -815,7 +726,7 @@ public class TrackRecordingService extends Service implements LocationListener { /** * Inserts a statistics marker. A statistics marker holds the stats for the * last segment up to this marker. - * + * * @param location the location where to insert * @return the unique id of the inserted marker */ @@ -872,7 +783,7 @@ public class TrackRecordingService extends Service implements LocationListener { public boolean hasRecorded() { return providerUtils.getLastTrackId() >= 0; } - + @Override public long startNewTrack() { Log.d(MyTracksConstants.TAG, "TrackRecordingService.startNewTrack"); @@ -903,11 +814,11 @@ public class TrackRecordingService extends Service implements LocationListener { signalManager.restore(); return trackId; } - + /** * Insert the given waypoint marker. Users can insert waypoint markers * to tag locations with a name, description, category etc. - * + * * @param waypoint a waypoint * @return the unique id of the inserted marker */ @@ -915,11 +826,11 @@ public class TrackRecordingService extends Service implements LocationListener { public long insertWaypointMarker(Waypoint waypoint) { return TrackRecordingService.this.insertWaypointMarker(waypoint); } - + /** * Insert a statistics marker. A statistics marker holds the stats for * the last segment up to this marker. - * + * * @param location the location where to insert * @return the unique id of the inserted marker */ @@ -927,7 +838,7 @@ public class TrackRecordingService extends Service implements LocationListener { public long insertStatisticsMarker(Location location) { return TrackRecordingService.this.insertStatisticsMarker(location); } - + @Override public void endCurrentTrack() { Log.d(MyTracksConstants.TAG, "TrackRecordingService.endCurrentTrack"); @@ -952,18 +863,18 @@ public class TrackRecordingService extends Service implements LocationListener { showNotification(); recordingTrackId = -1; } - + @Override public void deleteAllTracks() { endCurrentTrack(); providerUtils.deleteAllTracks(); } - + @Override public void recordLocation(Location loc) { onLocationChanged(loc); } - + @Override public void sharedPreferenceChanged(String key) { Log.d(MyTracksConstants.TAG, @@ -983,4 +894,68 @@ public class TrackRecordingService extends Service implements LocationListener { long getRecordingTrackId() { return recordingTrackId; } + + public int getAnnouncementFrequency() { + return announcementFrequency; + } + + public void setAnnouncementFrequency(int announcementFrequency) { + this.announcementFrequency = announcementFrequency; + if (mTTSAvailable) { + if (announcementFrequency == -1) { + if (executer != null) { + executer.shutdown(); + executer = null; + } + } else { + if (executer == null) { + SafeStatusAnnouncerTask announcer = + new SafeStatusAnnouncerTask(this); + executer = new PeriodicTaskExecuter(announcer, this); + } + executer.scheduleTask(announcementFrequency * 60000); + } + } + } + + public int getMaxRecordingDistance() { + return maxRecordingDistance; + } + + public void setMaxRecordingDistance(int maxRecordingDistance) { + this.maxRecordingDistance = maxRecordingDistance; + } + + public int getMinRecordingDistance() { + return minRecordingDistance; + } + + public void setMinRecordingDistance(int minRecordingDistance) { + this.minRecordingDistance = minRecordingDistance; + } + + public int getMinRequiredAccuracy() { + return minRequiredAccuracy; + } + + public void setMinRequiredAccuracy(int minRequiredAccuracy) { + this.minRequiredAccuracy = minRequiredAccuracy; + } + + public LocationListenerPolicy getLocationListenerPolicy() { + return locationListenerPolicy; + } + + public void setLocationListenerPolicy( + LocationListenerPolicy locationListenerPolicy) { + this.locationListenerPolicy = locationListenerPolicy; + } + + public SplitManager getSplitManager() { + return splitManager; + } + + public TaskExecuterManager getSignalManager() { + return signalManager; + } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/stats/TripStatistics.java b/MyTracks/src/com/google/android/apps/mytracks/stats/TripStatistics.java index 85c7f421c..160c237f6 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/stats/TripStatistics.java +++ b/MyTracks/src/com/google/android/apps/mytracks/stats/TripStatistics.java @@ -159,7 +159,7 @@ public class TripStatistics implements Parcelable { * stopped */ public long getStopTime() { - return startTime; + return stopTime; } /** diff --git a/MyTracks/src/com/google/android/apps/mytracks/stats/TripStatisticsBuilder.java b/MyTracks/src/com/google/android/apps/mytracks/stats/TripStatisticsBuilder.java index 9724ba727..f0107e832 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/stats/TripStatisticsBuilder.java +++ b/MyTracks/src/com/google/android/apps/mytracks/stats/TripStatisticsBuilder.java @@ -347,6 +347,8 @@ public class TripStatisticsBuilder { * Returns the amount of time the user has been idle or 0 if they are moving. */ public long getIdleTime() { + if (lastLocation == null || lastMovingLocation == null) + return 0; return lastLocation.getTime() - lastMovingLocation.getTime(); } diff --git a/MyTracks/src/com/google/android/apps/mytracks/util/ChartURLGenerator.java b/MyTracks/src/com/google/android/apps/mytracks/util/ChartURLGenerator.java index f232dbf51..bd721512a 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/util/ChartURLGenerator.java +++ b/MyTracks/src/com/google/android/apps/mytracks/util/ChartURLGenerator.java @@ -53,7 +53,8 @@ public class ChartURLGenerator { context.getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0); boolean metricUnits = true; if (preferences != null) { - metricUnits = preferences.getBoolean(MyTracksSettings.METRIC_UNITS, true); + metricUnits = preferences.getBoolean( + context.getString(R.string.metric_units_key), true); } return getChartUrl(distances, elevations, track, diff --git a/MyTracks/src/com/google/android/apps/mytracks/util/FileUtils.java b/MyTracks/src/com/google/android/apps/mytracks/util/FileUtils.java new file mode 100644 index 000000000..c2a380e88 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/util/FileUtils.java @@ -0,0 +1,151 @@ +/* + * Copyright 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.google.android.apps.mytracks.util; + +import com.google.android.apps.mytracks.MyTracksConstants; + +import android.os.Environment; + +import java.io.File; +import java.util.regex.Pattern; + +/** + * Utilities for dealing with files. + * + * @author Rodrigo Damazio + */ +public class FileUtils { + /** + * The maximum length of a filename, as per the FAT32 specification. + */ + private static final int MAX_FILENAME_LENGTH = 260; + + /** + * A set of characters that are prohibited from being in file names. + */ + private static final Pattern PROHIBITED_CHAR_PATTERN = + Pattern.compile("[^ A-Za-z0-9_.()]+"); + + /** + * Builds a path inside the My Tracks directory in the SD card. + * + * @param components the path components inside the mytracks directory + * @return the full path to the destination + */ + public String buildExternalDirectoryPath(String... components) { + StringBuilder dirNameBuilder = new StringBuilder(); + dirNameBuilder.append(Environment.getExternalStorageDirectory()); + dirNameBuilder.append(File.separatorChar); + dirNameBuilder.append(MyTracksConstants.SDCARD_TOP_DIR); + for (String component : components) { + dirNameBuilder.append(File.separatorChar); + dirNameBuilder.append(component); + } + return dirNameBuilder.toString(); + } + + /** + * Returns whether the SD card is available. + */ + public boolean isSdCardAvailable() { + return Environment.MEDIA_MOUNTED.equals( + Environment.getExternalStorageState()); + } + + /** + * Normalizes the input string and make sure it is a valid fat32 file name. + * + * @param name the name to normalize + * @param overheadSize the number of additional characters that will be added + * to the name after sanitization + * @return the sanitized name + */ + String sanitizeName(String name) { + String cleaned = PROHIBITED_CHAR_PATTERN.matcher(name).replaceAll(""); + + return (cleaned.length() > MAX_FILENAME_LENGTH) + ? cleaned.substring(0, MAX_FILENAME_LENGTH) + : cleaned.toString(); + } + + /** + * Ensures the given directory exists by creating it and its parents if + * necessary. + * + * @return whether the directory exists (either already existed or was + * successfully created) + */ + public boolean ensureDirectoryExists(File dir) { + if (dir.exists() && dir.isDirectory()) { + return true; + } + + if (dir.mkdirs()) { + return true; + } + + return false; + } + + /** + * Builds a filename with the given base name (prefix) and the given + * extension, possibly adding a suffix to ensure the file doesn't exist. + * + * @param directory the directory the file will live in + * @param fileBaseName the prefix for the file name + * @param extension the file's extension + * @return the complete file name, without the directory + */ + public synchronized String buildUniqueFileName(File directory, + String fileBaseName, String extension) { + return buildUniqueFileName(directory, fileBaseName, extension, 0); + } + + /** + * Builds a filename with the given base name (prefix) and the given + * extension, possibly adding a suffix to ensure the file doesn't exist. + * + * @param directory the directory the file will live in + * @param fileBaseName the prefix for the file name + * @param extension the file's extension + * @param suffix the first numeric suffix to try to use, or 0 for none + * @return the complete file name, without the directory + */ + private String buildUniqueFileName(File directory, String fileBaseName, + String extension, int suffix) { + String suffixedBaseName = fileBaseName; + if (suffix > 0) { + suffixedBaseName += " (" + Integer.toString(suffix) + ")"; + } + + String fullName = suffixedBaseName + "." + extension; + String sanitizedName = sanitizeName(fullName); + if (!fileExists(directory, sanitizedName)) { + return sanitizedName; + } + + return buildUniqueFileName(directory, fileBaseName, extension, suffix + 1); + } + + /** + * Checks whether a file with the given name exists in the given directory. + * This is isolated so it can be overridden in tests. + */ + protected boolean fileExists(File directory, String fullName) { + File file = new File(directory, fullName); + return file.exists(); + } +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/util/StringUtils.java b/MyTracks/src/com/google/android/apps/mytracks/util/StringUtils.java index d5a8ed34b..c4c0b2317 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/util/StringUtils.java +++ b/MyTracks/src/com/google/android/apps/mytracks/util/StringUtils.java @@ -173,7 +173,7 @@ public class StringUtils implements DescriptionGenerator { context.getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0); if (preferences != null) { displaySpeed = - preferences.getBoolean(MyTracksSettings.REPORT_SPEED, true); + preferences.getBoolean(context.getString(R.string.report_speed_key), true); } TripStatistics trackStats = track.getStatistics(); 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()); + } + } +} diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/io/TcxTrackWriterTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/io/TcxTrackWriterTest.java new file mode 100644 index 000000000..ec132e0db --- /dev/null +++ b/MyTracksTest/src/com/google/android/apps/mytracks/io/TcxTrackWriterTest.java @@ -0,0 +1,66 @@ +// Copyright 2010 Google Inc. All Rights Reserved. +package com.google.android.apps.mytracks.io; + +import android.location.Location; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import java.util.Date; +import java.util.List; + +/** + * Tests for the GPX track exporter. + * + * @author Sandor Dornbush + */ +public class TcxTrackWriterTest extends TrackFormatWriterTest { + + public void testXmlOutput() throws Exception { + TrackFormatWriter writer = new TcxTrackWriter(); + String result = writeTrack(writer); + Document doc = parseXmlDocument(result); + + Element root = getChildElement(doc, "TrainingCenterDatabase"); + Element activitiesTag = getChildElement(root, "Activities"); + Element activityTag = getChildElement(activitiesTag, "Activity"); + Element lapTag = getChildElement(activityTag, "Lap"); + + List segmentTags = getChildElements(lapTag, "Track", 2); + Element segment1Tag = segmentTags.get(0); + Element segment2Tag = segmentTags.get(1); + List seg1PointTags = getChildElements(segment1Tag, "Trackpoint", 2); + List seg2PointTags = getChildElements(segment2Tag, "Trackpoint", 2); + assertTagsMatchPoints(seg1PointTags, location1, location2); + assertTagsMatchPoints(seg2PointTags, location3, location4); + } + + /** + * Asserts that the given tags describe the given points, in the same order. + */ + private void assertTagsMatchPoints(List tags, Location... locs) { + assertEquals(locs.length, tags.size()); + for (int i = 0; i < locs.length; i++) { + Element tag = tags.get(i); + Location loc = locs[i]; + + assertTagMatchesLocation(tag, loc); + } + } + + /** + * Asserts that the given tag describes the given location. + */ + private void assertTagMatchesLocation(Element tag, Location loc) { + Element posTag = getChildElement(tag, "Position"); + assertEquals(Double.toString(loc.getLatitude()), + getChildTextValue(posTag, "LatitudeDegrees")); + assertEquals(Double.toString(loc.getLongitude()), + getChildTextValue(posTag, "LongitudeDegrees")); + assertEquals( + TcxTrackWriter.TIMESTAMP_FORMAT.format(new Date(loc.getTime())), + getChildTextValue(tag, "Time")); + assertEquals(Double.toString(loc.getAltitude()), + getChildTextValue(tag, "AltitudeMeters")); + } +} diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/io/TrackWriterTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/io/TrackWriterTest.java index 05de8e7d3..ecb05224d 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/io/TrackWriterTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/io/TrackWriterTest.java @@ -2,6 +2,10 @@ package com.google.android.apps.mytracks.io; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.isA; +import static org.easymock.EasyMock.same; + import com.google.android.apps.mytracks.MyTracksConstants; import com.google.android.apps.mytracks.content.MyTracksProviderUtils; import com.google.android.apps.mytracks.content.MyTracksProviderUtils.Factory; @@ -15,18 +19,14 @@ import android.database.Cursor; import android.location.Location; import android.test.AndroidTestCase; -import org.easymock.EasyMock; -import org.easymock.IAnswer; - -import static org.easymock.EasyMock.expect; -import static org.easymock.EasyMock.isA; -import static org.easymock.EasyMock.same; -import org.easymock.IMocksControl; - import java.io.ByteArrayOutputStream; import java.io.FileNotFoundException; import java.io.OutputStream; +import org.easymock.EasyMock; +import org.easymock.IAnswer; +import org.easymock.IMocksControl; + /** * Tests for the track writer. * @@ -115,11 +115,10 @@ public class TrackWriterTest extends AndroidTestCase { } private static final long TRACK_ID = 1234567L; - private static final String DEFAULT_DIR = "default/dir"; private static final String EXTENSION = "ext"; private static final String TRACK_NAME = "Swimming across the pacific"; - private static final String TRACK_SANITIZED_NAME = "Swimmingacrossthepacific"; - private static final String FULL_TRACK_NAME = "Swimmingacrossthepacific.ext"; + private static final String FULL_TRACK_NAME = + "Swimming across the pacific.ext"; private Track track; private TrackFormatWriter formatWriter; @@ -142,7 +141,6 @@ public class TrackWriterTest extends AndroidTestCase { oldProviderUtilsFactory = TestingProviderUtilsFactory.installWithInstance(providerUtils); - expect(formatWriter.getDefaultDirectory()).andStubReturn(DEFAULT_DIR); expect(formatWriter.getExtension()).andStubReturn(EXTENSION); track = new Track(); @@ -190,10 +188,6 @@ public class TrackWriterTest extends AndroidTestCase { mocksControl.verify(); } - public void testSanitizeName() { - assertEquals(TRACK_SANITIZED_NAME, TrackWriter.sanitizeName(TRACK_NAME)); - } - public void testOpenFile() { final ByteArrayOutputStream stream = new ByteArrayOutputStream(); writer = new OpenFileTrackWriter( diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/io/backup/DatabaseDumperTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/io/backup/DatabaseDumperTest.java new file mode 100644 index 000000000..4316ff3b4 --- /dev/null +++ b/MyTracksTest/src/com/google/android/apps/mytracks/io/backup/DatabaseDumperTest.java @@ -0,0 +1,212 @@ +/* + * 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.backup; + +import com.google.android.apps.mytracks.content.ContentTypeIds; + +import android.database.MatrixCursor; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; + +import junit.framework.TestCase; + +/** + * Tests for {@link DatabaseDumper}. + * + * @author Rodrigo Damazio + */ +public class DatabaseDumperTest extends TestCase { + + private static final String[] COLUMN_NAMES = { + "intCol", "longCol", "floatCol", "doubleCol", "stringCol", "boolCol" + }; + private static final byte[] COLUMN_TYPES = { + ContentTypeIds.INT_TYPE_ID, ContentTypeIds.LONG_TYPE_ID, + ContentTypeIds.FLOAT_TYPE_ID, ContentTypeIds.DOUBLE_TYPE_ID, + ContentTypeIds.STRING_TYPE_ID, ContentTypeIds.BOOLEAN_TYPE_ID + }; + private static final String[][] FAKE_DATA = { + { "42", "123456789", "3.1415", "2.72", "lolcat", "1" }, + { null, "123456789", "3.1415", "2.72", "lolcat", "1" }, + { "42", null, "3.1415", "2.72", "lolcat", "1" }, + { "42", "123456789", null, "2.72", "lolcat", "1" }, + { "42", "123456789", "3.1415", null, "lolcat", "1" }, + { "42", "123456789", "3.1415", "2.72", null, "1" }, + { "42", "123456789", "3.1415", "2.72", "lolcat", null }, + }; + private static final long[] EXPECTED_FIELD_SETS = { + 0x3F, 0x3E, 0x3D, 0x3B, 0x37, 0x2F, 0x1F + }; + + private MatrixCursor cursor; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + // Add fake data to the cursor + cursor = new MatrixCursor(COLUMN_NAMES); + for (String[] row : FAKE_DATA) { + cursor.addRow(row); + } + } + + public void testWriteAllRows_noNulls() throws Exception { + testWriteAllRows(false); + } + + public void testWriteAllRows_withNulls() throws Exception { + testWriteAllRows(true); + } + + private void testWriteAllRows(boolean hasNullFields) throws Exception { + // Dump it + DatabaseDumper dumper = new DatabaseDumper(COLUMN_NAMES, COLUMN_TYPES, hasNullFields); + ByteArrayOutputStream outStream = new ByteArrayOutputStream(1024); + DataOutputStream writer = new DataOutputStream(outStream ); + dumper.writeAllRows(cursor, writer); + + // Read the results + byte[] result = outStream.toByteArray(); + ByteArrayInputStream inputStream = new ByteArrayInputStream(result); + DataInputStream reader = new DataInputStream(inputStream); + + // Verify the header + assertHeader(reader); + + // Verify the number of rows + assertEquals(FAKE_DATA.length, reader.readInt()); + + // Verify the row contents + + // Row 0 + assertEquals(EXPECTED_FIELD_SETS[0], reader.readLong()); + assertEquals(42, reader.readInt()); + assertEquals(123456789L, reader.readLong()); + assertEquals(3.1415f, reader.readFloat()); + assertEquals(2.72, reader.readDouble()); + assertEquals("lolcat", reader.readUTF()); + assertTrue(reader.readBoolean()); + + // Row 1 + assertEquals(EXPECTED_FIELD_SETS[1], reader.readLong()); + if (hasNullFields) reader.readInt(); + assertEquals(123456789L, reader.readLong()); + assertEquals(3.1415f, reader.readFloat()); + assertEquals(2.72, reader.readDouble()); + assertEquals("lolcat", reader.readUTF()); + assertTrue(reader.readBoolean()); + + // Row 2 + assertEquals(EXPECTED_FIELD_SETS[2], reader.readLong()); + assertEquals(42, reader.readInt()); + if (hasNullFields) reader.readLong(); + assertEquals(3.1415f, reader.readFloat()); + assertEquals(2.72, reader.readDouble()); + assertEquals("lolcat", reader.readUTF()); + assertTrue(reader.readBoolean()); + + // Row 3 + assertEquals(EXPECTED_FIELD_SETS[3], reader.readLong()); + assertEquals(42, reader.readInt()); + assertEquals(123456789L, reader.readLong()); + if (hasNullFields) reader.readFloat(); + assertEquals(2.72, reader.readDouble()); + assertEquals("lolcat", reader.readUTF()); + assertTrue(reader.readBoolean()); + + // Row 4 + assertEquals(EXPECTED_FIELD_SETS[4], reader.readLong()); + assertEquals(42, reader.readInt()); + assertEquals(123456789L, reader.readLong()); + assertEquals(3.1415f, reader.readFloat()); + if (hasNullFields) reader.readDouble(); + assertEquals("lolcat", reader.readUTF()); + assertTrue(reader.readBoolean()); + + // Row 5 + assertEquals(EXPECTED_FIELD_SETS[5], reader.readLong()); + assertEquals(42, reader.readInt()); + assertEquals(123456789L, reader.readLong()); + assertEquals(3.1415f, reader.readFloat()); + assertEquals(2.72, reader.readDouble()); + if (hasNullFields) reader.readUTF(); + assertTrue(reader.readBoolean()); + + // Row 6 + assertEquals(EXPECTED_FIELD_SETS[6], reader.readLong()); + assertEquals(42, reader.readInt()); + assertEquals(123456789L, reader.readLong()); + assertEquals(3.1415f, reader.readFloat()); + assertEquals(2.72, reader.readDouble()); + assertEquals("lolcat", reader.readUTF()); + if (hasNullFields) reader.readBoolean(); + } + + public void testFewerRows() throws Exception { + // Dump only the first two rows + DatabaseDumper dumper = new DatabaseDumper(COLUMN_NAMES, COLUMN_TYPES, false); + ByteArrayOutputStream outStream = new ByteArrayOutputStream(1024); + DataOutputStream writer = new DataOutputStream(outStream); + + dumper.writeHeaders(cursor, 2, writer); + cursor.moveToFirst(); + dumper.writeOneRow(cursor, writer); + cursor.moveToNext(); + dumper.writeOneRow(cursor, writer); + + // Read the results + byte[] result = outStream.toByteArray(); + ByteArrayInputStream inputStream = new ByteArrayInputStream(result); + DataInputStream reader = new DataInputStream(inputStream); + + // Verify the header + assertHeader(reader); + + // Verify the number of rows + assertEquals(2, reader.readInt()); + + // Row 0 + assertEquals(EXPECTED_FIELD_SETS[0], reader.readLong()); + assertEquals(42, reader.readInt()); + assertEquals(123456789L, reader.readLong()); + assertEquals(3.1415f, reader.readFloat()); + assertEquals(2.72, reader.readDouble()); + assertEquals("lolcat", reader.readUTF()); + assertTrue(reader.readBoolean()); + + // Row 1 + assertEquals(EXPECTED_FIELD_SETS[1], reader.readLong()); + // Null field not read + assertEquals(123456789L, reader.readLong()); + assertEquals(3.1415f, reader.readFloat()); + assertEquals(2.72, reader.readDouble()); + assertEquals("lolcat", reader.readUTF()); + assertTrue(reader.readBoolean()); + } + + private void assertHeader(DataInputStream reader) throws IOException { + assertEquals(6, reader.readInt()); + for (int i = 0; i < COLUMN_NAMES.length; i++) { + assertEquals(COLUMN_NAMES[i], reader.readUTF()); + assertEquals(COLUMN_TYPES[i], reader.readByte()); + } + } +} diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/io/backup/DatabaseImporterTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/io/backup/DatabaseImporterTest.java new file mode 100644 index 000000000..05b13aeaa --- /dev/null +++ b/MyTracksTest/src/com/google/android/apps/mytracks/io/backup/DatabaseImporterTest.java @@ -0,0 +1,301 @@ +/* + * 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.backup; + +import com.google.android.apps.mytracks.content.ContentTypeIds; + +import android.content.ContentValues; +import android.net.Uri; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.util.ArrayList; + +import junit.framework.TestCase; + +/** + * Tests for {@link DatabaseImporter}. + * + * @author Rodrigo Damazio + */ +public class DatabaseImporterTest extends TestCase { + + private static final Uri DESTINATION_URI = Uri.parse("http://www.google.com/"); + private static final int TEST_BULK_SIZE = 10; + private ArrayList insertedValues; + + private class TestableDatabaseImporter extends DatabaseImporter { + public TestableDatabaseImporter(boolean readNullFields) { + super(DESTINATION_URI, null, readNullFields, TEST_BULK_SIZE); + } + + @Override + protected void doBulkInsert(ContentValues[] values) { + insertedValues.ensureCapacity(insertedValues.size() + values.length); + + // We need to make a copy of the values since the objects are re-used + for (ContentValues contentValues : values) { + insertedValues.add(new ContentValues(contentValues)); + } + } + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + + insertedValues = new ArrayList(); + } + + public void testImportAllRows() throws Exception { + testImportAllRows(false); + } + + public void testImportAllRows_readNullFields() throws Exception { + testImportAllRows(true); + } + + private void testImportAllRows(boolean readNullFields) throws Exception { + // Create a fake data stream to be read + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(1024); + DataOutputStream writer = new DataOutputStream(outputStream); + + // Add the header + writer.writeInt(6); + writer.writeUTF("col1"); + writer.writeByte(ContentTypeIds.INT_TYPE_ID); + writer.writeUTF("col2"); + writer.writeByte(ContentTypeIds.BOOLEAN_TYPE_ID); + writer.writeUTF("col3"); + writer.writeByte(ContentTypeIds.STRING_TYPE_ID); + writer.writeUTF("col4"); + writer.writeByte(ContentTypeIds.FLOAT_TYPE_ID); + writer.writeUTF("col5"); + writer.writeByte(ContentTypeIds.DOUBLE_TYPE_ID); + writer.writeUTF("col6"); + writer.writeByte(ContentTypeIds.LONG_TYPE_ID); + + // Add the number of rows + writer.writeInt(2); + + // Add a row with all fields present + writer.writeLong(0x3F); + writer.writeInt(42); + writer.writeBoolean(true); + writer.writeUTF("lolcat"); + writer.writeFloat(3.1415f); + writer.writeDouble(2.72); + writer.writeLong(123456789L); + + // Add a row with some missing fields + writer.writeLong(0x15); + writer.writeInt(42); + if (readNullFields) writer.writeBoolean(false); + writer.writeUTF("lolcat"); + if (readNullFields) writer.writeFloat(0.0f); + writer.writeDouble(2.72); + if (readNullFields) writer.writeLong(0L); + + writer.flush(); + + // Do the importing + DatabaseImporter importer = new TestableDatabaseImporter(readNullFields); + byte[] dataBytes = outputStream.toByteArray(); + importer.importAllRows(new DataInputStream(new ByteArrayInputStream(dataBytes))); + + assertEquals(2, insertedValues.size()); + + // Verify the first row + ContentValues value = insertedValues.get(0); + assertEquals(value.toString(), 6, value.size()); + + assertValue(42, "col1", value); + assertValue(true, "col2", value); + assertValue("lolcat", "col3", value); + assertValue(3.1415f, "col4", value); + assertValue(2.72, "col5", value); + assertValue(123456789L, "col6", value); + + // Verify the second row + value = insertedValues.get(1); + assertEquals(value.toString(), 3, value.size()); + + assertValue(42, "col1", value); + assertValue("lolcat", "col3", value); + assertValue(2.72, "col5", value); + } + + public void testImportAllRows_noRows() throws Exception { + // Create a fake data stream to be read + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(1024); + DataOutputStream writer = new DataOutputStream(outputStream); + + // Add the header + writer.writeInt(6); + writer.writeUTF("col1"); + writer.writeByte(ContentTypeIds.INT_TYPE_ID); + writer.writeUTF("col2"); + writer.writeByte(ContentTypeIds.BOOLEAN_TYPE_ID); + writer.writeUTF("col3"); + writer.writeByte(ContentTypeIds.STRING_TYPE_ID); + writer.writeUTF("col4"); + writer.writeByte(ContentTypeIds.FLOAT_TYPE_ID); + writer.writeUTF("col5"); + writer.writeByte(ContentTypeIds.DOUBLE_TYPE_ID); + writer.writeUTF("col6"); + writer.writeByte(ContentTypeIds.LONG_TYPE_ID); + + // Add the number of rows + writer.writeInt(0); + + writer.flush(); + + // Do the importing + DatabaseImporter importer = new TestableDatabaseImporter(false); + byte[] dataBytes = outputStream.toByteArray(); + importer.importAllRows(new DataInputStream(new ByteArrayInputStream(dataBytes))); + + assertTrue(insertedValues.isEmpty()); + } + + public void testImportAllRows_emptyRows() throws Exception { + testImportAllRowsWithEmptyRows(false); + } + + public void testImportAllRows_emptyRowsWithNulls() throws Exception { + testImportAllRowsWithEmptyRows(true); + } + + private void testImportAllRowsWithEmptyRows(boolean readNullFields) throws Exception { + // Create a fake data stream to be read + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(1024); + DataOutputStream writer = new DataOutputStream(outputStream); + + // Add the header + writer.writeInt(6); + writer.writeUTF("col1"); + writer.writeByte(ContentTypeIds.INT_TYPE_ID); + writer.writeUTF("col2"); + writer.writeByte(ContentTypeIds.BOOLEAN_TYPE_ID); + writer.writeUTF("col3"); + writer.writeByte(ContentTypeIds.STRING_TYPE_ID); + writer.writeUTF("col4"); + writer.writeByte(ContentTypeIds.FLOAT_TYPE_ID); + writer.writeUTF("col5"); + writer.writeByte(ContentTypeIds.DOUBLE_TYPE_ID); + writer.writeUTF("col6"); + writer.writeByte(ContentTypeIds.LONG_TYPE_ID); + + // Add the number of rows + writer.writeInt(3); + + // Add 2 rows with no fields + for (int i = 0; i < 2; i++) { + writer.writeLong(0); + if (readNullFields) { + writer.writeInt(0); + writer.writeBoolean(false); + writer.writeUTF(""); + writer.writeFloat(0.0f); + writer.writeDouble(0.0); + writer.writeLong(0L); + } + } + + // Add a row with some missing fields + writer.writeLong(0x15); + writer.writeInt(42); + if (readNullFields) writer.writeBoolean(false); + writer.writeUTF("lolcat"); + if (readNullFields) writer.writeFloat(0.0f); + writer.writeDouble(2.72); + if (readNullFields) writer.writeLong(0L); + + writer.flush(); + + // Do the importing + DatabaseImporter importer = new TestableDatabaseImporter(readNullFields); + byte[] dataBytes = outputStream.toByteArray(); + importer.importAllRows(new DataInputStream(new ByteArrayInputStream(dataBytes))); + + assertEquals(insertedValues.toString(), 3, insertedValues.size()); + + ContentValues value = insertedValues.get(0); + assertEquals(value.toString(), 0, value.size()); + + value = insertedValues.get(1); + assertEquals(value.toString(), 0, value.size()); + + // Verify the third row (only one with values) + value = insertedValues.get(2); + assertEquals(value.toString(), 3, value.size()); + + assertFalse(value.containsKey("col2")); + assertFalse(value.containsKey("col4")); + assertFalse(value.containsKey("col6")); + + assertValue(42, "col1", value); + assertValue("lolcat", "col3", value); + assertValue(2.72, "col5", value); + } + + public void testImportAllRows_bulks() throws Exception { + // Create a fake data stream to be read + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(1024); + DataOutputStream writer = new DataOutputStream(outputStream); + + // Add the header + writer.writeInt(2); + writer.writeUTF("col1"); + writer.writeByte(ContentTypeIds.INT_TYPE_ID); + writer.writeUTF("col2"); + writer.writeByte(ContentTypeIds.STRING_TYPE_ID); + + // Add lots of rows (so the insertions are split in multiple bulks) + int numRows = TEST_BULK_SIZE * 5 / 2; + writer.writeInt(numRows); + for (int i = 0; i < numRows; i++) { + writer.writeLong(3); + writer.writeInt(i); + writer.writeUTF(Integer.toString(i * 2)); + } + + writer.flush(); + + // Do the importing + DatabaseImporter importer = new TestableDatabaseImporter(false); + byte[] dataBytes = outputStream.toByteArray(); + importer.importAllRows(new DataInputStream(new ByteArrayInputStream(dataBytes))); + + // Verify the rows + assertEquals(numRows, insertedValues.size()); + for (int i = 0; i < numRows; i++) { + ContentValues value = insertedValues.get(i); + assertEquals(value.toString(), 2, value.size()); + assertValue(i, "col1", value); + assertValue(Integer.toString(i * 2), "col2", value); + } + } + + private void assertValue(T expectedValue, String name, ContentValues values) { + @SuppressWarnings("unchecked") + T value = (T) values.get(name); + assertNotNull(value); + assertEquals(expectedValue, value); + } +} diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/io/backup/PreferenceBackupHelperTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/io/backup/PreferenceBackupHelperTest.java new file mode 100644 index 000000000..0cdd7c626 --- /dev/null +++ b/MyTracksTest/src/com/google/android/apps/mytracks/io/backup/PreferenceBackupHelperTest.java @@ -0,0 +1,197 @@ +/* + * 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.backup; + +import android.content.SharedPreferences; +import android.content.SharedPreferences.Editor; + +import java.util.HashMap; +import java.util.Map; + +import junit.framework.TestCase; + +/** + * Tests for {@link PreferenceBackupHelper}. + * + * @author Rodrigo Damazio + */ +public class PreferenceBackupHelperTest extends TestCase { + private Map preferenceValues; + private SharedPreferences preferences; + private PreferenceBackupHelper preferenceBackupHelper; + + /** + * Mock shared preferences editor which does not persist state. + */ + private class MockPreferenceEditor implements SharedPreferences.Editor { + private Map newPreferences = new HashMap(preferenceValues); + + @Override + public Editor clear() { + newPreferences.clear(); + return this; + } + + @Override + public boolean commit() { + preferenceValues = newPreferences; + return true; + } + + @Override + public Editor putBoolean(String key, boolean value) { + return put(key, value); + } + + @Override + public Editor putFloat(String key, float value) { + return put(key, value); + } + + @Override + public Editor putInt(String key, int value) { + return put(key, value); + } + + @Override + public Editor putLong(String key, long value) { + return put(key, value); + } + + @Override + public Editor putString(String key, String value) { + return put(key, value); + } + + private Editor put(String key, T value) { + newPreferences.put(key, value); + return this; + } + + @Override + public Editor remove(String key) { + newPreferences.remove(key); + return this; + } + } + + /** + * Mock shared preferences which does not persist state. + */ + private class MockPreferences implements SharedPreferences { + @Override + public boolean contains(String key) { + return preferenceValues.containsKey(key); + } + + @Override + public Editor edit() { + return new MockPreferenceEditor(); + } + + @Override + public Map getAll() { + return preferenceValues; + } + + @Override + public boolean getBoolean(String key, boolean defValue) { + return get(key, defValue); + } + + @Override + public float getFloat(String key, float defValue) { + return get(key, defValue); + } + + @Override + public int getInt(String key, int defValue) { + return get(key, defValue); + } + + @Override + public long getLong(String key, long defValue) { + return get(key, defValue); + } + + @Override + public String getString(String key, String defValue) { + return get(key, defValue); + } + + @Override + public void registerOnSharedPreferenceChangeListener( + OnSharedPreferenceChangeListener listener) { + throw new UnsupportedOperationException(); + } + + @Override + public void unregisterOnSharedPreferenceChangeListener( + OnSharedPreferenceChangeListener listener) { + throw new UnsupportedOperationException(); + } + + @SuppressWarnings("unchecked") + private T get(String key, T defValue) { + Object value = preferenceValues.get(key); + if (value == null) return defValue; + return (T) value; + } + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + + preferenceValues = new HashMap(); + preferences = new MockPreferences(); + preferenceBackupHelper = new PreferenceBackupHelper(); + } + + public void testExportImportPreferences() throws Exception { + // Populate with some initial values + Editor editor = preferences.edit(); + editor.clear(); + editor.putBoolean("bool1", true); + editor.putBoolean("bool2", false); + editor.putFloat("flt1", 3.14f); + editor.putInt("int1", 42); + editor.putLong("long1", 123456789L); + editor.putString("str1", "lolcat"); + editor.commit(); + + // Export it + byte[] exported = preferenceBackupHelper.exportPreferences(preferences); + + // Mess with the previous values + editor = preferences.edit(); + editor.clear(); + editor.putString("str2", "Shouldn't be there after restore"); + editor.putBoolean("bool2", true); + editor.commit(); + + // Import it back + preferenceBackupHelper.importPreferences(exported, preferences); + + assertFalse(preferences.contains("str2")); + assertTrue(preferences.getBoolean("bool1", false)); + assertFalse(preferences.getBoolean("bool2", true)); + assertEquals(3.14f, preferences.getFloat("flt1", 0.0f)); + assertEquals(42, preferences.getInt("int1", 0)); + assertEquals(123456789L, preferences.getLong("long1", 0)); + assertEquals("lolcat", preferences.getString("str1", "")); + } +} diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/util/FileUtilsTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/util/FileUtilsTest.java new file mode 100644 index 000000000..715370bab --- /dev/null +++ b/MyTracksTest/src/com/google/android/apps/mytracks/util/FileUtilsTest.java @@ -0,0 +1,95 @@ +/* + * Copyright 2010 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package com.google.android.apps.mytracks.util; + +import com.google.android.apps.mytracks.MyTracksConstants; + +import android.os.Environment; + +import java.io.File; +import java.util.HashSet; +import java.util.Set; + +import junit.framework.TestCase; + +/** + * Tests for {@link FileUtils}. + * + * @author Rodrigo Damazio + */ +public class FileUtilsTest extends TestCase { + private static final String ORIGINAL_NAME = "Swim\10ming ^across: the/ pacific (ocean)."; + private static final String SANITIZED_NAME = "Swimming across the pacific (ocean)."; + + private FileUtils fileUtils; + private Set existingFiles; + + @Override + protected void setUp() throws Exception { + super.setUp(); + + existingFiles = new HashSet(); + fileUtils = new FileUtils() { + @Override + protected boolean fileExists(File directory, String fullName) { + return existingFiles.contains(fullName); + } + }; + } + + public void testBuildExternalDirectoryPath() { + String expectedName = Environment.getExternalStorageDirectory() + + File.separator + + MyTracksConstants.SDCARD_TOP_DIR + + File.separator + + "a" + + File.separator + + "b" + + File.separator + + "c"; + + String dirName = fileUtils.buildExternalDirectoryPath("a", "b", "c"); + assertEquals(expectedName, dirName); + } + + public void testSanitizeName() { + assertEquals(SANITIZED_NAME, fileUtils.sanitizeName(ORIGINAL_NAME)); + } + + public void testBuildUniqueFileName_someExist() { + existingFiles = new HashSet(); + existingFiles.add("Filename.ext"); + existingFiles.add("Filename (1).ext"); + existingFiles.add("Filename (2).ext"); + existingFiles.add("Filename (3).ext"); + existingFiles.add("Filename (4).ext"); + + String filename = fileUtils.buildUniqueFileName(null, "Filename", "ext"); + assertEquals("Filename (5).ext", filename); + } + + public void testBuildUniqueFileName_oneExists() { + existingFiles.add("Filename.ext"); + + String filename = fileUtils.buildUniqueFileName(null, "Filename", "ext"); + assertEquals("Filename (1).ext", filename); + } + + public void testBuildUniqueFileName_noneExists() { + String filename = fileUtils.buildUniqueFileName(null, "Filename", "ext"); + assertEquals("Filename.ext", filename); + } +} diff --git a/eclipse/mytracks-style.xml b/eclipse/mytracks-style.xml new file mode 100644 index 000000000..45afba5bc --- /dev/null +++ b/eclipse/mytracks-style.xml @@ -0,0 +1,269 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/eclipse/mytracks.importorder b/eclipse/mytracks.importorder new file mode 100644 index 000000000..794a97650 --- /dev/null +++ b/eclipse/mytracks.importorder @@ -0,0 +1,7 @@ +#Organize Import Order +#Fri May 28 03:20:44 BRT 2010 +4=org +3=javax +2=java +1=android +0=com