Move everything one directory upper

This commit is contained in:
Plonk42
2016-02-16 23:51:42 +01:00
parent aba8cb4090
commit a9b1581d1d
879 changed files with 16 additions and 5 deletions
+294
View File
@@ -0,0 +1,294 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:installLocation="auto"
android:versionCode="74"
android:versionName="2.0.6"
package="com.google.android.maps.mytracks" >
<!-- Permission to initialize services -->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<!-- Permissions to record locations -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<!-- Permission for activity recognition -->
<uses-permission android:name="com.google.android.gms.permission.ACTIVITY_RECOGNITION" />
<!-- Permissions to access sensors -->
<uses-permission android:name="com.dsi.ant.permission.ANT" />
<uses-permission android:name="com.dsi.ant.permission.ANT_ADMIN" />
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<!-- Permissions to upload tracks -->
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
<uses-permission android:name="android.permission.USE_CREDENTIALS" />
<uses-permission android:name="com.google.android.googleapps.permission.GOOGLE_AUTH" />
<uses-permission android:name="com.google.android.googleapps.permission.GOOGLE_AUTH.local" />
<!-- Permission for Android sync provider -->
<uses-permission android:name="android.permission.READ_SYNC_STATS" />
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
<!-- Permission to access Google services to communicate with the map servers. -->
<uses-permission android:name="com.google.android.providers.gsf.permission.READ_GSERVICES" />
<!-- Permission to read phone state so voice announcement will not interrupt ongoing calls -->
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<!-- Permission to read/write external storage -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<!-- Permission to read contacts -->
<uses-permission android:name="android.permission.READ_CONTACTS" />
<!-- Permissions to access My Tracks data -->
<uses-permission android:name="com.google.android.apps.mytracks.MYTRACKS_NOTIFICATIONS" />
<uses-permission android:name="com.google.android.apps.mytracks.READ_TRACK_DATA" />
<uses-permission android:name="com.google.android.apps.mytracks.WRITE_TRACK_DATA" />
<!-- Permission to receive My Tracks internal broadcasts -->
<permission
android:description="@string/permission_notification_label"
android:label="@string/permission_notification_label"
android:name="com.google.android.apps.mytracks.MYTRACKS_NOTIFICATIONS"
android:permissionGroup="android.permission-group.LOCATION"
android:protectionLevel="signature" />
<!-- Permission to read track data -->
<permission
android:description="@string/permission_read_description"
android:label="@string/permission_read_label"
android:name="com.google.android.apps.mytracks.READ_TRACK_DATA"
android:permissionGroup="android.permission-group.LOCATION"
android:protectionLevel="dangerous" />
<!-- Permission to receive track start/end broadcasts -->
<permission
android:description="@string/permission_notification_description"
android:label="@string/permission_notification_label"
android:name="com.google.android.apps.mytracks.TRACK_NOTIFICATIONS"
android:permissionGroup="android.permission-group.LOCATION"
android:protectionLevel="dangerous" />
<!-- Permission to write track data, either directly or through service calls -->
<permission
android:description="@string/permission_write_description"
android:label="@string/permission_write_label"
android:name="com.google.android.apps.mytracks.WRITE_TRACK_DATA"
android:permissionGroup="android.permission-group.LOCATION"
android:protectionLevel="dangerous" />
<!-- Permission for map view v2. Note the prefix must be the application package name -->
<permission
android:name="com.google.android.maps.mytracks.permission.MAPS_RECEIVE"
android:protectionLevel="signature" />
<uses-permission android:name="com.google.android.maps.mytracks.permission.MAPS_RECEIVE" />
<uses-sdk
android:minSdkVersion="8"
android:targetSdkVersion="19" />
<!-- Required features -->
<uses-feature
android:glEsVersion="0x00020000"
android:required="true" />
<uses-feature
android:name="android.hardware.location"
android:required="true" />
<uses-feature
android:name="android.hardware.location.gps"
android:required="true" />
<!-- Optional features -->
<uses-feature
android:name="android.hardware.bluetooth"
android:required="false" />
<uses-feature
android:name="android.hardware.camera"
android:required="false" />
<uses-feature
android:name="android.hardware.faketouch"
android:required="false" />
<uses-feature
android:name="android.hardware.location.network"
android:required="false" />
<uses-feature
android:name="android.hardware.sensor.compass"
android:required="false" />
<uses-feature
android:name="android.hardware.telephony"
android:required="false" />
<uses-feature
android:name="android.hardware.telephony.gsm"
android:required="false" />
<uses-feature
android:name="android.hardware.touchscreen"
android:required="false" />
<uses-feature
android:name="android.hardware.touchscreen.multitouch"
android:required="false" />
<supports-screens
android:anyDensity="true"
android:largeScreens="true"
android:normalScreens="true"
android:smallScreens="true"
android:xlargeScreens="true" />
<application
android:allowBackup="true"
android:backupAgent="com.google.android.apps.mytracks.io.backup.MyTracksBackupAgent"
android:hardwareAccelerated="true"
android:icon="@drawable/ic_launcher_my_tracks"
android:label="@string/my_tracks_app_name"
android:theme="@style/ThemeMyTracks"
tools:replace="android:label" >
<!-- For data backup -->
<meta-data
android:name="com.google.android.backup.api_key"
android:value="AEdPqrEAAAAIi-_QiwoRSc9_bAC9cmuNXTQyU8ajJmGtKdhskQ" />
<!-- For map view v2 -->
<meta-data
android:name="com.google.android.maps.v2.API_KEY"
android:value="AIzaSyCKg_WrcnfP0I105X1CGHoiJsxT7w45XEI" />
<!-- Activities -->
<activity
android:label="@string/menu_aggregated_statistics"
android:name="com.google.android.apps.mytracks.AggregatedStatsActivity" />
<activity android:name="com.google.android.apps.mytracks.DeleteActivity" />
<activity
android:label="@string/menu_help"
android:name="com.google.android.apps.mytracks.HelpActivity" />
<activity android:name="com.google.android.apps.mytracks.io.file.importer.ImportActivity" >
<intent-filter>
<action android:name="android.intent.action.ATTACH_DATA" />
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="application/gpx" />
<data android:mimeType="application/gpx+xml" />
<data android:mimeType="application/vnd.google-earth.gpx" />
<data android:mimeType="application/vnd.google-earth.gpx+xml" />
<data android:scheme="file" />
</intent-filter>
</activity>
<activity android:name="com.google.android.apps.mytracks.MarkerEditActivity" />
<activity android:name="com.google.android.apps.mytracks.MarkerDetailActivity" />
<activity
android:label="@string/menu_markers"
android:name="com.google.android.apps.mytracks.MarkerListActivity" >
<meta-data
android:name="android.app.default_searchable"
android:value="com.google.android.apps.mytracks.SearchListActivity" />
</activity>
<activity
android:launchMode="singleTop"
android:name="com.google.android.apps.mytracks.SearchListActivity" >
<intent-filter>
<action android:name="android.intent.action.SEARCH" />
</intent-filter>
<meta-data
android:name="android.app.searchable"
android:resource="@xml/search" />
</activity>
<activity
android:label="@string/menu_sensor_state"
android:name="com.google.android.apps.mytracks.SensorStateActivity" />
<activity
android:name="com.google.android.apps.mytracks.TrackDetailActivity"
android:parentActivityName="com.google.android.apps.mytracks.TrackListActivity" >
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="com.google.android.apps.mytracks.TrackListActivity" />
</activity>
<activity android:name="com.google.android.apps.mytracks.TrackEditActivity" />
<activity android:name="com.google.android.apps.mytracks.TrackListActivity" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data
android:name="android.app.default_searchable"
android:value="com.google.android.apps.mytracks.SearchListActivity" />
</activity>
<activity android:name="com.google.android.apps.mytracks.io.drive.SendDriveActivity" />
<activity android:name="com.google.android.apps.mytracks.io.file.exporter.SaveActivity" />
<activity android:name="com.google.android.apps.mytracks.io.fusiontables.SendFusionTablesActivity" />
<activity android:name="com.google.android.apps.mytracks.io.maps.SendMapsActivity" />
<activity android:name="com.google.android.apps.mytracks.io.sendtogoogle.UploadResultActivity" />
<activity android:name="com.google.android.apps.mytracks.io.spreadsheets.SendSpreadsheetsActivity" />
<activity android:name="com.google.android.apps.mytracks.settings.AdvancedSettingsActivity" />
<activity android:name="com.google.android.apps.mytracks.settings.ChartSettingsActivity" />
<activity android:name="com.google.android.apps.mytracks.settings.MapSettingsActivity" />
<activity android:name="com.google.android.apps.mytracks.settings.RecordingSettingsActivity" />
<activity android:name="com.google.android.apps.mytracks.settings.SensorSettingsActivity" />
<activity android:name="com.google.android.apps.mytracks.settings.SettingsActivity" />
<activity android:name="com.google.android.apps.mytracks.settings.StatsSettingsActivity" />
<activity
android:label="@string/track_widget_config_title"
android:name="com.google.android.apps.mytracks.widgets.TrackWidgetConfigActivity" >
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
</intent-filter>
</activity>
<!-- Services -->
<service
android:exported="true"
android:name="com.google.android.apps.mytracks.io.sync.SyncService" >
<intent-filter>
<action android:name="android.content.SyncAdapter" />
</intent-filter>
<meta-data
android:name="android.content.SyncAdapter"
android:resource="@xml/sync_adapter" />
</service>
<service
android:name="com.google.android.apps.mytracks.services.ActivityRecognitionIntentService" />
<service
android:exported="false"
android:name="com.google.android.apps.mytracks.services.ControlRecordingService" />
<service
android:enabled="true"
android:exported="true"
android:icon="@drawable/ic_arrow_320"
android:label="@string/my_tracks_recording_service"
android:name="com.google.android.apps.mytracks.services.TrackRecordingService"
android:permission="com.google.android.apps.mytracks.WRITE_TRACK_DATA" />
<!-- Receivers -->
<receiver android:name="com.google.android.apps.mytracks.BootReceiver" >
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
<receiver android:name="com.google.android.apps.mytracks.widgets.TrackWidgetProvider" >
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
<action android:name="com.google.android.apps.mytracks.TRACK_PAUSED" />
<action android:name="com.google.android.apps.mytracks.TRACK_RESUMED" />
<action android:name="com.google.android.apps.mytracks.TRACK_STARTED" />
<action android:name="com.google.android.apps.mytracks.TRACK_STOPPED" />
<action android:name="com.google.android.apps.mytracks.TRACK_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/track_widget_info" />
</receiver>
<!-- Providers -->
<!-- My Tracks data provider -->
<provider
android:authorities="com.google.android.maps.mytracks"
android:exported="true"
android:grantUriPermissions="true"
android:name="com.google.android.apps.mytracks.content.MyTracksProvider"
android:readPermission="com.google.android.apps.mytracks.READ_TRACK_DATA"
android:writePermission="com.google.android.apps.mytracks.WRITE_TRACK_DATA" />
<!-- Search suggestion provider -->
<provider
android:authorities="com.google.android.maps.mytracks.search"
android:exported="false"
android:name="com.google.android.apps.mytracks.content.SearchEngineProvider" />
</application>
</manifest>
@@ -0,0 +1,101 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks;
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import com.google.android.apps.mytracks.util.TrackIconUtils;
import android.os.Bundle;
import android.speech.tts.TextToSpeech;
import android.support.v4.app.FragmentActivity;
import android.view.Menu;
import android.view.MenuItem;
/**
* An abstract class for all My Tracks activities.
*
* @author Jimmy Shih
*/
public abstract class AbstractMyTracksActivity extends FragmentActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Set volume control stream for text to speech
setVolumeControlStream(TextToSpeech.Engine.DEFAULT_STREAM);
// Hide title must be before setContentView
if (hideTitle()) {
ApiAdapterFactory.getApiAdapter().hideTitle(this);
}
setContentView(getLayoutResId());
// Configure action bar must be after setContentView
if (configureActionBarHomeAsUp()) {
ApiAdapterFactory.getApiAdapter().configureActionBarHomeAsUp(this);
}
}
/**
* Returns true to hide the title. Be default, do not hide the title.
*/
protected boolean hideTitle() {
return false;
}
/**
* Gets the layout resource id.
*/
protected abstract int getLayoutResId();
/**
* Returns true to configure the action bar home button as the up button.
*/
protected boolean configureActionBarHomeAsUp() {
return true;
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Set menu icon color
TrackIconUtils.setMenuIconColor(menu);
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle home menu item, up navigation
if (item.getItemId() == android.R.id.home) {
onHomeSelected();
return true;
}
return super.onOptionsItemSelected(item);
}
/**
* Callback when the home menu item is selected. E.g., setup the back stack
* when home is selected.
*/
protected void onHomeSelected() {
finish();
}
}
@@ -0,0 +1,433 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks;
import com.google.android.apps.mytracks.fragments.ConfirmDeleteDialogFragment;
import com.google.android.apps.mytracks.fragments.ConfirmDeleteDialogFragment.ConfirmDeleteCaller;
import com.google.android.apps.mytracks.fragments.ExportDialogFragment.ExportType;
import com.google.android.apps.mytracks.fragments.InstallEarthDialogFragment;
import com.google.android.apps.mytracks.fragments.ShareTrackDialogFragment;
import com.google.android.apps.mytracks.fragments.ShareTrackDialogFragment.ShareTrackCaller;
import com.google.android.apps.mytracks.io.drive.SendDriveActivity;
import com.google.android.apps.mytracks.io.file.TrackFileFormat;
import com.google.android.apps.mytracks.io.file.exporter.SaveActivity;
import com.google.android.apps.mytracks.io.fusiontables.SendFusionTablesActivity;
import com.google.android.apps.mytracks.io.gdata.maps.MapsConstants;
import com.google.android.apps.mytracks.io.maps.SendMapsActivity;
import com.google.android.apps.mytracks.io.sendtogoogle.SendRequest;
import com.google.android.apps.mytracks.io.sendtogoogle.SendToGoogleUtils;
import com.google.android.apps.mytracks.io.sendtogoogle.UploadResultActivity;
import com.google.android.apps.mytracks.io.spreadsheets.SendSpreadsheetsActivity;
import com.google.android.apps.mytracks.io.sync.SyncUtils;
import com.google.android.apps.mytracks.services.TrackRecordingServiceConnection;
import com.google.android.apps.mytracks.services.tasks.CheckPermissionAsyncTask;
import com.google.android.apps.mytracks.services.tasks.CheckPermissionAsyncTask.CheckPermissionCaller;
import com.google.android.apps.mytracks.util.AnalyticsUtils;
import com.google.android.apps.mytracks.util.GoogleEarthUtils;
import com.google.android.apps.mytracks.util.IntentUtils;
import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.android.apps.mytracks.util.TrackRecordingServiceConnectionUtils;
import com.google.android.maps.mytracks.R;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.AccountManagerCallback;
import android.accounts.AccountManagerFuture;
import android.accounts.AuthenticatorException;
import android.accounts.OperationCanceledException;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.os.Parcelable;
import android.util.Log;
import android.widget.Toast;
import java.io.IOException;
/**
* An abstract class for the following common tasks across
* {@link TrackListActivity}, {@link TrackDetailActivity}, and
* {@link SearchListActivity}:
* <p>
* - share track <br>
* - export track to Google services<br>
* - enable sync to Google Drive <br>
* - delete tracks <br>
* - play tracks
*
* @author Jimmy Shih
*/
public abstract class AbstractSendToGoogleActivity extends AbstractMyTracksActivity
implements CheckPermissionCaller, ShareTrackCaller, ConfirmDeleteCaller {
private static final String TAG = AbstractMyTracksActivity.class.getSimpleName();
private static final String SEND_REQUEST_KEY = "send_request_key";
private static final int DRIVE_REQUEST_CODE = 0;
private static final int FUSION_TABLES_REQUEST_CODE = 1;
private static final int SPREADSHEETS_REQUEST_CODE = 2;
private static final int DELETE_REQUEST_CODE = 3;
protected static final int GOOGLE_PLAY_SERVICES_REQUEST_CODE = 4;
protected static final int CAMERA_REQUEST_CODE = 5;
private SendRequest sendRequest;
private CheckPermissionAsyncTask asyncTask;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState != null) {
sendRequest = savedInstanceState.getParcelable(SEND_REQUEST_KEY);
}
Object retained = getLastCustomNonConfigurationInstance();
if (retained instanceof CheckPermissionAsyncTask) {
asyncTask = (CheckPermissionAsyncTask) retained;
asyncTask.setActivity(this);
}
}
@Override
public Object onRetainCustomNonConfigurationInstance() {
if (asyncTask != null) {
asyncTask.setActivity(null);
}
return asyncTask;
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putParcelable(SEND_REQUEST_KEY, sendRequest);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode) {
case DRIVE_REQUEST_CODE:
SendToGoogleUtils.cancelNotification(this, SendToGoogleUtils.DRIVE_NOTIFICATION_ID);
if (resultCode == Activity.RESULT_OK) {
onDrivePermissionSuccess();
} else {
onPermissionFailure();
}
break;
case FUSION_TABLES_REQUEST_CODE:
SendToGoogleUtils.cancelNotification(this, SendToGoogleUtils.FUSION_TABLES_NOTIFICATION_ID);
if (resultCode == Activity.RESULT_OK) {
onFusionTablesSuccess();
} else {
onPermissionFailure();
}
break;
case SPREADSHEETS_REQUEST_CODE:
SendToGoogleUtils.cancelNotification(this, SendToGoogleUtils.SPREADSHEETS_NOTIFICATION_ID);
if (resultCode == Activity.RESULT_OK) {
onSpreadsheetsPermissionSuccess();
} else {
onPermissionFailure();
}
break;
case DELETE_REQUEST_CODE:
onDeleted();
break;
default:
super.onActivityResult(requestCode, resultCode, data);
}
}
/**
* Shares a track.
*
* @param trackId the track id
*/
protected void shareTrack(long trackId) {
AnalyticsUtils.sendPageViews(this, AnalyticsUtils.ACTION_SHARE_DRIVE);
ShareTrackDialogFragment.newInstance(trackId)
.show(getSupportFragmentManager(), ShareTrackDialogFragment.SHARE_TRACK_DIALOG_TAG);
}
@Override
public void onShareTrackDone(long trackId, boolean makePublic, String emails, Account account) {
sendRequest = new SendRequest(trackId);
sendRequest.setSendDrive(true);
sendRequest.setDriveSharePublic(makePublic);
sendRequest.setDriveShareEmails(emails);
sendRequest.setAccount(account);
checkPermissions();
}
protected void exportTrackToGoogle(long trackId, ExportType exportType, Account account) {
sendRequest = new SendRequest(trackId);
String pageView;
switch (exportType) {
case GOOGLE_DRIVE:
pageView = AnalyticsUtils.ACTION_EXPORT_DRIVE;
sendRequest.setSendDrive(true);
break;
case GOOGLE_MAPS:
pageView = AnalyticsUtils.ACTION_EXPORT_MAPS;
sendRequest.setSendMaps(true);
break;
case GOOGLE_FUSION_TABLES:
pageView = AnalyticsUtils.ACTION_EXPORT_FUSION_TABLES;
sendRequest.setSendFusionTables(true);
break;
default:
pageView = AnalyticsUtils.ACTION_EXPORT_SPREADSHEETS;
sendRequest.setSendSpreadsheets(true);
}
AnalyticsUtils.sendPageViews(this, pageView);
sendRequest.setAccount(account);
checkPermissions();
}
/**
* Enables Google Drive sync.
*/
protected void enableSync(Account account) {
sendRequest = new SendRequest(-1L);
sendRequest.setSendDrive(true);
sendRequest.setDriveSync(true);
sendRequest.setAccount(account);
checkPermissions();
}
/**
* Checks permissions to needed Google services.
*/
private void checkPermissions() {
// Check Drive permission
boolean needDrivePermission = sendRequest.isSendDrive();
if (!needDrivePermission && sendRequest.isSendFusionTables()) {
needDrivePermission = PreferencesUtils.getBoolean(this,
R.string.export_google_fusion_tables_public_key,
PreferencesUtils.EXPORT_GOOGLE_FUSION_TABLES_PUBLIC_DEFAULT);
}
if (!needDrivePermission) {
needDrivePermission = sendRequest.isSendSpreadsheets();
}
if (needDrivePermission) {
startCheckPermission(SendToGoogleUtils.DRIVE_SCOPE);
} else {
onDrivePermissionSuccess();
}
}
/**
* Starts checking permission for a Google service.
*
* @param scope the service scope
*/
private void startCheckPermission(String scope) {
asyncTask = new CheckPermissionAsyncTask(this, sendRequest.getAccount().name, scope);
asyncTask.execute();
}
@Override
public void onCheckPermissionDone(String scope, boolean success, Intent userRecoverableIntent) {
asyncTask = null;
if (success) {
if (scope.equals(SendToGoogleUtils.DRIVE_SCOPE)) {
onDrivePermissionSuccess();
} else if (scope.equals(SendToGoogleUtils.FUSION_TABLES_SCOPE)) {
onFusionTablesSuccess();
} else {
onSpreadsheetsPermissionSuccess();
}
} else {
if (userRecoverableIntent != null) {
int requestCode;
if (scope.equals(SendToGoogleUtils.DRIVE_SCOPE)) {
requestCode = DRIVE_REQUEST_CODE;
} else if (scope.equals(SendToGoogleUtils.FUSION_TABLES_SCOPE)) {
requestCode = FUSION_TABLES_REQUEST_CODE;
} else {
requestCode = SPREADSHEETS_REQUEST_CODE;
}
startActivityForResult(userRecoverableIntent, requestCode);
} else {
onPermissionFailure();
}
}
}
private void onDrivePermissionSuccess() {
// Check Maps permission
if (sendRequest.isSendMaps()) {
AccountManager.get(this).getAuthToken(
sendRequest.getAccount(), MapsConstants.SERVICE_NAME, null, this,
new AccountManagerCallback<Bundle>() {
@Override
public void run(AccountManagerFuture<Bundle> future) {
try {
if (future.getResult().getString(AccountManager.KEY_AUTHTOKEN) != null) {
runOnUiThread(new Runnable() {
@Override
public void run() {
onMapsPermissionSuccess();
}
});
return;
} else {
Log.d(TAG, "auth token is null");
}
} catch (OperationCanceledException e) {
Log.d(TAG, "Unable to get auth token", e);
} catch (AuthenticatorException e) {
Log.d(TAG, "Unable to get auth token", e);
} catch (IOException e) {
Log.d(TAG, "Unable to get auth token", e);
}
runOnUiThread(new Runnable() {
@Override
public void run() {
onPermissionFailure();
}
});
}
}, null);
} else {
onMapsPermissionSuccess();
}
}
private void onMapsPermissionSuccess() {
// Check Fusion Tables permission
if (sendRequest.isSendFusionTables()) {
startCheckPermission(SendToGoogleUtils.FUSION_TABLES_SCOPE);
} else {
onFusionTablesSuccess();
}
}
private void onFusionTablesSuccess() {
// Check Spreadsheets permission
if (sendRequest.isSendSpreadsheets()) {
startCheckPermission(SendToGoogleUtils.SPREADSHEETS_SCOPE);
} else {
onSpreadsheetsPermissionSuccess();
}
}
/**
* On spreadsheets permission success. If
* <p>
* isSendDrive and isDriveEnableSync -> enable sync
* <p>
* isSendDrive -> start {@link SendDriveActivity}
* <p>
* isSendMaps -> start {@link SendMapsActivity}
* <p>
* isSendFusionTables -> start {@link SendFusionTablesActivity}
* <p>
* isSendSpreadsheets -> start {@link SendSpreadsheetsActivity}
* <p>
* else -> start {@link UploadResultActivity}
*/
private void onSpreadsheetsPermissionSuccess() {
Class<?> next;
if (sendRequest.isSendDrive()) {
if (sendRequest.isDriveSync()) {
SyncUtils.enableSync(this);
return;
} else {
next = SendDriveActivity.class;
}
} else if (sendRequest.isSendMaps()) {
next = SendMapsActivity.class;
} else if (sendRequest.isSendFusionTables()) {
next = SendFusionTablesActivity.class;
} else if (sendRequest.isSendSpreadsheets()) {
next = SendSpreadsheetsActivity.class;
} else {
next = UploadResultActivity.class;
}
Intent intent = IntentUtils.newIntent(this, next)
.putExtra(SendRequest.SEND_REQUEST_KEY, sendRequest);
startActivity(intent);
}
/**
* Call when not able to get permission for a google service.
*/
private void onPermissionFailure() {
Toast.makeText(this, R.string.send_google_no_account_permission, Toast.LENGTH_LONG).show();
}
/**
* Delete tracks.
*
* @param trackIds the track ids
*/
protected void deleteTracks(long[] trackIds) {
ConfirmDeleteDialogFragment.newInstance(trackIds)
.show(getSupportFragmentManager(), ConfirmDeleteDialogFragment.CONFIRM_DELETE_DIALOG_TAG);
}
@Override
public void onConfirmDeleteDone(long[] trackIds) {
boolean stopRecording = false;
if (trackIds.length == 1 && trackIds[0] == -1L) {
stopRecording = true;
} else {
long recordingTrackId = PreferencesUtils.getLong(this, R.string.recording_track_id_key);
for (long trackId : trackIds) {
if (trackId == recordingTrackId) {
stopRecording = true;
break;
}
}
}
if (stopRecording) {
TrackRecordingServiceConnectionUtils.stopRecording(
this, getTrackRecordingServiceConnection(), false);
}
Intent intent = IntentUtils.newIntent(this, DeleteActivity.class);
intent.putExtra(DeleteActivity.EXTRA_TRACK_IDS, trackIds);
startActivityForResult(intent, DELETE_REQUEST_CODE);
}
/**
* Gets the track recording service connection. For stopping the current
* recording if need to delete the current recording track.
*/
abstract protected TrackRecordingServiceConnection getTrackRecordingServiceConnection();
/**
* Called after {@link DeleteActivity} returns its result.
*/
abstract protected void onDeleted();
/**
* Play tracks in Google Earth.
*
* @param trackIds the track ids
*/
protected void playTracks(long[] trackIds) {
AnalyticsUtils.sendPageViews(this, AnalyticsUtils.ACTION_PLAY);
if (GoogleEarthUtils.isEarthInstalled(this)) {
Intent intent = IntentUtils.newIntent(this, SaveActivity.class)
.putExtra(SaveActivity.EXTRA_TRACK_IDS, trackIds)
.putExtra(SaveActivity.EXTRA_TRACK_FILE_FORMAT, (Parcelable) TrackFileFormat.KML)
.putExtra(SaveActivity.EXTRA_PLAY_TRACK, true);
startActivity(intent);
} else {
new InstallEarthDialogFragment().show(
getSupportFragmentManager(), InstallEarthDialogFragment.INSTALL_EARTH_DIALOG_TAG);
}
}
}
@@ -0,0 +1,65 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.android.apps.mytracks.util.CalorieUtils.ActivityType;
import com.google.android.apps.mytracks.util.StatsUtils;
import com.google.android.maps.mytracks.R;
import android.os.Bundle;
import java.util.List;
/**
* An activity to view aggregated stats from all recorded tracks.
*
* @author Fergus Nelson
*/
public class AggregatedStatsActivity extends AbstractMyTracksActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
StatsUtils.setTripStatisticsValues(
this, this, null, getTripStatistics(), ActivityType.WALKING, null);
StatsUtils.setLocationValues(this, this, null, null, false);
}
@Override
protected int getLayoutResId() {
return R.layout.stats;
}
/**
* Gets the aggregated trip statistics for all the recorded tracks or null if
* there is no track.
*/
private TripStatistics getTripStatistics() {
List<Track> tracks = MyTracksProviderUtils.Factory.get(this).getAllTracks();
TripStatistics tripStatistics = null;
if (!tracks.isEmpty()) {
tripStatistics = new TripStatistics(tracks.iterator().next().getTripStatistics());
for (int i = 1; i < tracks.size(); i++) {
tripStatistics.merge(tracks.get(i).getTripStatistics());
}
}
return tripStatistics;
}
}
@@ -0,0 +1,57 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks;
import static android.content.Intent.ACTION_BOOT_COMPLETED;
import com.google.android.apps.mytracks.services.TrackRecordingService;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
/**
* This class handles MyTracks related broadcast messages.
*
* One example of a broadcast message that this class is interested in,
* is notification about the phone boot. We may want to resume a previously
* started tracking session if the phone crashed (hopefully not), or the user
* decided to swap the battery or some external event occurred which forced
* a phone reboot.
*
* This class simply delegates to {@link TrackRecordingService} to make a
* decision whether to continue with the previous track (if any), or just
* abandon it.
*
* @author Bartlomiej Niechwiej
*/
public class BootReceiver extends BroadcastReceiver {
private static final String TAG = BootReceiver.class.getSimpleName();
@Override
public void onReceive(Context context, Intent intent) {
Log.d(TAG, "BootReceiver.onReceive: " + intent.getAction());
if (ACTION_BOOT_COMPLETED.equals(intent.getAction())) {
Intent startIntent = new Intent(context, TrackRecordingService.class)
.putExtra(TrackRecordingService.RESUME_TRACK_EXTRA_NAME, true);
context.startService(startIntent);
} else {
Log.w(TAG, "BootReceiver: unsupported action");
}
}
}
@@ -0,0 +1,264 @@
/*
* Copyright 2009 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks;
import com.google.android.apps.mytracks.stats.ExtremityMonitor;
import com.google.common.annotations.VisibleForTesting;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Paint.Align;
import android.graphics.Paint.Style;
import android.graphics.Path;
import java.text.NumberFormat;
/**
* This class encapsulates the meta data for one series of the chart values.
*
* @author Sandor Dornbush
*/
public class ChartValueSeries {
private static final float STROKE_WIDTH = 2f;
private final int absoluteMin;
private final int absoluteMax;
private final int[] intervalValues;
private final int metricTitleId;
private final int imperialTitleId;
private final Paint fillPaint;
private final Paint strokePaint;
private final Paint titlePaint;
private final Paint markerPaint;
private final ExtremityMonitor extremityMonitor;
private final NumberFormat numberFormat;
private final Path path;
private int interval = 1;
private int minMarkerValue = 0;
private int maxMarkerValue = interval * ChartView.Y_AXIS_INTERVALS;
private boolean enabled = true;
/**
* Constructor.
*
* @param context the context
* @param absoluteMin the absolute min value
* @param absoluteMax the absolute max value
* @param intervalValues the list of interval values
* @param metricTitleId the metric title id
* @param imperialTitleId the imperial title id
* @param fillColor the fill color
* @param strokeColor the stroke color
*/
public ChartValueSeries(Context context, int absoluteMin, int absoluteMax, int[] intervalValues,
int metricTitleId, int imperialTitleId, int fillColor, int strokeColor) {
this.absoluteMin = absoluteMin;
this.absoluteMax = absoluteMax;
this.intervalValues = intervalValues;
this.metricTitleId = metricTitleId;
this.imperialTitleId = imperialTitleId;
fillPaint = new Paint();
fillPaint.setStyle(Style.FILL);
fillPaint.setColor(context.getResources().getColor(fillColor));
fillPaint.setAntiAlias(true);
strokePaint = new Paint();
strokePaint.setStyle(Style.STROKE);
strokePaint.setColor(context.getResources().getColor(strokeColor));
strokePaint.setAntiAlias(true);
float scale = context.getResources().getDisplayMetrics().density;
// Make copies of the stroke paint with the default thickness
titlePaint = new Paint(strokePaint);
titlePaint.setTextSize(ChartView.MEDIUM_TEXT_SIZE * scale);
titlePaint.setTextAlign(Align.CENTER);
markerPaint = new Paint(strokePaint);
markerPaint.setTextSize(ChartView.SMALL_TEXT_SIZE * scale);
markerPaint.setTextAlign(Align.RIGHT);
// Set stroke paint thickness
strokePaint.setStrokeWidth(STROKE_WIDTH);
extremityMonitor = new ExtremityMonitor();
numberFormat = NumberFormat.getIntegerInstance();
path = new Path();
}
/**
* Sets the series enabled value.
*
* @param enabled true to enable
*/
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
/**
* Returns true if the series is enabled.
*/
public boolean isEnabled() {
return enabled;
}
/**
* Returns true if the series has data.
*/
public boolean hasData() {
return extremityMonitor.hasData();
}
/**
* Updates the series with a new value.
*
* @param value the new value
*/
public void update(double value) {
extremityMonitor.update(value);
}
/**
* Gets the path.
*/
public Path getPath() {
return path;
}
/**
* Draws the path on canvas.
*
* @param canvas the canvas
*/
public void drawPath(Canvas canvas) {
canvas.drawPath(path, fillPaint);
canvas.drawPath(path, strokePaint);
}
/**
* Updates the y axis dimension.
*/
public void updateDimension() {
double min = hasData() ? extremityMonitor.getMin() : 0.0;
double max = hasData() ? extremityMonitor.getMax() : 1.0;
min = Math.max(min, absoluteMin);
max = Math.min(max, absoluteMax);
interval = getInterval(min, max);
minMarkerValue = getMinMarkerValue(min, interval);
maxMarkerValue = minMarkerValue + interval * ChartView.Y_AXIS_INTERVALS;
}
/**
* Gets the interval value.
*
* @param min the min value
* @param max the max value
*/
private int getInterval(double min, double max) {
for (int i = 0; i < intervalValues.length; i++) {
int intervalValue = intervalValues[i];
int minValue = getMinMarkerValue(min, intervalValue);
double targetInterval = (max - minValue) / ChartView.Y_AXIS_INTERVALS;
if (intervalValue >= targetInterval) {
return intervalValue;
}
}
// Return the largest interval
return intervalValues[intervalValues.length - 1];
}
/**
* Gets the min marker value.
*
* @param min the min series value
* @param intervalValue the interval value
*/
private int getMinMarkerValue(double min, int intervalValue) {
// Round down to the nearest intervalValue
int value = ((int) (min / intervalValue)) * intervalValue;
// value > min if min is negative
if (value > min) {
return value - intervalValue;
}
return value;
}
/**
* Gets the interval value.
*/
public int getInterval() {
return interval;
}
/**
* Gets the min marker value.
*/
public int getMinMarkerValue() {
return minMarkerValue;
}
/**
* Gets the max marker value.
*/
@VisibleForTesting
int getMaxMarkerValue() {
return maxMarkerValue;
}
/**
* Gets the title id.
*/
public int getTitleId(boolean metricUnits) {
return metricUnits ? metricTitleId : imperialTitleId;
}
/**
* Gets the title paint.
*/
public Paint getTitlePaint() {
return titlePaint;
}
/**
* Gets the marker paint.
*/
public Paint getMarkerPaint() {
return markerPaint;
}
/**
* Gets the largest marker.
*/
public String getLargestMarker() {
String minMarker = numberFormat.format(getMinMarkerValue());
String maxMarker = numberFormat.format(getMaxMarkerValue());
return minMarker.length() >= maxMarker.length() ? minMarker : maxMarker;
}
/**
* Formats a marker value.
*
* @param value the value
*/
public String formatMarker(int value) {
return numberFormat.format(value);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,62 @@
/*
* Copyright 2011 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks;
import android.graphics.Paint;
import android.graphics.Path;
/**
* Represents a colored {@code Path} to save its relative color for drawing.
* @author Vangelis S.
*/
public class ColoredPath {
private final Path path;
private final Paint pathPaint;
/**
* Constructor for a ColoredPath by color.
*/
public ColoredPath(int color) {
path = new Path();
pathPaint = new Paint();
pathPaint.setColor(color);
pathPaint.setStrokeWidth(3);
pathPaint.setStyle(Paint.Style.STROKE);
pathPaint.setAntiAlias(true);
}
/**
* Constructor for a ColoredPath by Paint.
*/
public ColoredPath(Paint paint) {
path = new Path();
pathPaint = paint;
}
/**
* @return the path
*/
public Path getPath() {
return path;
}
/**
* @return the pathPaint
*/
public Paint getPathPaint() {
return pathPaint;
}
}
@@ -0,0 +1,42 @@
/*
* 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;
/**
* Common constants.
*
* @author Leif Hendrik Wilden
*/
public class Constants {
private Constants() {}
/**
* The google account type.
*/
public static final String ACCOUNT_TYPE = "com.google";
/**
* Maximum number of waypoints that will be loaded at one time.
*/
public static final int MAX_LOADED_WAYPOINTS_POINTS = 10000;
/**
* The settings file name.
*/
public static final String SETTINGS_NAME = "SettingsActivity";
}
@@ -0,0 +1,46 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks;
import android.view.Menu;
/**
* Callback when items in the contextual action mode are selected.
*
* @author Jimmy Shih
*/
public interface ContextualActionModeCallback {
/**
* Invoked to prepare the menu for the selected items.
*
* @param menu the menu
* @param positions the selected items' positions
* @param ids the selected items' ids, if available
* @param showSelectAll true to show select all
*/
public void onPrepare(Menu menu, int[] positions, long[] ids, boolean showSelectAll);
/**
* Invoked when items are selected.
*
* @param itemId the context menu item id
* @param positions the selected items' positions
* @param ids the selected items' ids, if available
*/
public boolean onClick(int itemId, int[] positions, long[] ids);
}
@@ -0,0 +1,97 @@
/*
* Copyright 2013 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.apps.mytracks.util.DialogUtils;
import com.google.android.maps.mytracks.R;
import android.app.Activity;
import android.app.Dialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
/**
* An activity for delete tracks.
*
* @author Jimmy Shih
*/
public class DeleteActivity extends Activity {
public static final String EXTRA_TRACK_IDS = "track_ids";
private static final int DIALOG_PROGRESS_ID = 0;
private DeleteAsyncTask deleteAsyncTask;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setResult(RESULT_CANCELED);
Intent intent = getIntent();
long[] trackIds = intent.getLongArrayExtra(EXTRA_TRACK_IDS);
Object retained = getLastNonConfigurationInstance();
if (retained instanceof DeleteAsyncTask) {
deleteAsyncTask = (DeleteAsyncTask) retained;
deleteAsyncTask.setActivity(this);
} else {
deleteAsyncTask = new DeleteAsyncTask(this, trackIds);
deleteAsyncTask.execute();
}
}
@Override
public Object onRetainNonConfigurationInstance() {
deleteAsyncTask.setActivity(null);
return deleteAsyncTask;
}
@Override
protected Dialog onCreateDialog(int id) {
if (id != DIALOG_PROGRESS_ID) {
return null;
}
return DialogUtils.createSpinnerProgressDialog(
this, R.string.track_delete_progress_message, new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
deleteAsyncTask.cancel(true);
dialog.dismiss();
finish();
}
});
}
/**
* Invokes when the associated AsyncTask completes.
*/
public void onAsyncTaskCompleted() {
removeDialog(DIALOG_PROGRESS_ID);
setResult(RESULT_OK);
finish();
}
/**
* Shows the progress dialog.
*/
public void showProgressDialog() {
showDialog(DIALOG_PROGRESS_ID);
}
}
@@ -0,0 +1,127 @@
package com.google.android.apps.mytracks;
/*
* Copyright 2013 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.
*/
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.io.sync.SyncUtils;
import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.android.apps.mytracks.util.UnitConversions;
import com.google.android.maps.mytracks.R;
import android.content.Context;
import android.os.AsyncTask;
/**
* Async Task to delete tracks.
*
* @author Jimmy Shih
*/
public class DeleteAsyncTask extends AsyncTask<Void, Integer, Boolean> {
// 1 second in milliseconds
private static final long ONE_SECOND = (long) UnitConversions.S_TO_MS;
private DeleteActivity deleteActivity;
private final long[] trackIds;
private final Context context;
// true if the AsyncTask has completed
private boolean completed;
/**
* Creates an AsyncTask.
*
* @param deleteActivity the activity currently associated with this task
* @param trackIds the track ids to delete. To delete all, set to size 1 with
* trackIds[0] == -1L
*/
public DeleteAsyncTask(DeleteActivity deleteActivity, long[] trackIds) {
this.deleteActivity = deleteActivity;
this.trackIds = trackIds;
context = deleteActivity.getApplicationContext();
completed = false;
}
/**
* Sets the current activity associated with this AyncTask.
*
* @param deleteActivity the current activity, can be null
*/
public void setActivity(DeleteActivity deleteActivity) {
this.deleteActivity = deleteActivity;
if (completed && deleteActivity != null) {
deleteActivity.onAsyncTaskCompleted();
}
}
@Override
protected void onPreExecute() {
if (deleteActivity != null) {
deleteActivity.showProgressDialog();
}
}
@Override
protected Boolean doInBackground(Void... params) {
MyTracksProviderUtils myTracksProviderUtils = MyTracksProviderUtils.Factory.get(context);
if (trackIds.length == 1 && trackIds[0] == -1L) {
try {
while (SyncUtils.isSyncActive(context)) {
if (isCancelled()) {
return false;
}
Thread.sleep(ONE_SECOND);
}
} catch (InterruptedException e) {
return false;
}
SyncUtils.disableSync(context);
myTracksProviderUtils.deleteAllTracks(context);
return true;
} else {
for (long id : trackIds) {
if (isCancelled()) {
return false;
}
Track track = myTracksProviderUtils.getTrack(id);
myTracksProviderUtils.deleteTrack(context, id);
if (track != null) {
String driveId = track.getDriveId();
if (driveId != null && !driveId.equals("")) {
boolean driveSync = PreferencesUtils.getBoolean(
context, R.string.drive_sync_key, PreferencesUtils.DRIVE_SYNC_DEFAULT);
if (driveSync) {
PreferencesUtils.addToList(context, R.string.drive_deleted_list_key,
PreferencesUtils.DRIVE_DELETED_LIST_DEFAULT, driveId);
}
}
}
}
return true;
}
}
@Override
protected void onPostExecute(Boolean result) {
completed = true;
if (deleteActivity != null) {
deleteActivity.onAsyncTaskCompleted();
}
}
}
@@ -0,0 +1,132 @@
/*
* 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;
import com.google.android.apps.mytracks.fragments.EulaDialogFragment;
import com.google.android.apps.mytracks.fragments.EulaDialogFragment.EulaCaller;
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import com.google.android.apps.mytracks.util.GoogleFeedbackUtils;
import com.google.android.apps.mytracks.util.SystemUtils;
import com.google.android.maps.mytracks.R;
import android.annotation.SuppressLint;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.Button;
import java.util.Locale;
/**
* An activity that displays the help page.
*
* @author Sandor Dornbush
*/
public class HelpActivity extends AbstractMyTracksActivity implements EulaCaller {
WebView webView;
@SuppressLint("SetJavaScriptEnabled")
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
String subtitle = getString(R.string.my_tracks_app_name) + " v"
+ SystemUtils.getMyTracksVersion(this);
ApiAdapterFactory.getApiAdapter()
.setTitleAndSubtitle(this, getString(R.string.menu_help), subtitle);
webView = (WebView) findViewById(R.id.help_webview);
webView.getSettings().setJavaScriptEnabled(true);
webView.setWebViewClient(new WebViewClient());
if (savedInstanceState == null) {
String language = Locale.getDefault().getLanguage();
if (language == null || language.equals("")) {
language = "en";
}
webView.loadUrl(getString(R.string.my_tracks_help_url, language));
} else {
webView.restoreState(savedInstanceState);
}
Button feedback = (Button) findViewById(R.id.help_feedback);
boolean showFeedback = ApiAdapterFactory.getApiAdapter().isGoogleFeedbackAvailable();
feedback.setVisibility(showFeedback ? View.VISIBLE : View.GONE);
if (showFeedback) {
feedback.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
GoogleFeedbackUtils.bindFeedback(HelpActivity.this);
}
});
}
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
if (webView != null) {
webView.saveState(outState);
}
}
@Override
protected int getLayoutResId() {
return R.layout.help;
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.help, menu);
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
Intent intent;
switch (item.getItemId()) {
case R.id.help_play_store:
intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse("market://details?id=com.google.android.maps.mytracks"));
startActivity(intent);
return true;
case R.id.help_eula:
EulaDialogFragment.newInstance(true)
.show(getSupportFragmentManager(), EulaDialogFragment.EULA_DIALOG_TAG);
return true;
case R.id.help_forum:
intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse(
"http://groups.google.com/a/googleproductforums.com/forum/#!categories/maps/mytracks/"));
startActivity(intent);
return true;
default:
return super.onOptionsItemSelected(item);
}
}
@Override
public void onEulaDone() {
// Do nothing
}
}
@@ -0,0 +1,302 @@
/*
* 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;
import static com.google.android.apps.mytracks.content.TrackDataHub.TARGET_DISPLAYED_TRACK_POINTS;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.content.Waypoint.WaypointType;
import com.google.android.apps.mytracks.maps.TrackPath;
import com.google.android.apps.mytracks.maps.TrackPathFactory;
import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.android.apps.mytracks.util.LocationUtils;
import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.android.apps.mytracks.util.UnitConversions;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.model.BitmapDescriptorFactory;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.MarkerOptions;
import com.google.android.gms.maps.model.Polyline;
import com.google.android.maps.mytracks.R;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.location.Location;
import android.util.Log;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
/**
* A map overlay that displays my location arrow, error circle, and track info.
*
* @author Leif Hendrik Wilden
*/
public class MapOverlay {
public static final float WAYPOINT_X_ANCHOR = 13f / 48f;
private static final String TAG = MapOverlay.class.getSimpleName();
private static final float WAYPOINT_Y_ANCHOR = 43f / 48f;
private static final float MARKER_X_ANCHOR = 50f / 96f;
private static final float MARKER_Y_ANCHOR = 90f / 96f;
private static final int INITIAL_LOCATIONS_SIZE = 1024;
private final OnSharedPreferenceChangeListener
sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if (key == null
|| key.equals(PreferencesUtils.getKey(context, R.string.track_color_mode_key))) {
trackColorMode = PreferencesUtils.getString(
context, R.string.track_color_mode_key, PreferencesUtils.TRACK_COLOR_MODE_DEFAULT);
trackPath = TrackPathFactory.getTrackPath(context, trackColorMode);
}
}
};
private final Context context;
private final List<CachedLocation> locations;
private final BlockingQueue<CachedLocation> pendingLocations;
private final List<Waypoint> waypoints;
private String trackColorMode = PreferencesUtils.TRACK_COLOR_MODE_DEFAULT;
private boolean showEndMarker = true;
private TrackPath trackPath;
/**
* A pre-processed {@link Location} to speed up drawing.
*
* @author Jimmy Shih
*/
public static class CachedLocation {
private final boolean valid;
private final LatLng latLng;
private final double speed;
/**
* Constructor for an invalid cached location.
*/
public CachedLocation() {
this.valid = false;
this.latLng = null;
this.speed = -1.0;
}
/**
* Constructor for a potentially valid cached location.
*/
public CachedLocation(Location location) {
this.valid = LocationUtils.isValidLocation(location);
this.latLng = valid ? new LatLng(location.getLatitude(), location.getLongitude()) : null;
this.speed = location.hasSpeed() ? location.getSpeed() * UnitConversions.MS_TO_KMH : -1.0;
}
/**
* Returns true if the location is valid.
*/
public boolean isValid() {
return valid;
}
/**
* Gets the speed in kilometers per hour.
*/
public double getSpeed() {
return speed;
}
/**
* Gets the LatLng.
*/
public LatLng getLatLng() {
return latLng;
}
};
public MapOverlay(Context context) {
this.context = context;
this.waypoints = new ArrayList<Waypoint>();
this.locations = new ArrayList<CachedLocation>(INITIAL_LOCATIONS_SIZE);
// Set the number of points to be 2x the TARGET_DISPLAYED_TRACK_POINTS
this.pendingLocations = new ArrayBlockingQueue<CachedLocation>(
2 * TARGET_DISPLAYED_TRACK_POINTS, true);
context.getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE)
.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
sharedPreferenceChangeListener.onSharedPreferenceChanged(null, null);
}
/**
* Add a location.
*
* @param location the location
*/
public void addLocation(Location location) {
// Queue up in the pendingLocations until it's merged with locations
if (!pendingLocations.offer(new CachedLocation(location))) {
Log.e(TAG, "Unable to add to pendingLocations.");
}
}
/**
* Adds a segment split.
*/
public void addSegmentSplit() {
// Queue up in the pendingLocations until it's merged with locations
if (!pendingLocations.offer(new CachedLocation())) {
Log.e(TAG, "Unable to add to pendingLocations.");
}
}
/**
* Clears the locations.
*/
public void clearPoints() {
synchronized (locations) {
locations.clear();
pendingLocations.clear();
}
}
/**
* Adds a waypoint.
*
* @param waypoint the waypoint
*/
public void addWaypoint(Waypoint waypoint) {
synchronized (waypoints) {
waypoints.add(waypoint);
}
}
/**
* Clears the waypoints.
*/
public void clearWaypoints() {
synchronized (waypoints) {
waypoints.clear();
}
}
/**
* Sets whether to show the end marker.
*
* @param show true to show the end marker
*/
public void setShowEndMarker(boolean show) {
showEndMarker = show;
}
/**
* Updates the track, start and end markers, and waypoints.
*
* @param googleMap the google map
* @param paths the paths
* @param tripStatistics the trip statistics
* @param reload true to reload all points
* @return true if has the start marker
*/
public boolean update(GoogleMap googleMap, ArrayList<Polyline> paths,
TripStatistics tripStatistics, boolean reload) {
synchronized (locations) {
boolean hasStartMarker = false;
// Merge pendingLocations with locations
int newLocations = pendingLocations.drainTo(locations);
// Call updateState first because we want to update its state each time
// (for dynamic coloring)
if (trackPath.updateState(tripStatistics) || reload) {
googleMap.clear();
paths.clear();
trackPath.updatePath(googleMap, paths, 0, locations);
hasStartMarker = updateStartAndEndMarkers(googleMap);
updateWaypoints(googleMap);
} else {
if (newLocations != 0) {
int numLocations = locations.size();
trackPath.updatePath(googleMap, paths, numLocations - newLocations, locations);
}
}
return hasStartMarker;
}
}
/**
* Updates the start and end markers.
*
* @param googleMap the google map
* @return true if has the start marker
*/
private boolean updateStartAndEndMarkers(GoogleMap googleMap) {
// Add the end marker
if (showEndMarker) {
for (int i = locations.size() - 1; i >= 0; i--) {
CachedLocation cachedLocation = locations.get(i);
if (cachedLocation.valid) {
MarkerOptions markerOptions = new MarkerOptions().position(cachedLocation.getLatLng())
.anchor(MARKER_X_ANCHOR, MARKER_Y_ANCHOR).draggable(false).visible(true)
.icon(BitmapDescriptorFactory.fromResource(R.drawable.ic_marker_red_paddle));
googleMap.addMarker(markerOptions);
break;
}
}
}
// Add the start marker
boolean hasStartMarker = false;
for (int i = 0; i < locations.size(); i++) {
CachedLocation cachedLocation = locations.get(i);
if (cachedLocation.valid) {
MarkerOptions markerOptions = new MarkerOptions().position(cachedLocation.getLatLng())
.anchor(MARKER_X_ANCHOR, MARKER_Y_ANCHOR).draggable(false).visible(true)
.icon(BitmapDescriptorFactory.fromResource(R.drawable.ic_marker_green_paddle));
googleMap.addMarker(markerOptions);
hasStartMarker = true;
break;
}
}
return hasStartMarker;
}
/**
* Updates the waypoints.
*
* @param googleMap the google map.
*/
private void updateWaypoints(GoogleMap googleMap) {
synchronized (waypoints) {
for (Waypoint waypoint : waypoints) {
Location location = waypoint.getLocation();
LatLng latLng = new LatLng(location.getLatitude(), location.getLongitude());
int drawableId = waypoint.getType() == WaypointType.STATISTICS
? R.drawable.ic_marker_yellow_pushpin : R.drawable.ic_marker_blue_pushpin;
MarkerOptions markerOptions = new MarkerOptions().position(latLng)
.anchor(WAYPOINT_X_ANCHOR, WAYPOINT_Y_ANCHOR).draggable(false).visible(true)
.icon(BitmapDescriptorFactory.fromResource(drawableId))
.title(String.valueOf(waypoint.getId()));
googleMap.addMarker(markerOptions);
}
}
}
}
@@ -0,0 +1,132 @@
/*
* Copyright 2009 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.fragments.DeleteMarkerDialogFragment.DeleteMarkerCaller;
import com.google.android.apps.mytracks.fragments.MarkerDetailFragment;
import com.google.android.maps.mytracks.R;
import android.database.Cursor;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentStatePagerAdapter;
import android.support.v4.view.ViewPager;
import android.util.Log;
import java.util.ArrayList;
/**
* An activity to display marker detail info.
*
* @author Leif Hendrik Wilden
*/
public class MarkerDetailActivity extends AbstractMyTracksActivity implements DeleteMarkerCaller {
public static final String EXTRA_MARKER_ID = "marker_id";
private static final String TAG = MarkerDetailActivity.class.getSimpleName();
private Waypoint waypoint;
private ArrayList<Long> markerIds;
@Override
protected void onCreate(Bundle bundle) {
super.onCreate(bundle);
long markerId = getIntent().getLongExtra(EXTRA_MARKER_ID, -1L);
if (markerId == -1L) {
Log.d(TAG, "invalid marker id");
finish();
return;
}
MyTracksProviderUtils myTracksProviderUtils = MyTracksProviderUtils.Factory.get(this);
waypoint = myTracksProviderUtils.getWaypoint(markerId);
markerIds = new ArrayList<Long>();
int markerIndex = -1;
Cursor cursor = null;
try {
cursor = myTracksProviderUtils.getWaypointCursor(waypoint.getTrackId(), -1L, -1);
if (cursor != null && cursor.moveToFirst()) {
/*
* Yes, this will skip the first waypoint and that is intentional as the
* first waypoint holds the stats for the track.
*/
while (cursor.moveToNext()) {
Waypoint current = myTracksProviderUtils.createWaypoint(cursor);
markerIds.add(current.getId());
if (current.getId() == markerId) {
markerIndex = markerIds.size() - 1;
}
}
}
} finally {
if (cursor != null) {
cursor.close();
}
}
ViewPager viewPager = (ViewPager) findViewById(R.id.maker_detail_activity_view_pager);
viewPager.setAdapter(new MarkerDetailPagerAdapter(getSupportFragmentManager()));
if (markerIndex != -1) {
viewPager.setCurrentItem(markerIndex);
}
}
@Override
protected int getLayoutResId() {
return R.layout.marker_detail_activity;
}
@Override
public void onDeleteMarkerDone() {
runOnUiThread(new Runnable() {
@Override
public void run() {
finish();
}
});
}
/**
* Marker detail pager adapter.
*
* @author Jimmy Shih
*/
private class MarkerDetailPagerAdapter extends FragmentStatePagerAdapter {
public MarkerDetailPagerAdapter(FragmentManager fragmentManager) {
super(fragmentManager);
}
@Override
public Fragment getItem(int position) {
String title = getString(R.string.marker_title, position + 1, getCount());
return MarkerDetailFragment.newInstance(markerIds.get(position), title);
}
@Override
public int getCount() {
return markerIds.size();
}
}
}
@@ -0,0 +1,205 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.content.Waypoint.WaypointType;
import com.google.android.apps.mytracks.content.WaypointCreationRequest;
import com.google.android.apps.mytracks.services.TrackRecordingServiceConnection;
import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.android.apps.mytracks.util.TrackRecordingServiceConnectionUtils;
import com.google.android.maps.mytracks.R;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.ArrayAdapter;
import android.widget.AutoCompleteTextView;
import android.widget.Button;
import android.widget.EditText;
/**
* An activity to add/edit a marker.
*
* @author Jimmy Shih
*/
public class MarkerEditActivity extends AbstractMyTracksActivity {
private static final String TAG = MarkerEditActivity.class.getSimpleName();
public static final String EXTRA_TRACK_ID = "track_id";
public static final String EXTRA_MARKER_ID = "marker_id";
private long trackId;
private long markerId;
private TrackRecordingServiceConnection trackRecordingServiceConnection;
private Waypoint waypoint;
// UI elements
private View statisticsSection;
private EditText statisticsName;
private View waypointSection;
private EditText waypointName;
private AutoCompleteTextView waypointMarkerType;
private EditText waypointDescription;
private Button done;
@Override
protected void onCreate(Bundle bundle) {
super.onCreate(bundle);
trackId = getIntent().getLongExtra(EXTRA_TRACK_ID, -1L);
markerId = getIntent().getLongExtra(EXTRA_MARKER_ID, -1L);
trackRecordingServiceConnection = new TrackRecordingServiceConnection(this, null);
// Setup UI elements
statisticsSection = findViewById(R.id.marker_edit_statistics_section);
statisticsName = (EditText) findViewById(R.id.marker_edit_statistics_name);
waypointSection = findViewById(R.id.marker_edit_waypoint_section);
waypointName = (EditText) findViewById(R.id.marker_edit_waypoint_name);
waypointMarkerType = (AutoCompleteTextView) findViewById(R.id.marker_edit_waypoint_marker_type);
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(
this, R.array.waypoint_types, android.R.layout.simple_dropdown_item_1line);
waypointMarkerType.setAdapter(adapter);
waypointDescription = (EditText) findViewById(R.id.marker_edit_waypoint_description);
Button cancel = (Button) findViewById(R.id.marker_edit_cancel);
cancel.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
finish();
}
});
done = (Button) findViewById(R.id.marker_edit_done);
updateUiByMarkerId();
}
@Override
protected void onStart() {
super.onStart();
TrackRecordingServiceConnectionUtils.startConnection(this, trackRecordingServiceConnection);
}
@Override
protected void onStop() {
super.onStop();
trackRecordingServiceConnection.unbind();
}
@Override
protected int getLayoutResId() {
return R.layout.marker_edit;
}
/**
* Updates the UI based on the marker id.
*/
private void updateUiByMarkerId() {
final boolean newMarker = markerId == -1L;
setTitle(newMarker ? R.string.menu_insert_marker : R.string.menu_edit);
done.setText(newMarker ? R.string.generic_add : R.string.generic_save);
done.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (newMarker) {
addMarker();
} else {
saveMarker();
}
boolean driveSync = PreferencesUtils.getBoolean(
MarkerEditActivity.this, R.string.drive_sync_key, PreferencesUtils.DRIVE_SYNC_DEFAULT);
if (driveSync) {
MyTracksProviderUtils myTracksProviderUtils = MyTracksProviderUtils.Factory.get(
MarkerEditActivity.this);
Track track = myTracksProviderUtils.getTrack(newMarker ? trackId : waypoint.getTrackId());
track.setModifiedTime(System.currentTimeMillis());
myTracksProviderUtils.updateTrack(track);
PreferencesUtils.addToList(MarkerEditActivity.this, R.string.drive_edited_list_key,
PreferencesUtils.DRIVE_EDITED_LIST_DEFAULT, String.valueOf(track.getId()));
}
finish();
}
});
if (newMarker) {
statisticsSection.setVisibility(View.GONE);
waypointSection.setVisibility(View.VISIBLE);
int nextWaypointNumber = trackId == -1L ? -1
: MyTracksProviderUtils.Factory.get(this).getNextWaypointNumber(trackId, WaypointType.WAYPOINT);
if (nextWaypointNumber == -1) {
nextWaypointNumber = 0;
}
waypointName.setText(getString(R.string.marker_name_format, nextWaypointNumber));
waypointName.selectAll();
waypointMarkerType.setText("");
waypointDescription.setText("");
} else {
waypoint = MyTracksProviderUtils.Factory.get(this).getWaypoint(markerId);
if (waypoint == null) {
Log.d(TAG, "waypoint is null");
finish();
return;
}
boolean statistics = waypoint.getType() == WaypointType.STATISTICS;
statisticsSection.setVisibility(statistics ? View.VISIBLE : View.GONE);
waypointSection.setVisibility(statistics ? View.GONE : View.VISIBLE);
if (statistics) {
statisticsName.setText(waypoint.getName());
} else {
waypointName.setText(waypoint.getName());
waypointMarkerType.setText(waypoint.getCategory());
waypointDescription.setText(waypoint.getDescription());
}
}
}
/**
* Adds a marker.
*/
private void addMarker() {
WaypointCreationRequest waypointCreationRequest = new WaypointCreationRequest(
WaypointType.WAYPOINT,
false,
waypointName.getText().toString(),
waypointMarkerType.getText().toString(),
waypointDescription.getText().toString(),
null,
null);
TrackRecordingServiceConnectionUtils.addMarker(
this, trackRecordingServiceConnection, waypointCreationRequest);
}
/**
* Saves a marker.
*/
private void saveMarker() {
boolean statistics = waypoint.getType() == WaypointType.STATISTICS;
if (statistics) {
waypoint.setName(statisticsName.getText().toString());
} else {
waypoint.setName(waypointName.getText().toString());
waypoint.setCategory(waypointMarkerType.getText().toString());
waypoint.setDescription(waypointDescription.getText().toString());
}
MyTracksProviderUtils.Factory.get(this).updateWaypoint(waypoint);
}
}
@@ -0,0 +1,352 @@
/*
* Copyright 2009 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.Waypoint.WaypointType;
import com.google.android.apps.mytracks.content.WaypointsColumns;
import com.google.android.apps.mytracks.fragments.DeleteMarkerDialogFragment;
import com.google.android.apps.mytracks.fragments.DeleteMarkerDialogFragment.DeleteMarkerCaller;
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import com.google.android.apps.mytracks.util.IntentUtils;
import com.google.android.apps.mytracks.util.ListItemUtils;
import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.android.maps.mytracks.R;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.database.Cursor;
import android.os.Bundle;
import android.support.v4.app.LoaderManager.LoaderCallbacks;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.support.v4.widget.ResourceCursorAdapter;
import android.util.Log;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.AdapterContextMenuInfo;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ListView;
/**
* Activity to show a list of markers in a track.
*
* @author Leif Hendrik Wilden
*/
public class MarkerListActivity extends AbstractMyTracksActivity implements DeleteMarkerCaller {
public static final String EXTRA_TRACK_ID = "track_id";
private static final String TAG = MarkerListActivity.class.getSimpleName();
private static final String[] PROJECTION = new String[] { WaypointsColumns._ID,
WaypointsColumns.NAME, WaypointsColumns.DESCRIPTION, WaypointsColumns.CATEGORY,
WaypointsColumns.TYPE, WaypointsColumns.TIME, WaypointsColumns.PHOTOURL };
// Callback when an item is selected in the contextual action mode
private ContextualActionModeCallback
contextualActionModeCallback = new ContextualActionModeCallback() {
@Override
public void onPrepare(Menu menu, int[] positions, long[] ids, boolean showSelectAll) {
boolean isSingleSelection = ids.length == 1;
// Always disable
menu.findItem(R.id.list_context_menu_play).setVisible(false);
// Always disable
menu.findItem(R.id.list_context_menu_share).setVisible(false);
// One item
menu.findItem(R.id.list_context_menu_show_on_map).setVisible(isSingleSelection);
// One item, track not sharedWithMe
menu.findItem(R.id.list_context_menu_edit)
.setVisible(isSingleSelection && !track.isSharedWithMe());
// Track not sharedWithMe
menu.findItem(R.id.list_context_menu_delete).setVisible(!track.isSharedWithMe());
/*
* Set select all to the same visibility as delete since delete is the
* only action that can be applied to multiple markers.
*/
menu.findItem(R.id.list_context_menu_select_all)
.setVisible(showSelectAll && !track.isSharedWithMe());
}
@Override
public boolean onClick(int itemId, int[] positions, long[] ids) {
return handleContextItem(itemId, ids);
}
};
/*
* Note that sharedPreferenceChangeListener cannot be an anonymous inner
* class. Anonymous inner class will get garbage collected.
*/
private final OnSharedPreferenceChangeListener
sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
// Note that the key can be null
if (key == null || key.equals(
PreferencesUtils.getKey(MarkerListActivity.this, R.string.recording_track_id_key))) {
recordingTrackId = PreferencesUtils.getLong(
MarkerListActivity.this, R.string.recording_track_id_key);
}
if (key == null || key.equals(PreferencesUtils.getKey(
MarkerListActivity.this, R.string.recording_track_paused_key))) {
recordingTrackPaused = PreferencesUtils.getBoolean(MarkerListActivity.this,
R.string.recording_track_paused_key,
PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT);
}
if (key != null) {
runOnUiThread(new Runnable() {
@Override
public void run() {
ApiAdapterFactory.getApiAdapter().invalidMenu(MarkerListActivity.this);
}
});
}
}
};
private MyTracksProviderUtils myTracksProviderUtils;
private SharedPreferences sharedPreferences;
private long recordingTrackId = PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
private boolean recordingTrackPaused = PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT;
private Track track;
private ResourceCursorAdapter resourceCursorAdapter;
// UI elements
private ListView listView;
private MenuItem insertMarkerMenuItem;
private MenuItem searchMenuItem;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
myTracksProviderUtils = MyTracksProviderUtils.Factory.get(this);
sharedPreferences = getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
long trackId = getIntent().getLongExtra(EXTRA_TRACK_ID, -1L);
if (trackId == -1L) {
Log.d(TAG, "invalid track id");
finish();
return;
}
track = myTracksProviderUtils.getTrack(trackId);
setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
listView = (ListView) findViewById(R.id.marker_list);
listView.setEmptyView(findViewById(R.id.marker_list_empty));
listView.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Intent intent = IntentUtils.newIntent(MarkerListActivity.this, MarkerDetailActivity.class)
.putExtra(MarkerDetailActivity.EXTRA_MARKER_ID, id);
startActivity(intent);
}
});
resourceCursorAdapter = new ResourceCursorAdapter(this, R.layout.list_item, null, 0) {
@Override
public void bindView(View view, Context context, Cursor cursor) {
int typeIndex = cursor.getColumnIndex(WaypointsColumns.TYPE);
int nameIndex = cursor.getColumnIndex(WaypointsColumns.NAME);
int timeIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.TIME);
int categoryIndex = cursor.getColumnIndex(WaypointsColumns.CATEGORY);
int descriptionIndex = cursor.getColumnIndex(WaypointsColumns.DESCRIPTION);
int photoUrlIndex = cursor.getColumnIndex(WaypointsColumns.PHOTOURL);
boolean statistics = WaypointType.values()[cursor.getInt(typeIndex)]
== WaypointType.STATISTICS;
int iconId = statistics ? R.drawable.ic_marker_yellow_pushpin
: R.drawable.ic_marker_blue_pushpin;
String name = cursor.getString(nameIndex);
long time = cursor.getLong(timeIndex);
String category = statistics ? null : cursor.getString(categoryIndex);
String description = statistics ? null : cursor.getString(descriptionIndex);
String photoUrl = cursor.getString(photoUrlIndex);
ListItemUtils.setListItem(MarkerListActivity.this, view, false, true, iconId,
R.string.image_marker, name, null, null, null, 0, time, false, category, description,
photoUrl);
}
};
listView.setAdapter(resourceCursorAdapter);
ApiAdapterFactory.getApiAdapter()
.configureListViewContextualMenu(this, listView, contextualActionModeCallback);
final long firstWaypointId = myTracksProviderUtils.getFirstWaypointId(trackId);
getSupportLoaderManager().initLoader(0, null, new LoaderCallbacks<Cursor>() {
@Override
public Loader<Cursor> onCreateLoader(int arg0, Bundle arg1) {
return new CursorLoader(MarkerListActivity.this, WaypointsColumns.CONTENT_URI, PROJECTION,
WaypointsColumns.TRACKID + "=? AND " + WaypointsColumns._ID + "!=?",
new String[] { String.valueOf(track.getId()), String.valueOf(firstWaypointId) }, null);
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
resourceCursorAdapter.swapCursor(cursor);
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
resourceCursorAdapter.swapCursor(null);
}
});
}
@Override
protected void onStart() {
super.onStart();
sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
sharedPreferenceChangeListener.onSharedPreferenceChanged(null, null);
}
@Override
protected void onResume() {
super.onResume();
ApiAdapterFactory.getApiAdapter().invalidMenu(this);
}
@Override
protected void onStop() {
super.onStop();
sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
}
@Override
protected int getLayoutResId() {
return R.layout.marker_list;
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.marker_list, menu);
insertMarkerMenuItem = menu.findItem(R.id.marker_list_insert_marker);
searchMenuItem = menu.findItem(R.id.marker_list_search);
ApiAdapterFactory.getApiAdapter().configureSearchWidget(this, searchMenuItem, null);
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
if (insertMarkerMenuItem != null) {
insertMarkerMenuItem.setVisible(track.getId() == recordingTrackId && !recordingTrackPaused);
}
return super.onPrepareOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.marker_list_insert_marker:
Intent intent = IntentUtils.newIntent(this, MarkerEditActivity.class)
.putExtra(MarkerEditActivity.EXTRA_TRACK_ID, track.getId());
startActivity(intent);
return true;
case R.id.marker_list_search:
return ApiAdapterFactory.getApiAdapter().handleSearchMenuSelection(this);
default:
return super.onOptionsItemSelected(item);
}
}
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
super.onCreateContextMenu(menu, v, menuInfo);
getMenuInflater().inflate(R.menu.list_context_menu, menu);
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
contextualActionModeCallback.onPrepare(
menu, new int[] { info.position }, new long[] { info.id }, false);
}
@Override
public boolean onContextItemSelected(MenuItem item) {
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
if (handleContextItem(item.getItemId(), new long[] {info.id})) {
return true;
}
return super.onContextItemSelected(item);
}
/**
* Handles a context item selection.
*
* @param itemId the menu item id
* @param markerIds the marker ids
* @return true if handled.
*/
private boolean handleContextItem(int itemId, long[] markerIds) {
Intent intent;
switch (itemId) {
case R.id.list_context_menu_show_on_map:
if (markerIds.length == 1) {
intent = IntentUtils.newIntent(this, TrackDetailActivity.class)
.putExtra(TrackDetailActivity.EXTRA_MARKER_ID, markerIds[0]);
startActivity(intent);
}
return true;
case R.id.list_context_menu_edit:
if (markerIds.length == 1) {
intent = IntentUtils.newIntent(this, MarkerEditActivity.class)
.putExtra(MarkerEditActivity.EXTRA_MARKER_ID, markerIds[0]);
startActivity(intent);
}
return true;
case R.id.list_context_menu_delete:
if (markerIds.length > 1 && markerIds.length == listView.getCount()) {
markerIds = new long[] { -1L };
}
DeleteMarkerDialogFragment.newInstance(markerIds)
.show(getSupportFragmentManager(), DeleteMarkerDialogFragment.DELETE_MARKER_DIALOG_TAG);
return true;
case R.id.list_context_menu_select_all:
int size = listView.getCount();
for (int i = 0; i < size; i++) {
listView.setItemChecked(i, true);
}
return false;
default:
return false;
}
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_SEARCH && searchMenuItem != null) {
if (ApiAdapterFactory.getApiAdapter().handleSearchKey(searchMenuItem)) {
return true;
}
}
return super.onKeyUp(keyCode, event);
}
@Override
public void onDeleteMarkerDone() {
// Do nothing
}
}
@@ -0,0 +1,559 @@
/*
* Copyright 2011 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.SearchEngine;
import com.google.android.apps.mytracks.content.SearchEngine.ScoredResult;
import com.google.android.apps.mytracks.content.SearchEngine.SearchQuery;
import com.google.android.apps.mytracks.content.SearchEngineProvider;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.content.Waypoint.WaypointType;
import com.google.android.apps.mytracks.fragments.DeleteMarkerDialogFragment;
import com.google.android.apps.mytracks.fragments.DeleteMarkerDialogFragment.DeleteMarkerCaller;
import com.google.android.apps.mytracks.services.MyTracksLocationManager;
import com.google.android.apps.mytracks.services.TrackRecordingServiceConnection;
import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import com.google.android.apps.mytracks.util.IntentUtils;
import com.google.android.apps.mytracks.util.ListItemUtils;
import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.android.apps.mytracks.util.StringUtils;
import com.google.android.apps.mytracks.util.TrackIconUtils;
import com.google.android.apps.mytracks.util.TrackRecordingServiceConnectionUtils;
import com.google.android.gms.location.LocationListener;
import com.google.android.maps.mytracks.R;
import android.app.SearchManager;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.location.Location;
import android.os.Bundle;
import android.os.Looper;
import android.provider.SearchRecentSuggestions;
import android.util.Log;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.AdapterContextMenuInfo;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.SortedSet;
/**
* An activity to display a list of search results.
*
* @author Rodrigo Damazio
*/
public class SearchListActivity extends AbstractSendToGoogleActivity implements DeleteMarkerCaller {
private static final String TAG = SearchListActivity.class.getSimpleName();
private static final String IS_RECORDING_FIELD = "isRecording";
private static final String IS_PAUSED_FIELD = "isPaused";
private static final String ICON_ID_FIELD = "icon";
private static final String ICON_CONTENT_DESCRIPTION_ID_FIELD = "iconContentDescription";
private static final String NAME_FIELD = "name";
private static final String SHARED_OWNER_FIELD = "sharedOwner";
private static final String TOTAL_TIME_FIELD = "totalTime";
private static final String TOTAL_DISTANCE_FIELD = "totalDistance";
private static final String MARKER_COUNT_FIELD = "markerCount";
private static final String START_TIME_FIELD = "startTime";
private static final String CATEGORY_FIELD = "category";
private static final String DESCRIPTION_FIELD = "description";
private static final String PHOTO_URL_FIELD = "photoUrl";
private static final String TRACK_ID_FIELD = "trackId";
private static final String MARKER_ID_FIELD = "markerId";
private final OnSharedPreferenceChangeListener
sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
if (key == null || key.equals(
PreferencesUtils.getKey(SearchListActivity.this, R.string.stats_units_key))) {
metricUnits = PreferencesUtils.isMetricUnits(SearchListActivity.this);
}
if (key == null || key.equals(
PreferencesUtils.getKey(SearchListActivity.this, R.string.recording_track_id_key))) {
recordingTrackId = PreferencesUtils.getLong(
SearchListActivity.this, R.string.recording_track_id_key);
}
if (key == null || key.equals(PreferencesUtils.getKey(
SearchListActivity.this, R.string.recording_track_paused_key))) {
recordingTrackPaused = PreferencesUtils.getBoolean(SearchListActivity.this,
R.string.recording_track_paused_key,
PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT);
}
if (key != null) {
runOnUiThread(new Runnable() {
@Override
public void run() {
arrayAdapter.notifyDataSetChanged();
}
});
}
}
};
// Callback when an item is selected in the contextual action mode
private ContextualActionModeCallback
contextualActionModeCallback = new ContextualActionModeCallback() {
@Override
public boolean onClick(int itemId, int[] positions, long[] ids) {
return handleContextItem(itemId, positions);
}
@Override
public void onPrepare(Menu menu, int[] positions, long[] ids, boolean showSelectAll) {
boolean isRecording = recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
boolean isSingleSelection = positions.length == 1;
boolean isSingleSelectionShareWithMe;
boolean isSingleSelectionTrack;
if (isSingleSelection) {
Map<String, Object> item = arrayAdapter.getItem(positions[0]);
Long trackId = (Long) item.get(TRACK_ID_FIELD);
Track track = myTracksProviderUtils.getTrack(trackId);
isSingleSelectionShareWithMe = track.isSharedWithMe();
isSingleSelectionTrack = item.get(MARKER_ID_FIELD) == null;
} else {
isSingleSelectionShareWithMe = false;
isSingleSelectionTrack = false;
}
// Not recording, one item, item is a track
menu.findItem(R.id.list_context_menu_play)
.setVisible(!isRecording && isSingleSelection && isSingleSelectionTrack);
// Not recording, one item, item is a track, not shareWithMe item
menu.findItem(R.id.list_context_menu_share).setVisible(!isRecording && isSingleSelection
&& isSingleSelectionTrack && !isSingleSelectionShareWithMe);
// One item, item is a marker
menu.findItem(R.id.list_context_menu_show_on_map)
.setVisible(isSingleSelection && !isSingleSelectionTrack);
// One item, can be a track or a marker, cannot be a sharedWithMe item
menu.findItem(R.id.list_context_menu_edit)
.setVisible(isSingleSelection && !isSingleSelectionShareWithMe);
// One item. If track, no restriction. If marker, cannot be a
// shareWithMe item
menu.findItem(R.id.list_context_menu_delete).setVisible(
isSingleSelection && (isSingleSelectionTrack || !isSingleSelectionShareWithMe));
// Disable select all, no action is available for multiple selection
menu.findItem(R.id.list_context_menu_select_all).setVisible(false);
}
};
private MyTracksProviderUtils myTracksProviderUtils;
private SharedPreferences sharedPreferences;
private TrackRecordingServiceConnection trackRecordingServiceConnection;
private SearchEngine searchEngine;
private SearchRecentSuggestions searchRecentSuggestions;
private ArrayAdapter<Map<String, Object>> arrayAdapter;
private boolean metricUnits = true;
private long recordingTrackId = PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
private boolean recordingTrackPaused = PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT;
// UI elements
private ListView listView;
private MenuItem searchMenuItem;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
myTracksProviderUtils = MyTracksProviderUtils.Factory.get(this);
sharedPreferences = getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
trackRecordingServiceConnection = new TrackRecordingServiceConnection(this, null);
searchEngine = new SearchEngine(myTracksProviderUtils);
searchRecentSuggestions = SearchEngineProvider.newHelper(this);
listView = (ListView) findViewById(R.id.search_list);
listView.setEmptyView(findViewById(R.id.search_list_empty));
listView.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Map<String, Object> item = arrayAdapter.getItem(position);
Long trackId = (Long) item.get(TRACK_ID_FIELD);
Long markerId = (Long) item.get(MARKER_ID_FIELD);
Intent intent;
if (markerId != null) {
intent = IntentUtils.newIntent(SearchListActivity.this, TrackDetailActivity.class)
.putExtra(TrackDetailActivity.EXTRA_MARKER_ID, markerId);
} else {
intent = IntentUtils.newIntent(SearchListActivity.this, TrackDetailActivity.class)
.putExtra(TrackDetailActivity.EXTRA_TRACK_ID, trackId);
}
startActivity(intent);
}
});
arrayAdapter = new ArrayAdapter<Map<String, Object>>(
this, R.layout.list_item, R.id.list_item_name) {
@Override
public View getView(int position, View convertView, android.view.ViewGroup parent) {
View view;
if (convertView == null) {
view = getLayoutInflater().inflate(R.layout.list_item, parent, false);
} else {
view = convertView;
}
Map<String, Object> resultMap = getItem(position);
boolean isRecording = (Boolean) resultMap.get(IS_RECORDING_FIELD);
boolean isPaused = (Boolean) resultMap.get(IS_PAUSED_FIELD);
int iconId = (Integer) resultMap.get(ICON_ID_FIELD);
int iconContentDescriptionId = (Integer) resultMap.get(ICON_CONTENT_DESCRIPTION_ID_FIELD);
String name = (String) resultMap.get(NAME_FIELD);
String sharedOwner = (String) resultMap.get(SHARED_OWNER_FIELD);
String totalTime = (String) resultMap.get(TOTAL_TIME_FIELD);
String totalDistance = (String) resultMap.get(TOTAL_DISTANCE_FIELD);
int markerCount = (Integer) resultMap.get(MARKER_COUNT_FIELD);
Long startTime = (Long) resultMap.get(START_TIME_FIELD);
String category = (String) resultMap.get(CATEGORY_FIELD);
String description = (String) resultMap.get(DESCRIPTION_FIELD);
String photoUrl = (String) resultMap.get(PHOTO_URL_FIELD);
ListItemUtils.setListItem(SearchListActivity.this, view, isRecording, isPaused, iconId,
iconContentDescriptionId, name, sharedOwner, totalTime, totalDistance, markerCount,
startTime, false, category, description, photoUrl);
return view;
}
};
listView.setAdapter(arrayAdapter);
ApiAdapterFactory.getApiAdapter()
.configureListViewContextualMenu(this, listView, contextualActionModeCallback);
handleIntent(getIntent());
}
@Override
protected void onStart() {
super.onStart();
sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
sharedPreferenceChangeListener.onSharedPreferenceChanged(null, null);
TrackRecordingServiceConnectionUtils.startConnection(this, trackRecordingServiceConnection);
}
@Override
protected void onResume() {
super.onResume();
arrayAdapter.notifyDataSetChanged();
}
@Override
protected void onStop() {
super.onStop();
sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
trackRecordingServiceConnection.unbind();
}
@Override
protected int getLayoutResId() {
return R.layout.search_list;
}
@Override
public void onNewIntent(Intent intent) {
setIntent(intent);
handleIntent(intent);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.search_list, menu);
searchMenuItem = menu.findItem(R.id.search_list_search);
ApiAdapterFactory.getApiAdapter().configureSearchWidget(this, searchMenuItem, null);
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.search_list_search:
return ApiAdapterFactory.getApiAdapter().handleSearchMenuSelection(this);
default:
return super.onOptionsItemSelected(item);
}
}
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
super.onCreateContextMenu(menu, v, menuInfo);
getMenuInflater().inflate(R.menu.list_context_menu, menu);
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
contextualActionModeCallback.onPrepare(
menu, new int[] { info.position }, new long[] { info.id }, false);
}
@Override
public boolean onContextItemSelected(MenuItem item) {
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
if (handleContextItem(item.getItemId(), new int[] {info.position})) {
return true;
}
return super.onContextItemSelected(item);
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_SEARCH && searchMenuItem != null) {
if (ApiAdapterFactory.getApiAdapter().handleSearchKey(searchMenuItem)) {
return true;
}
}
return super.onKeyUp(keyCode, event);
}
/**
* Handles a context item selection.
*
* @param itemId the menu item id
* @param positions the positions of the selected rows
* @return true if handled.
*/
private boolean handleContextItem(int itemId, int[] positions) {
if (positions.length != 1) {
return false;
}
Map<String, Object> item = arrayAdapter.getItem(positions[0]);
Long trackId = (Long) item.get(TRACK_ID_FIELD);
Long markerId = (Long) item.get(MARKER_ID_FIELD);
Intent intent;
switch (itemId) {
case R.id.list_context_menu_play:
playTracks(new long[] {trackId});
return true;
case R.id.list_context_menu_share:
shareTrack(trackId);
return true;
case R.id.list_context_menu_show_on_map:
intent = IntentUtils.newIntent(this, TrackDetailActivity.class)
.putExtra(TrackDetailActivity.EXTRA_MARKER_ID, markerId);
startActivity(intent);
return true;
case R.id.list_context_menu_edit:
if (markerId != null) {
intent = IntentUtils.newIntent(this, MarkerEditActivity.class)
.putExtra(MarkerEditActivity.EXTRA_MARKER_ID, markerId);
} else {
intent = IntentUtils.newIntent(this, TrackEditActivity.class)
.putExtra(TrackEditActivity.EXTRA_TRACK_ID, trackId);
}
startActivity(intent);
// Close the search result since its content can change after edit.
finish();
return true;
case R.id.list_context_menu_delete:
if (markerId != null) {
DeleteMarkerDialogFragment.newInstance(new long[] { markerId }).show(
getSupportFragmentManager(), DeleteMarkerDialogFragment.DELETE_MARKER_DIALOG_TAG);
} else {
deleteTracks(new long[] { trackId });
}
return true;
default:
return false;
}
}
/**
* Handles the intent.
*
* @param intent the intent
*/
private void handleIntent(Intent intent) {
if (!Intent.ACTION_SEARCH.equals(intent.getAction())) {
Log.e(TAG, "Invalid intent action: " + intent);
finish();
return;
}
final String textQuery = intent.getStringExtra(SearchManager.QUERY);
setTitle(textQuery);
final MyTracksLocationManager myTracksLocationManager = new MyTracksLocationManager(
this, Looper.myLooper(), true);
LocationListener locationListener = new LocationListener() {
@Override
public void onLocationChanged(final Location location) {
myTracksLocationManager.close();
new Thread() {
@Override
public void run() {
SearchQuery query = new SearchQuery(
textQuery, location, -1L, System.currentTimeMillis());
doSearch(query);
}
}.start();
}
};
myTracksLocationManager.requestLastLocation(locationListener);
}
/**
* Do the search.
*
* @param query the query
*/
private void doSearch(SearchQuery query) {
SortedSet<ScoredResult> scoredResults = searchEngine.search(query);
final List<Map<String, Object>> displayResults = prepareResultsforDisplay(scoredResults);
// Use the UI thread to display the results
runOnUiThread(new Runnable() {
@Override
public void run() {
arrayAdapter.clear();
ApiAdapterFactory.getApiAdapter().addAllToArrayAdapter(arrayAdapter, displayResults);
}
});
// Save the query as a suggestion for the future
searchRecentSuggestions.saveRecentQuery(query.textQuery, null);
}
/**
* Prepares the result for display.
*
* @param scoredResults a list of score results
* @return a list of result maps
*/
private List<Map<String, Object>> prepareResultsforDisplay(
Collection<ScoredResult> scoredResults) {
ArrayList<Map<String, Object>> output = new ArrayList<Map<String, Object>>(
scoredResults.size());
for (ScoredResult result : scoredResults) {
Map<String, Object> resultMap = new HashMap<String, Object>();
if (result.track != null) {
prepareTrackForDisplay(result.track, resultMap);
} else {
prepareMarkerForDisplay(result.waypoint, resultMap);
}
output.add(resultMap);
}
return output;
}
/**
* Prepares a marker for display by filling in a result map.
*
* @param waypoint the marker
* @param resultMap the result map
*/
private void prepareMarkerForDisplay(Waypoint waypoint, Map<String, Object> resultMap) {
/*
* TODO: It may be more appropriate to obtain the track name as a join in
* the retrieval phase of the search.
*/
String trackName = null;
long trackId = waypoint.getTrackId();
if (trackId != -1L) {
Track track = myTracksProviderUtils.getTrack(trackId);
if (track != null) {
trackName = track.getName();
}
}
boolean statistics = waypoint.getType() == WaypointType.STATISTICS;
resultMap.put(IS_RECORDING_FIELD, false);
resultMap.put(IS_PAUSED_FIELD, true);
resultMap.put(ICON_ID_FIELD,
statistics ? R.drawable.ic_marker_yellow_pushpin : R.drawable.ic_marker_blue_pushpin);
resultMap.put(ICON_CONTENT_DESCRIPTION_ID_FIELD, R.string.image_marker);
resultMap.put(NAME_FIELD, waypoint.getName());
resultMap.put(SHARED_OWNER_FIELD, null);
// Display the marker's track name in the total time field
resultMap.put(TOTAL_TIME_FIELD, trackName == null ? null
: getString(R.string.search_list_marker_track_location, trackName));
resultMap.put(TOTAL_DISTANCE_FIELD, null);
resultMap.put(MARKER_COUNT_FIELD, 0);
resultMap.put(START_TIME_FIELD, waypoint.getLocation().getTime());
resultMap.put(CATEGORY_FIELD, statistics ? null : waypoint.getCategory());
resultMap.put(DESCRIPTION_FIELD, statistics ? null : waypoint.getDescription());
resultMap.put(PHOTO_URL_FIELD, waypoint.getPhotoUrl());
resultMap.put(TRACK_ID_FIELD, waypoint.getTrackId());
resultMap.put(MARKER_ID_FIELD, waypoint.getId());
}
/**
* Prepares a track for display by filling in a result map.
*
* @param track the track
* @param resultMap the result map
*/
private void prepareTrackForDisplay(Track track, Map<String, Object> resultMap) {
TripStatistics tripStatitics = track.getTripStatistics();
String icon = track.getIcon();
String category = icon != null && !icon.equals("") ? null : track.getCategory();
resultMap.put(IS_RECORDING_FIELD, track.getId() == recordingTrackId);
resultMap.put(IS_PAUSED_FIELD, recordingTrackPaused);
resultMap.put(ICON_ID_FIELD, TrackIconUtils.getIconDrawable(icon));
resultMap.put(ICON_CONTENT_DESCRIPTION_ID_FIELD, R.string.image_track);
resultMap.put(NAME_FIELD, track.getName());
resultMap.put(SHARED_OWNER_FIELD, track.getSharedOwner());
resultMap.put(TOTAL_TIME_FIELD, StringUtils.formatElapsedTime(tripStatitics.getTotalTime()));
resultMap.put(TOTAL_DISTANCE_FIELD,
StringUtils.formatDistance(this, tripStatitics.getTotalDistance(), metricUnits));
resultMap.put(MARKER_COUNT_FIELD, myTracksProviderUtils.getWaypointCount(track.getId()));
resultMap.put(START_TIME_FIELD, tripStatitics.getStartTime());
resultMap.put(CATEGORY_FIELD, category);
resultMap.put(DESCRIPTION_FIELD, track.getDescription());
resultMap.put(PHOTO_URL_FIELD, null);
resultMap.put(TRACK_ID_FIELD, track.getId());
resultMap.put(MARKER_ID_FIELD, null);
}
@Override
public void onDeleteMarkerDone() {
runOnUiThread(new Runnable() {
@Override
public void run() {
handleIntent(getIntent());
}
});
}
@Override
protected TrackRecordingServiceConnection getTrackRecordingServiceConnection() {
return trackRecordingServiceConnection;
}
@Override
protected void onDeleted() {
runOnUiThread(new Runnable() {
@Override
public void run() {
handleIntent(getIntent());
}
});
}
}
@@ -0,0 +1,143 @@
/*
* Copyright 2013 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.apps.mytracks.content.TracksColumns;
import com.google.android.maps.mytracks.R;
import android.content.Context;
import android.database.Cursor;
import android.support.v4.widget.ResourceCursorAdapter;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
/**
* A section based resource cursor adapter. Currently only supports one
* additional section header, "Shared with me".
*
* @author Jimmy Shih
*/
public abstract class SectionResourceCursorAdapter extends ResourceCursorAdapter {
private enum ItemType {
TRACK, HEADER
}
private final LayoutInflater layoutInflater;
private int sharedWithMeIndex = -1;
public SectionResourceCursorAdapter(Context context, int layout, Cursor cursor, int flags) {
super(context, layout, cursor, flags);
this.layoutInflater = LayoutInflater.from(context);
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (getItemViewType(position) == ItemType.TRACK.ordinal()) {
return super.getView(getCursorPosition(position), convertView, parent);
} else {
TextView textView;
if (convertView == null) {
textView = (TextView) layoutInflater.inflate(R.layout.track_list_header, parent, false);
} else {
textView = (TextView) convertView;
}
return textView;
}
}
@Override
public int getViewTypeCount() {
return ItemType.values().length;
}
@Override
public int getCount() {
int count = super.getCount();
return count != 0 && sharedWithMeIndex != -1 ? count + 1 : count;
}
@Override
public boolean isEnabled(int position) {
return getItemViewType(position) == ItemType.TRACK.ordinal();
}
@Override
public Object getItem(int position) {
if (getItemViewType(position) == ItemType.TRACK.ordinal()) {
return super.getItem(getCursorPosition(position));
}
return null;
}
@Override
public long getItemId(int position) {
if (getItemViewType(position) == ItemType.TRACK.ordinal()) {
return super.getItemId(getCursorPosition(position));
}
return -1L;
}
@Override
public int getItemViewType(int position) {
if (sharedWithMeIndex != -1 && sharedWithMeIndex == position) {
return ItemType.HEADER.ordinal();
}
return ItemType.TRACK.ordinal();
}
@Override
public Cursor swapCursor(Cursor newCursor) {
Cursor oldCursor = super.swapCursor(newCursor);
sharedWithMeIndex = getSharedWithMeIndex();
return oldCursor;
}
/**
* Gets the Shared with me header index.
*/
private int getSharedWithMeIndex() {
int i = 0;
Cursor cursor = getCursor();
if (cursor != null && cursor.moveToFirst()) {
int index = cursor.getColumnIndex(TracksColumns.SHAREDWITHME);
do {
int sharedWithMe = cursor.getInt(index);
if (sharedWithMe == 1) {
// Returns the index where sharedWithMe == 1
return i;
}
i++;
} while (cursor.moveToNext());
}
return -1;
}
/**
* Gets the underlying cursor position.
*
* @param position the requested position
*/
private int getCursorPosition(int position) {
if (sharedWithMeIndex != -1 && position > sharedWithMeIndex) {
return position - 1;
}
return position;
}
}
@@ -0,0 +1,291 @@
/*
* 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.apps.mytracks.content.Sensor;
import com.google.android.apps.mytracks.services.ITrackRecordingService;
import com.google.android.apps.mytracks.services.TrackRecordingServiceConnection;
import com.google.android.apps.mytracks.services.sensors.SensorManager;
import com.google.android.apps.mytracks.services.sensors.SensorManagerFactory;
import com.google.android.apps.mytracks.services.sensors.SensorUtils;
import com.google.android.apps.mytracks.util.TrackRecordingServiceConnectionUtils;
import com.google.android.apps.mytracks.util.UnitConversions;
import com.google.android.maps.mytracks.R;
import com.google.protobuf.InvalidProtocolBufferException;
import android.os.Bundle;
import android.os.Handler;
import android.os.RemoteException;
import android.text.format.DateFormat;
import android.util.Log;
import android.widget.TextView;
/**
* An activity that displays information about sensors.
*
* @author Sandor Dornbush
*/
public class SensorStateActivity extends AbstractMyTracksActivity {
private static final String TAG = SensorStateActivity.class.getName();
// 1 second in milliseconds
private static final long ONE_SECOND = (long) UnitConversions.S_TO_MS;
private TrackRecordingServiceConnection trackRecordingServiceConnection;
private Handler handler;
private SensorManager tempSensorManager;
private final Runnable updateUiRunnable = new Runnable() {
@Override
public void run() {
ITrackRecordingService trackRecordingService = trackRecordingServiceConnection
.getServiceIfBound();
// Check if service is available and recording
boolean isRecording = false;
if (trackRecordingService != null) {
try {
isRecording = trackRecordingService.isRecording();
} catch (RemoteException e) {
Log.e(TAG, "Unable to determine if the track recording service is recording.", e);
}
}
if (!isRecording) {
updateFromTempSensorManager();
} else {
stopTempSensorManager();
updateFromSystemSensorManager();
}
handler.postDelayed(this, ONE_SECOND);
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
trackRecordingServiceConnection = new TrackRecordingServiceConnection(this, null);
handler = new Handler();
}
@Override
protected void onStart() {
super.onStart();
TrackRecordingServiceConnectionUtils.startConnection(this, trackRecordingServiceConnection);
}
@Override
protected void onResume() {
super.onResume();
handler.post(updateUiRunnable);
}
@Override
protected void onPause() {
super.onPause();
handler.removeCallbacks(updateUiRunnable);
stopTempSensorManager();
}
@Override
protected void onStop() {
super.onStop();
trackRecordingServiceConnection.unbind();
}
@Override
protected int getLayoutResId() {
return R.layout.sensor_state;
}
/**
* Stops the temp sensor manager.
*/
private void stopTempSensorManager() {
if (tempSensorManager != null) {
SensorManagerFactory.releaseTempSensorManager();
tempSensorManager = null;
}
}
/**
* Updates from a temp sensor manager.
*/
private void updateFromTempSensorManager() {
Sensor.SensorState sensorState = Sensor.SensorState.NONE;
Sensor.SensorDataSet sensorDataSet = null;
if (tempSensorManager == null) {
tempSensorManager = SensorManagerFactory.getTempSensorManager(this);
}
if (tempSensorManager != null) {
sensorState = tempSensorManager.getSensorState();
sensorDataSet = tempSensorManager.getSensorDataSet();
}
updateSensorStateAndDataSet(sensorState, sensorDataSet);
}
/**
* Updates from the system sensor manager.
*/
private void updateFromSystemSensorManager() {
Sensor.SensorState sensorState = Sensor.SensorState.NONE;
Sensor.SensorDataSet sensorDataSet = null;
ITrackRecordingService trackRecordingService = trackRecordingServiceConnection
.getServiceIfBound();
// Get sensor details from the service.
if (trackRecordingService == null) {
Log.d(TAG, "Cannot get teh track recording service.");
} else {
try {
sensorState = Sensor.SensorState.valueOf(trackRecordingService.getSensorState());
} catch (RemoteException e) {
Log.e(TAG, "Cannote read sensor state.", e);
sensorState = Sensor.SensorState.NONE;
}
try {
byte[] buff = trackRecordingService.getSensorData();
if (buff != null) {
sensorDataSet = Sensor.SensorDataSet.parseFrom(buff);
}
} catch (RemoteException e) {
Log.e(TAG, "Cannot read sensor data set.", e);
} catch (InvalidProtocolBufferException e) {
Log.e(TAG, "Cannot read sensor data set.", e);
}
}
updateSensorStateAndDataSet(sensorState, sensorDataSet);
}
/**
* Updates the sensor state and data set.
*
* @param sensorState sensor state
* @param sensorDataSet sensor data set
*/
private void updateSensorStateAndDataSet(
Sensor.SensorState sensorState, Sensor.SensorDataSet sensorDataSet) {
((TextView) findViewById(R.id.sensor_state)).setText(
SensorUtils.getStateAsString(sensorState, this));
String lastSensorTime = sensorDataSet == null ? getString(R.string.value_unknown)
: getLastSensorTime(sensorDataSet);
String heartRate = sensorDataSet == null ? getString(R.string.value_unknown)
: getHeartRate(sensorDataSet);
String cadence = sensorDataSet == null ? getString(R.string.value_unknown)
: getCadence(sensorDataSet);
String power = sensorDataSet == null ? getString(R.string.value_unknown)
: getPower(sensorDataSet);
String battery = sensorDataSet == null ? getString(R.string.value_unknown)
: getBattery(sensorDataSet);
((TextView) findViewById(R.id.sensor_state_last_sensor_time)).setText(lastSensorTime);
((TextView) findViewById(R.id.sensor_state_heart_rate)).setText(heartRate);
((TextView) findViewById(R.id.sensor_state_cadence)).setText(cadence);
((TextView) findViewById(R.id.sensor_state_power)).setText(power);
((TextView) findViewById(R.id.sensor_state_battery)).setText(battery);
}
/**
* Gets the last sensor time.
*
* @param sensorDataSet sensor data set
*/
private String getLastSensorTime(Sensor.SensorDataSet sensorDataSet) {
return DateFormat.format("h:mm:ss aa", sensorDataSet.getCreationTime()).toString();
}
/**
* Gets the heart rate.
*
* @param sensorDataSet sensor data set
*/
private String getHeartRate(Sensor.SensorDataSet sensorDataSet) {
String value;
if (sensorDataSet.hasHeartRate() && sensorDataSet.getHeartRate().hasValue()
&& sensorDataSet.getHeartRate().getState() == Sensor.SensorState.SENDING) {
value = getString(
R.string.sensor_state_heart_rate_value, sensorDataSet.getHeartRate().getValue());
} else {
value = SensorUtils.getStateAsString(
sensorDataSet.hasHeartRate() ? sensorDataSet.getHeartRate().getState()
: Sensor.SensorState.NONE, this);
}
return value;
}
/**
* Gets the cadence.
*
* @param sensorDataSet sensor data set
*/
private String getCadence(Sensor.SensorDataSet sensorDataSet) {
String value;
if (sensorDataSet.hasCadence() && sensorDataSet.getCadence().hasValue()
&& sensorDataSet.getCadence().getState() == Sensor.SensorState.SENDING) {
value = getString(R.string.sensor_state_cadence_value, sensorDataSet.getCadence().getValue());
} else {
value = SensorUtils.getStateAsString(
sensorDataSet.hasCadence() ? sensorDataSet.getCadence().getState()
: Sensor.SensorState.NONE, this);
}
return value;
}
/**
* Gets the power.
*
* @param sensorDataSet sensor data set
*/
private String getPower(Sensor.SensorDataSet sensorDataSet) {
String value;
if (sensorDataSet.hasPower() && sensorDataSet.getPower().hasValue()
&& sensorDataSet.getPower().getState() == Sensor.SensorState.SENDING) {
value = getString(R.string.sensor_state_power_value, sensorDataSet.getPower().getValue());
} else {
value = SensorUtils.getStateAsString(
sensorDataSet.hasPower() ? sensorDataSet.getPower().getState() : Sensor.SensorState.NONE,
this);
}
return value;
}
/**
* Gets the battery.
*
* @param sensorDataSet sensor data set
*/
private String getBattery(Sensor.SensorDataSet sensorDataSet) {
String value;
if (sensorDataSet.hasBatteryLevel() && sensorDataSet.getBatteryLevel().hasValue()
&& sensorDataSet.getBatteryLevel().getState() == Sensor.SensorState.SENDING) {
value = getString(R.string.value_integer_percent, sensorDataSet.getBatteryLevel().getValue());
} else {
value = SensorUtils.getStateAsString(
sensorDataSet.hasBatteryLevel() ? sensorDataSet.getBatteryLevel().getState()
: Sensor.SensorState.NONE, this);
}
return value;
}
}
@@ -0,0 +1,150 @@
/*
* Copyright 2013q 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 android.content.Context;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v4.view.ViewPager;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TabHost;
import android.widget.TabHost.TabContentFactory;
import android.widget.TabWidget;
import java.util.ArrayList;
/**
* This is a helper class that implements the management of tabs and all details
* of connecting a ViewPager with associated TabHost. It relies on a trick.
* Normally a tab host has a simple API for supplying a View or Intent that each
* tab will show. This is not sufficient for switching between pages. So instead
* we make the content part of the tab host 0dp high (it is not shown) and the
* TabsAdapter supplies its own dummy view to show as the tab content. It
* listens to changes in tabs, and takes care of switch to the correct paged in
* the ViewPager whenever the selected tab changes.
* <p>
* Copied from the FragmentTabsPager sample in the support library.
*
* @author Jimmy Shih
*/
public class TabsAdapter extends FragmentPagerAdapter
implements TabHost.OnTabChangeListener, ViewPager.OnPageChangeListener {
private final Context context;
private final TabHost tabHost;
private final ViewPager viewPager;
private final ArrayList<TabInfo> tabInfos = new ArrayList<TabInfo>();
/**
* An object to hold a tab's info.
*
* @author Jimmy Shih
*/
private static final class TabInfo {
private final Class<?> clss;
private final Bundle bundle;
public TabInfo(Class<?> clss, Bundle bunlde) {
this.clss = clss;
this.bundle = bunlde;
}
}
/**
* A dummy {@link TabContentFactory} that creates an empty view to satisfy the
* {@link TabHost} API.
*
* @author Jimmy Shih
*/
private static class DummyTabFactory implements TabHost.TabContentFactory {
private final Context context;
public DummyTabFactory(Context context) {
this.context = context;
}
@Override
public View createTabContent(String tag) {
View view = new View(context);
view.setMinimumWidth(0);
view.setMinimumHeight(0);
return view;
}
}
public TabsAdapter(FragmentActivity activity, TabHost tabHost, ViewPager viewPager) {
super(activity.getSupportFragmentManager());
this.context = activity;
this.tabHost = tabHost;
this.viewPager = viewPager;
this.tabHost.setOnTabChangedListener(this);
this.viewPager.setAdapter(this);
this.viewPager.setOnPageChangeListener(this);
}
public void addTab(TabHost.TabSpec tabSpec, Class<?> clss, Bundle bundle) {
tabSpec.setContent(new DummyTabFactory(context));
TabInfo info = new TabInfo(clss, bundle);
tabInfos.add(info);
tabHost.addTab(tabSpec);
notifyDataSetChanged();
}
@Override
public int getCount() {
return tabInfos.size();
}
@Override
public Fragment getItem(int position) {
TabInfo info = tabInfos.get(position);
return Fragment.instantiate(context, info.clss.getName(), info.bundle);
}
@Override
public void onTabChanged(String tabId) {
int position = tabHost.getCurrentTab();
viewPager.setCurrentItem(position);
}
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {}
@Override
public void onPageSelected(int position) {
/*
* Unfortunately when TabHost changes the current tab, it kindly also takes
* care of putting focus on it when not in touch mode. The jerk. This hack
* tries to prevent this from pulling focus out of our ViewPager.
*/
TabWidget tabWidget = tabHost.getTabWidget();
int oldFocusability = tabWidget.getDescendantFocusability();
tabWidget.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
tabHost.setCurrentTab(position);
tabWidget.setDescendantFocusability(oldFocusability);
}
@Override
public void onPageScrollStateChanged(int state) {}
}
@@ -0,0 +1,165 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks;
import com.google.android.apps.mytracks.services.ITrackRecordingService;
import com.google.android.apps.mytracks.services.TrackRecordingServiceConnection;
import com.google.android.apps.mytracks.util.StringUtils;
import com.google.android.apps.mytracks.util.UnitConversions;
import com.google.android.maps.mytracks.R;
import android.app.Activity;
import android.os.Handler;
import android.os.RemoteException;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.ImageButton;
import android.widget.TextView;
/**
* Track controller for record, pause, resume, and stop.
*
* @author Jimmy Shih
*/
public class TrackController {
private static final String TAG = TrackController.class.getSimpleName();
// 1 second in milliseconds
private static final long ONE_SECOND = (long) UnitConversions.S_TO_MS;
private final Activity activity;
private final TrackRecordingServiceConnection trackRecordingServiceConnection;
private final Handler handler;
private final View containerView;
private final TextView statusTextView;
private final TextView totalTimeTextView;
private final ImageButton recordImageButton;
private final ImageButton stopImageButton;
private final boolean alwaysShow;
private boolean isRecording;
private boolean isPaused;
private long totalTime = 0;
// the timestamp for the toal time
private long totalTimeTimestamp = 0;
private boolean isResumed = false;
// A runnable to update the total time.
private final Runnable updateTotalTimeRunnable = new Runnable() {
public void run() {
if (isResumed && isRecording && !isPaused) {
totalTimeTextView.setText(StringUtils.formatElapsedTimeWithHour(
System.currentTimeMillis() - totalTimeTimestamp + totalTime));
handler.postDelayed(this, ONE_SECOND);
}
}
};
public TrackController(Activity activity,
TrackRecordingServiceConnection trackRecordingServiceConnection, boolean alwaysShow,
OnClickListener recordListener, OnClickListener stopListener) {
this.activity = activity;
this.trackRecordingServiceConnection = trackRecordingServiceConnection;
this.alwaysShow = alwaysShow;
handler = new Handler();
containerView = (View) activity.findViewById(R.id.track_controler_container);
statusTextView = (TextView) activity.findViewById(R.id.track_controller_status);
totalTimeTextView = (TextView) activity.findViewById(R.id.track_controller_total_time);
recordImageButton = (ImageButton) activity.findViewById(R.id.track_controller_record);
recordImageButton.setOnClickListener(recordListener);
stopImageButton = (ImageButton) activity.findViewById(R.id.track_controller_stop);
stopImageButton.setOnClickListener(stopListener);
}
public void update(boolean recording, boolean paused) {
if (!isResumed) {
return;
}
isRecording = recording;
isPaused = paused;
boolean visible = alwaysShow || isRecording;
containerView.setVisibility(visible ? View.VISIBLE : View.GONE);
if (!visible) {
handler.removeCallbacks(updateTotalTimeRunnable);
return;
}
recordImageButton.setImageResource(
isRecording && !isPaused ? R.drawable.button_pause : R.drawable.button_record);
recordImageButton.setContentDescription(activity.getString(
isRecording && !isPaused ? R.string.image_pause : R.string.image_record));
stopImageButton.setImageResource(
isRecording ? R.drawable.button_stop : R.drawable.ic_button_stop_disabled);
stopImageButton.setEnabled(isRecording);
statusTextView.setVisibility(isRecording ? View.VISIBLE : View.INVISIBLE);
if (isRecording) {
statusTextView.setTextColor(activity.getResources()
.getColor(isPaused ? android.R.color.white : R.color.recording_text));
statusTextView.setText(isPaused ? R.string.generic_paused : R.string.generic_recording);
}
handler.removeCallbacks(updateTotalTimeRunnable);
totalTimeTextView.setVisibility(isRecording ? View.VISIBLE : View.INVISIBLE);
if (isRecording) {
totalTime = getTotalTime();
totalTimeTextView.setText(StringUtils.formatElapsedTimeWithHour(totalTime));
if (!isPaused) {
totalTimeTimestamp = System.currentTimeMillis();
handler.postDelayed(updateTotalTimeRunnable, ONE_SECOND);
}
}
}
public void onResume(boolean recording, boolean paused) {
isResumed = true;
update(recording, paused);
}
public void onPause() {
isResumed = false;
handler.removeCallbacks(updateTotalTimeRunnable);
}
public void hide() {
containerView.setVisibility(View.GONE);
}
public void show() {
containerView.setVisibility(View.VISIBLE);
}
/**
* Gets the total time for the current recording track.
*/
private long getTotalTime() {
ITrackRecordingService trackRecordingService = trackRecordingServiceConnection
.getServiceIfBound();
try {
return trackRecordingService != null ? trackRecordingService.getTotalTime() : 0L;
} catch (RemoteException e) {
Log.e(TAG, "exception", e);
return 0L;
}
}
}
@@ -0,0 +1,643 @@
/*
* 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;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.TrackDataHub;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.content.Waypoint.WaypointType;
import com.google.android.apps.mytracks.content.WaypointCreationRequest;
import com.google.android.apps.mytracks.fragments.ChartFragment;
import com.google.android.apps.mytracks.fragments.ChooseActivityTypeDialogFragment;
import com.google.android.apps.mytracks.fragments.ChooseActivityTypeDialogFragment.ChooseActivityTypeCaller;
import com.google.android.apps.mytracks.fragments.ExportDialogFragment;
import com.google.android.apps.mytracks.fragments.ExportDialogFragment.ExportCaller;
import com.google.android.apps.mytracks.fragments.ExportDialogFragment.ExportType;
import com.google.android.apps.mytracks.fragments.FrequencyDialogFragment;
import com.google.android.apps.mytracks.fragments.MapLayerDialogFragment;
import com.google.android.apps.mytracks.fragments.MyTracksMapFragment;
import com.google.android.apps.mytracks.fragments.PlayMultipleDialogFragment;
import com.google.android.apps.mytracks.fragments.PlayMultipleDialogFragment.PlayMultipleCaller;
import com.google.android.apps.mytracks.fragments.StatsFragment;
import com.google.android.apps.mytracks.io.file.TrackFileFormat;
import com.google.android.apps.mytracks.io.file.exporter.SaveActivity;
import com.google.android.apps.mytracks.services.TrackRecordingServiceConnection;
import com.google.android.apps.mytracks.settings.SettingsActivity;
import com.google.android.apps.mytracks.util.AnalyticsUtils;
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import com.google.android.apps.mytracks.util.CalorieUtils;
import com.google.android.apps.mytracks.util.CalorieUtils.ActivityType;
import com.google.android.apps.mytracks.util.FileUtils;
import com.google.android.apps.mytracks.util.IntentUtils;
import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.android.apps.mytracks.util.TrackIconUtils;
import com.google.android.apps.mytracks.util.TrackRecordingServiceConnectionUtils;
import com.google.android.apps.mytracks.util.TrackUtils;
import com.google.android.maps.mytracks.R;
import android.accounts.Account;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.Parcelable;
import android.provider.MediaStore;
import android.support.v4.app.TaskStackBuilder;
import android.support.v4.view.ViewPager;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.TabHost;
import android.widget.TabHost.TabSpec;
import android.widget.Toast;
import java.io.File;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* An activity to show the track detail.
*
* @author Leif Hendrik Wilden
* @author Rodrigo Damazio
*/
public class TrackDetailActivity extends AbstractSendToGoogleActivity
implements ChooseActivityTypeCaller, ExportCaller, PlayMultipleCaller {
public static final String EXTRA_TRACK_ID = "track_id";
public static final String EXTRA_MARKER_ID = "marker_id";
private static final String CURRENT_TAB_TAG_KEY = "current_tab_tag_key";
private static final String PHOTO_URI_KEY = "photo_uri_key";
private static final String HAS_PHOTO_KEY = "has_photo_key";
private static final String JPEG_EXTENSION = "jpeg";
// The following are set in onCreate
private boolean hasCamera;
private Uri photoUri;
private boolean hasPhoto;
private MyTracksProviderUtils myTracksProviderUtils;
private SharedPreferences sharedPreferences;
private TrackRecordingServiceConnection trackRecordingServiceConnection;
private TrackDataHub trackDataHub;
private TabHost tabHost;
private ViewPager viewPager;
private TabsAdapter tabsAdapter;
private TrackController trackController;
// From intent
private long trackId;
private long markerId;
// Preferences
private long recordingTrackId = PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
private boolean recordingTrackPaused = PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT;
private String sensorType = PreferencesUtils.SENSOR_TYPE_DEFAULT;
private MenuItem insertMarkerMenuItem;
private MenuItem insertPhotoMenuItem;
private MenuItem playMenuItem;
private MenuItem shareMenuItem;
private MenuItem exportMenuItem;
private MenuItem voiceFrequencyMenuItem;
private MenuItem splitFrequencyMenuItem;
private MenuItem sensorStateMenuItem;
private final Runnable bindChangedCallback = new Runnable() {
@Override
public void run() {
// After binding changes (is available), update the total time in
// trackController.
runOnUiThread(new Runnable() {
@Override
public void run() {
trackController.update(trackId == recordingTrackId, recordingTrackPaused);
if (hasPhoto && photoUri != null) {
hasPhoto = false;
WaypointCreationRequest waypointCreationRequest = new WaypointCreationRequest(
WaypointType.WAYPOINT, false, null, null, null, null, photoUri.toString());
long id = TrackRecordingServiceConnectionUtils.addMarker(
TrackDetailActivity.this, trackRecordingServiceConnection, waypointCreationRequest);
if (id != -1L) {
FileUtils.updateMediaScanner(TrackDetailActivity.this, photoUri);
}
}
}
});
}
};
/*
* Note that sharedPreferenceChangeListener cannot be an anonymous inner
* class. Anonymous inner class will get garbage collected.
*/
private final OnSharedPreferenceChangeListener
sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
// Note that key can be null
if (key == null || key.equals(
PreferencesUtils.getKey(TrackDetailActivity.this, R.string.recording_track_id_key))) {
recordingTrackId = PreferencesUtils.getLong(
TrackDetailActivity.this, R.string.recording_track_id_key);
}
if (key == null || key.equals(PreferencesUtils.getKey(
TrackDetailActivity.this, R.string.recording_track_paused_key))) {
recordingTrackPaused = PreferencesUtils.getBoolean(TrackDetailActivity.this,
R.string.recording_track_paused_key,
PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT);
}
if (key == null || key.equals(
PreferencesUtils.getKey(TrackDetailActivity.this, R.string.sensor_type_key))) {
sensorType = PreferencesUtils.getString(TrackDetailActivity.this,
R.string.sensor_type_key, PreferencesUtils.SENSOR_TYPE_DEFAULT);
}
if (key != null) {
runOnUiThread(new Runnable() {
@Override
public void run() {
ApiAdapterFactory.getApiAdapter().invalidMenu(TrackDetailActivity.this);
boolean isRecording = trackId == recordingTrackId;
trackController.update(isRecording, recordingTrackPaused);
}
});
}
}
};
private final OnClickListener recordListener = new OnClickListener() {
@Override
public void onClick(View v) {
if (recordingTrackPaused) {
// Paused -> Resume
AnalyticsUtils.sendPageViews(TrackDetailActivity.this, AnalyticsUtils.ACTION_RESUME_TRACK);
updateMenuItems(true, false);
TrackRecordingServiceConnectionUtils.resumeTrack(trackRecordingServiceConnection);
trackController.update(true, false);
} else {
// Recording -> Paused
AnalyticsUtils.sendPageViews(TrackDetailActivity.this, AnalyticsUtils.ACTION_PAUSE_TRACK);
updateMenuItems(true, true);
TrackRecordingServiceConnectionUtils.pauseTrack(trackRecordingServiceConnection);
trackController.update(true, true);
}
}
};
private final OnClickListener stopListener = new OnClickListener() {
@Override
public void onClick(View v) {
AnalyticsUtils.sendPageViews(TrackDetailActivity.this, AnalyticsUtils.ACTION_STOP_RECORDING);
updateMenuItems(false, true);
TrackRecordingServiceConnectionUtils.stopRecording(
TrackDetailActivity.this, trackRecordingServiceConnection, true);
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
hasCamera = getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA);
photoUri = savedInstanceState != null ? (Uri) savedInstanceState.getParcelable(PHOTO_URI_KEY)
: null;
hasPhoto = savedInstanceState != null ? savedInstanceState.getBoolean(HAS_PHOTO_KEY, false)
: false;
myTracksProviderUtils = MyTracksProviderUtils.Factory.get(this);
handleIntent(getIntent());
sharedPreferences = getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
trackRecordingServiceConnection = new TrackRecordingServiceConnection(
this, bindChangedCallback);
trackDataHub = TrackDataHub.newInstance(this);
tabHost = (TabHost) findViewById(android.R.id.tabhost);
tabHost.setup();
viewPager = (ViewPager) findViewById(R.id.pager);
tabsAdapter = new TabsAdapter(this, tabHost, viewPager);
TabSpec mapTabSpec = tabHost.newTabSpec(MyTracksMapFragment.MAP_FRAGMENT_TAG).setIndicator(
getString(R.string.track_detail_map_tab),
getResources().getDrawable(R.drawable.ic_tab_map));
tabsAdapter.addTab(mapTabSpec, MyTracksMapFragment.class, null);
TabSpec chartTabSpec = tabHost.newTabSpec(ChartFragment.CHART_FRAGMENT_TAG).setIndicator(
getString(R.string.track_detail_chart_tab),
getResources().getDrawable(R.drawable.ic_tab_chart));
tabsAdapter.addTab(chartTabSpec, ChartFragment.class, null);
TabSpec statsTabSpec = tabHost.newTabSpec(StatsFragment.STATS_FRAGMENT_TAG).setIndicator(
getString(R.string.track_detail_stats_tab),
getResources().getDrawable(R.drawable.ic_tab_stats));
tabsAdapter.addTab(statsTabSpec, StatsFragment.class, null);
if (savedInstanceState != null) {
tabHost.setCurrentTabByTag(savedInstanceState.getString(CURRENT_TAB_TAG_KEY));
}
// Set the background after all three tabs are added
ApiAdapterFactory.getApiAdapter().setTabBackground(tabHost.getTabWidget());
trackController = new TrackController(
this, trackRecordingServiceConnection, false, recordListener, stopListener);
}
@Override
protected void onStart() {
super.onStart();
sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
sharedPreferenceChangeListener.onSharedPreferenceChanged(null, null);
TrackRecordingServiceConnectionUtils.startConnection(this, trackRecordingServiceConnection);
trackDataHub.start();
AnalyticsUtils.sendPageViews(this, AnalyticsUtils.PAGE_TRACK_DETAIL);
}
@Override
protected void onResume() {
super.onResume();
trackDataHub.loadTrack(trackId);
// Update UI
ApiAdapterFactory.getApiAdapter().invalidMenu(this);
boolean isRecording = trackId == recordingTrackId;
trackController.onResume(isRecording, recordingTrackPaused);
}
@Override
protected void onPause() {
super.onPause();
trackController.onPause();
}
@Override
protected void onStop() {
super.onStop();
sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
trackRecordingServiceConnection.unbind();
trackDataHub.stop();
AnalyticsUtils.dispatch();
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString(CURRENT_TAB_TAG_KEY, tabHost.getCurrentTabTag());
if (photoUri != null) {
outState.putParcelable(PHOTO_URI_KEY, photoUri);
}
outState.putBoolean(HAS_PHOTO_KEY, hasPhoto);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == CAMERA_REQUEST_CODE) {
if (resultCode == RESULT_CANCELED) {
Toast.makeText(this, R.string.marker_add_canceled, Toast.LENGTH_LONG).show();
}
hasPhoto = resultCode == RESULT_OK;
} else {
super.onActivityResult(requestCode, resultCode, data);
}
}
@Override
protected int getLayoutResId() {
return R.layout.track_detail;
}
@Override
protected boolean hideTitle() {
return true;
}
@Override
protected void onHomeSelected() {
/*
* According to
* http://developer.android.com/training/implementing-navigation
* /ancestral.html, we should use NavUtils.shouldUpRecreateTask instead of
* always creating a new back stack. However, NavUtils.shouldUpRecreateTask
* seems to always return false.
*/
TaskStackBuilder.create(this).addParentStack(TrackDetailActivity.class).startActivities();
finish();
}
@Override
public void onNewIntent(Intent intent) {
setIntent(intent);
handleIntent(intent);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.track_detail, menu);
Track track = myTracksProviderUtils.getTrack(trackId);
boolean isSharedWithMe = track != null ? track.isSharedWithMe() : true;
menu.findItem(R.id.track_detail_edit).setVisible(!isSharedWithMe);
menu.findItem(R.id.track_detail_help_feedback).setTitle(
ApiAdapterFactory.getApiAdapter().isGoogleFeedbackAvailable() ? R.string.menu_help_feedback
: R.string.menu_help);
insertMarkerMenuItem = menu.findItem(R.id.track_detail_insert_marker);
insertPhotoMenuItem = menu.findItem(R.id.track_detail_insert_photo);
playMenuItem = menu.findItem(R.id.track_detail_play);
shareMenuItem = menu.findItem(R.id.track_detail_share);
shareMenuItem.setEnabled(!isSharedWithMe);
shareMenuItem.setVisible(!isSharedWithMe);
exportMenuItem = menu.findItem(R.id.track_detail_export);
voiceFrequencyMenuItem = menu.findItem(R.id.track_detail_voice_frequency);
splitFrequencyMenuItem = menu.findItem(R.id.track_detail_split_frequency);
sensorStateMenuItem = menu.findItem(R.id.track_detail_sensor_state);
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
updateMenuItems(trackId == recordingTrackId, recordingTrackPaused);
return super.onPrepareOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
Intent intent;
switch (item.getItemId()) {
case R.id.track_detail_insert_marker:
AnalyticsUtils.sendPageViews(this, AnalyticsUtils.ACTION_INSERT_MARKER);
intent = IntentUtils.newIntent(this, MarkerEditActivity.class)
.putExtra(MarkerEditActivity.EXTRA_TRACK_ID, trackId);
startActivity(intent);
return true;
case R.id.track_detail_insert_photo:
if (!FileUtils.isExternalStorageWriteable()) {
Toast.makeText(this, R.string.external_storage_not_writable, Toast.LENGTH_LONG).show();
return false;
}
File dir = FileUtils.getPhotoDir(trackId);
FileUtils.ensureDirectoryExists(dir);
String fileName = SimpleDateFormat.getDateTimeInstance().format(new Date());
File file = new File(dir, FileUtils.buildUniqueFileName(dir, fileName, JPEG_EXTENSION));
photoUri = Uri.fromFile(file);
intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE).putExtra(
MediaStore.EXTRA_OUTPUT, photoUri);
startActivityForResult(intent, CAMERA_REQUEST_CODE);
return true;
case R.id.track_detail_play:
playTracks(new long[] {trackId});
return true;
case R.id.track_detail_share:
shareTrack(trackId);
return true;
case R.id.track_detail_markers:
intent = IntentUtils.newIntent(this, MarkerListActivity.class)
.putExtra(MarkerListActivity.EXTRA_TRACK_ID, trackId);
startActivity(intent);
return true;
case R.id.track_detail_play_multiple:
PlayMultipleDialogFragment.newInstance(trackId)
.show(getSupportFragmentManager(), PlayMultipleDialogFragment.PLAY_MULTIPLE_DIALOG_TAG);
return true;
case R.id.track_detail_voice_frequency:
FrequencyDialogFragment.newInstance(R.string.voice_frequency_key,
PreferencesUtils.VOICE_FREQUENCY_DEFAULT, R.string.menu_voice_frequency)
.show(getSupportFragmentManager(), FrequencyDialogFragment.FREQUENCY_DIALOG_TAG);
return true;
case R.id.track_detail_split_frequency:
FrequencyDialogFragment.newInstance(R.string.split_frequency_key,
PreferencesUtils.SPLIT_FREQUENCY_DEFAULT, R.string.menu_split_frequency)
.show(getSupportFragmentManager(), FrequencyDialogFragment.FREQUENCY_DIALOG_TAG);
return true;
case R.id.track_detail_export:
Track track = myTracksProviderUtils.getTrack(trackId);
boolean hideDrive = track != null ? track.isSharedWithMe() : true;
ExportDialogFragment.newInstance(hideDrive)
.show(getSupportFragmentManager(), ExportDialogFragment.EXPORT_DIALOG_TAG);
return true;
case R.id.track_detail_edit:
intent = IntentUtils.newIntent(this, TrackEditActivity.class)
.putExtra(TrackEditActivity.EXTRA_TRACK_ID, trackId);
startActivity(intent);
return true;
case R.id.track_detail_delete:
deleteTracks(new long[] { trackId });
return true;
case R.id.track_detail_sensor_state:
intent = IntentUtils.newIntent(this, SensorStateActivity.class);
startActivity(intent);
return true;
case R.id.track_detail_settings:
intent = IntentUtils.newIntent(this, SettingsActivity.class);
startActivity(intent);
return true;
case R.id.track_detail_help_feedback:
intent = IntentUtils.newIntent(this, HelpActivity.class);
startActivity(intent);
return true;
default:
return super.onOptionsItemSelected(item);
}
}
@Override
public boolean onTrackballEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
if (trackId == recordingTrackId && !recordingTrackPaused) {
TrackRecordingServiceConnectionUtils.addMarker(
this, trackRecordingServiceConnection, WaypointCreationRequest.DEFAULT_WAYPOINT);
return true;
}
}
return super.onTrackballEvent(event);
}
@Override
public void onExportDone(
ExportType exportType, TrackFileFormat trackFileFormat, Account account) {
if (exportType == ExportType.EXTERNAL_STORAGE) {
AnalyticsUtils.sendPageViews(
this, AnalyticsUtils.ACTION_EXPORT_PREFIX + trackFileFormat.getExtension());
Intent intent = IntentUtils.newIntent(this, SaveActivity.class)
.putExtra(SaveActivity.EXTRA_TRACK_IDS, new long[] { trackId })
.putExtra(SaveActivity.EXTRA_TRACK_FILE_FORMAT, (Parcelable) trackFileFormat);
startActivity(intent);
} else {
exportTrackToGoogle(trackId, exportType, account);
}
}
@Override
protected TrackRecordingServiceConnection getTrackRecordingServiceConnection() {
return trackRecordingServiceConnection;
}
@Override
protected void onDeleted() {
runOnUiThread(new Runnable() {
@Override
public void run() {
finish();
}
});
}
/**
* Gets the {@link TrackDataHub}.
*/
public TrackDataHub getTrackDataHub() {
return trackDataHub;
}
/**
* Gets the track id.
*/
public long getTrackId() {
return trackId;
}
/**
* Gets the marker id.
*/
public long getMarkerId() {
return markerId;
}
/**
* Handles the data in the intent.
*/
private void handleIntent(Intent intent) {
trackId = intent.getLongExtra(EXTRA_TRACK_ID, -1L);
markerId = intent.getLongExtra(EXTRA_MARKER_ID, -1L);
if (markerId != -1L) {
// Use the trackId from the marker
Waypoint waypoint = myTracksProviderUtils.getWaypoint(markerId);
if (waypoint == null) {
finish();
return;
}
trackId = waypoint.getTrackId();
}
if (trackId == -1L) {
finish();
return;
}
Track track = myTracksProviderUtils.getTrack(trackId);
if (track == null) {
// Use the last track if markerId is not set
if (markerId == -1L) {
track = myTracksProviderUtils.getLastTrack();
if (track != null) {
trackId = track.getId();
return;
}
}
finish();
return;
}
}
/**
* Updates the menu items.
*
* @param isRecording true if recording
*/
private void updateMenuItems(boolean isRecording, boolean isPaused) {
if (insertMarkerMenuItem != null) {
insertMarkerMenuItem.setVisible(isRecording && !isPaused);
}
if (insertPhotoMenuItem != null) {
insertPhotoMenuItem.setVisible(hasCamera && isRecording && !isPaused);
}
if (playMenuItem != null) {
playMenuItem.setVisible(!isRecording);
}
if (shareMenuItem != null && shareMenuItem.isEnabled()) {
shareMenuItem.setVisible(!isRecording);
}
if (exportMenuItem != null) {
exportMenuItem.setVisible(!isRecording);
}
if (voiceFrequencyMenuItem != null) {
voiceFrequencyMenuItem.setVisible(isRecording);
}
if (splitFrequencyMenuItem != null) {
splitFrequencyMenuItem.setVisible(isRecording);
}
if (sensorStateMenuItem != null) {
sensorStateMenuItem.setVisible(!PreferencesUtils.SENSOR_TYPE_DEFAULT.equals(sensorType));
}
String title;
if (isRecording) {
title = getString(isPaused ? R.string.generic_paused : R.string.generic_recording);
} else {
Track track = myTracksProviderUtils.getTrack(trackId);
title = track != null ? track.getName() : "";
}
setTitle(title);
}
public void chooseActivityType(String category) {
ChooseActivityTypeDialogFragment.newInstance(category).show(getSupportFragmentManager(),
ChooseActivityTypeDialogFragment.CHOOSE_ACTIVITY_TYPE_DIALOG_TAG);
}
@Override
public void onChooseActivityTypeDone(String iconValue, boolean newWeight) {
Track track = myTracksProviderUtils.getTrack(trackId);
String category = getString(TrackIconUtils.getIconActivityType(iconValue));
TrackUtils.updateTrack(
this, track, null, category, null, myTracksProviderUtils, trackRecordingServiceConnection, newWeight);
// Add toast if cannot calculate calorie
if (CalorieUtils.getActivityType(this, category) == ActivityType.INVALID) {
String message = getString(R.string.stats_calorie_no_calculation, category);
Toast.makeText(this, message, Toast.LENGTH_LONG).show();
}
}
@Override
public void onPlayMultipleDone(long[] trackIds) {
playTracks(trackIds);
}
/**
* Shows the map layer dialog.
*/
public void showMapLayerDialog() {
new MapLayerDialogFragment().show(
getSupportFragmentManager(), MapLayerDialogFragment.MAP_LAYER_DIALOG_TAG);
}
}
@@ -0,0 +1,316 @@
/*
* 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;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.fragments.ChooseAccountDialogFragment;
import com.google.android.apps.mytracks.fragments.ChooseAccountDialogFragment.ChooseAccountCaller;
import com.google.android.apps.mytracks.fragments.ChooseActivityTypeDialogFragment;
import com.google.android.apps.mytracks.fragments.ChooseActivityTypeDialogFragment.ChooseActivityTypeCaller;
import com.google.android.apps.mytracks.fragments.EnableSyncDialogFragment;
import com.google.android.apps.mytracks.fragments.EnableSyncDialogFragment.EnableSyncCaller;
import com.google.android.apps.mytracks.io.sendtogoogle.SendToGoogleUtils;
import com.google.android.apps.mytracks.io.sync.SyncUtils;
import com.google.android.apps.mytracks.services.TrackRecordingServiceConnection;
import com.google.android.apps.mytracks.services.tasks.CheckPermissionAsyncTask;
import com.google.android.apps.mytracks.services.tasks.CheckPermissionAsyncTask.CheckPermissionCaller;
import com.google.android.apps.mytracks.util.EulaUtils;
import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.android.apps.mytracks.util.TrackIconUtils;
import com.google.android.apps.mytracks.util.TrackRecordingServiceConnectionUtils;
import com.google.android.apps.mytracks.util.TrackUtils;
import com.google.android.maps.mytracks.R;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.AutoCompleteTextView;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Spinner;
import android.widget.Toast;
/**
* An activity that let's the user see and edit the user editable track meta
* data such as track name, activity type, and track description.
*
* @author Leif Hendrik Wilden
*/
public class TrackEditActivity extends AbstractMyTracksActivity implements ChooseActivityTypeCaller,
EnableSyncCaller, ChooseAccountCaller, CheckPermissionCaller {
public static final String EXTRA_TRACK_ID = "track_id";
public static final String EXTRA_NEW_TRACK = "new_track";
private static final String TAG = TrackEditActivity.class.getSimpleName();
private static final String ICON_VALUE_KEY = "icon_value_key";
private static final int DRIVE_REQUEST_CODE = 0;
private Long trackId;
private TrackRecordingServiceConnection trackRecordingServiceConnection;
private MyTracksProviderUtils myTracksProviderUtils;
private Track track;
private String iconValue;
private EditText name;
private AutoCompleteTextView activityType;
private Spinner activityTypeIcon;
private EditText description;
private boolean newWeight = false;
private CheckPermissionAsyncTask syncDriveAsyncTask;
@Override
protected void onCreate(Bundle bundle) {
super.onCreate(bundle);
Object retained = getLastCustomNonConfigurationInstance();
if (retained instanceof CheckPermissionAsyncTask) {
syncDriveAsyncTask = (CheckPermissionAsyncTask) retained;
syncDriveAsyncTask.setActivity(this);
}
trackRecordingServiceConnection = new TrackRecordingServiceConnection(this, null);
trackId = getIntent().getLongExtra(EXTRA_TRACK_ID, -1L);
if (trackId == -1L) {
Log.e(TAG, "invalid trackId");
finish();
return;
}
myTracksProviderUtils = MyTracksProviderUtils.Factory.get(this);
track = myTracksProviderUtils.getTrack(trackId);
if (track == null) {
Log.e(TAG, "No track for " + trackId);
finish();
return;
}
name = (EditText) findViewById(R.id.track_edit_name);
name.setText(track.getName());
activityType = (AutoCompleteTextView) findViewById(R.id.track_edit_activity_type);
activityType.setText(track.getCategory());
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(
this, R.array.activity_types, android.R.layout.simple_dropdown_item_1line);
activityType.setAdapter(adapter);
activityType.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
setActivityTypeIcon(TrackIconUtils.getIconValue(
TrackEditActivity.this, (String) activityType.getAdapter().getItem(position)));
}
});
activityType.setOnFocusChangeListener(new View.OnFocusChangeListener() {
@Override
public void onFocusChange(View v, boolean hasFocus) {
if (!hasFocus) {
setActivityTypeIcon(TrackIconUtils.getIconValue(
TrackEditActivity.this, activityType.getText().toString()));
}
}
});
iconValue = null;
if (bundle != null) {
iconValue = bundle.getString(ICON_VALUE_KEY);
}
if (iconValue == null) {
iconValue = track.getIcon();
}
activityTypeIcon = (Spinner) findViewById(R.id.track_edit_activity_type_icon);
activityTypeIcon.setAdapter(TrackIconUtils.getIconSpinnerAdapter(this, iconValue));
activityTypeIcon.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_UP) {
ChooseActivityTypeDialogFragment.newInstance(activityType.getText().toString()).show(
getSupportFragmentManager(),
ChooseActivityTypeDialogFragment.CHOOSE_ACTIVITY_TYPE_DIALOG_TAG);
}
return true;
}
});
activityTypeIcon.setOnKeyListener(new View.OnKeyListener() {
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
ChooseActivityTypeDialogFragment.newInstance(activityType.getText().toString()).show(
getSupportFragmentManager(),
ChooseActivityTypeDialogFragment.CHOOSE_ACTIVITY_TYPE_DIALOG_TAG);
}
return true;
}
});
description = (EditText) findViewById(R.id.track_edit_description);
description.setText(track.getDescription());
Button save = (Button) findViewById(R.id.track_edit_save);
save.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
TrackUtils.updateTrack(TrackEditActivity.this, track, name.getText().toString(),
activityType.getText().toString(), description.getText().toString(),
myTracksProviderUtils, trackRecordingServiceConnection, newWeight);
if (EulaUtils.showEnableSync(TrackEditActivity.this)) {
EulaUtils.setShowEnableSync(TrackEditActivity.this);
if (PreferencesUtils.getBoolean(TrackEditActivity.this, R.string.drive_sync_key,
PreferencesUtils.DRIVE_SYNC_DEFAULT)) {
finish();
} else {
new EnableSyncDialogFragment().show(
getSupportFragmentManager(), EnableSyncDialogFragment.ENABLE_SYNC_DIALOG_TAG);
}
} else {
finish();
}
}
});
Button cancel = (Button) findViewById(R.id.track_edit_cancel);
if (getIntent().getBooleanExtra(EXTRA_NEW_TRACK, false)) {
setTitle(R.string.track_edit_new_track_title);
cancel.setVisibility(View.GONE);
} else {
setTitle(R.string.menu_edit);
cancel.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
finish();
}
});
cancel.setVisibility(View.VISIBLE);
}
}
@Override
public Object onRetainCustomNonConfigurationInstance() {
if (syncDriveAsyncTask != null) {
syncDriveAsyncTask.setActivity(null);
}
return syncDriveAsyncTask;
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == DRIVE_REQUEST_CODE) {
SendToGoogleUtils.cancelNotification(this, SendToGoogleUtils.DRIVE_NOTIFICATION_ID);
if (resultCode == Activity.RESULT_OK) {
onDrivePermissionSuccess();
} else {
onPermissionFailure();
}
} else {
super.onActivityResult(requestCode, resultCode, data);
}
}
@Override
protected void onStart() {
super.onStart();
TrackRecordingServiceConnectionUtils.startConnection(this, trackRecordingServiceConnection);
}
@Override
protected void onStop() {
super.onStop();
trackRecordingServiceConnection.unbind();
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString(ICON_VALUE_KEY, iconValue);
}
@Override
protected int getLayoutResId() {
return R.layout.track_edit;
}
private void setActivityTypeIcon(String value) {
iconValue = value;
TrackIconUtils.setIconSpinner(activityTypeIcon, value);
}
@Override
public void onChooseActivityTypeDone(String value, boolean hasNewWeight) {
if (!newWeight) {
newWeight = hasNewWeight;
}
setActivityTypeIcon(value);
activityType.setText(getString(TrackIconUtils.getIconActivityType(value)));
}
@Override
public void onEnableSyncDone(boolean enable) {
if (enable) {
new ChooseAccountDialogFragment().show(
getSupportFragmentManager(), ChooseAccountDialogFragment.CHOOSE_ACCOUNT_DIALOG_TAG);
} else {
finish();
}
}
@Override
public void onChooseAccountDone(String account) {
PreferencesUtils.setString(this, R.string.google_account_key, account);
if (PreferencesUtils.GOOGLE_ACCOUNT_DEFAULT.equals(account)) {
finish();
} else {
syncDriveAsyncTask = new CheckPermissionAsyncTask(this, account, SendToGoogleUtils.DRIVE_SCOPE);
syncDriveAsyncTask.execute();
}
}
@Override
public void onCheckPermissionDone(String scope, boolean success, Intent userRecoverableIntent) {
syncDriveAsyncTask = null;
if (success) {
onDrivePermissionSuccess();
} else {
if (userRecoverableIntent != null) {
startActivityForResult(userRecoverableIntent, DRIVE_REQUEST_CODE);
} else {
onPermissionFailure();
}
}
}
private void onDrivePermissionSuccess() {
SyncUtils.enableSync(this);
finish();
}
private void onPermissionFailure() {
Toast.makeText(this, R.string.send_google_no_account_permission, Toast.LENGTH_LONG).show();
finish();
}
}
@@ -0,0 +1,814 @@
/*
* 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;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.TracksColumns;
import com.google.android.apps.mytracks.fragments.ChooseAccountDialogFragment;
import com.google.android.apps.mytracks.fragments.ChooseAccountDialogFragment.ChooseAccountCaller;
import com.google.android.apps.mytracks.fragments.ConfirmSyncDialogFragment;
import com.google.android.apps.mytracks.fragments.ConfirmSyncDialogFragment.ConfirmSyncCaller;
import com.google.android.apps.mytracks.fragments.EulaDialogFragment;
import com.google.android.apps.mytracks.fragments.EulaDialogFragment.EulaCaller;
import com.google.android.apps.mytracks.fragments.FileTypeDialogFragment;
import com.google.android.apps.mytracks.fragments.FileTypeDialogFragment.FileTypeCaller;
import com.google.android.apps.mytracks.fragments.PlayMultipleDialogFragment;
import com.google.android.apps.mytracks.fragments.PlayMultipleDialogFragment.PlayMultipleCaller;
import com.google.android.apps.mytracks.io.file.TrackFileFormat;
import com.google.android.apps.mytracks.io.file.exporter.SaveActivity;
import com.google.android.apps.mytracks.io.file.importer.ImportActivity;
import com.google.android.apps.mytracks.io.sync.SyncUtils;
import com.google.android.apps.mytracks.services.ITrackRecordingService;
import com.google.android.apps.mytracks.services.MyTracksLocationManager;
import com.google.android.apps.mytracks.services.TrackRecordingServiceConnection;
import com.google.android.apps.mytracks.settings.SettingsActivity;
import com.google.android.apps.mytracks.util.AnalyticsUtils;
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import com.google.android.apps.mytracks.util.EulaUtils;
import com.google.android.apps.mytracks.util.GoogleLocationUtils;
import com.google.android.apps.mytracks.util.IntentUtils;
import com.google.android.apps.mytracks.util.ListItemUtils;
import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.android.apps.mytracks.util.StringUtils;
import com.google.android.apps.mytracks.util.TrackIconUtils;
import com.google.android.apps.mytracks.util.TrackRecordingServiceConnectionUtils;
import com.google.android.apps.mytracks.util.TrackUtils;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GooglePlayServicesUtil;
import com.google.android.maps.mytracks.BuildConfig;
import com.google.android.maps.mytracks.R;
import android.accounts.Account;
import android.app.Dialog;
import android.app.SearchManager;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.database.Cursor;
import android.os.Bundle;
import android.os.Looper;
import android.os.Parcelable;
import android.os.RemoteException;
import android.support.v4.app.Fragment;
import android.support.v4.app.LoaderManager.LoaderCallbacks;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.util.Log;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.AdapterView;
import android.widget.AdapterView.AdapterContextMenuInfo;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ListView;
import android.widget.Toast;
import java.util.Locale;
/**
* An activity displaying a list of tracks.
*
* @author Leif Hendrik Wilden
*/
public class TrackListActivity extends AbstractSendToGoogleActivity
implements EulaCaller, FileTypeCaller, PlayMultipleCaller, ChooseAccountCaller, ConfirmSyncCaller {
private static final String TAG = TrackListActivity.class.getSimpleName();
private static final String[] PROJECTION = new String[] { TracksColumns._ID, TracksColumns.NAME,
TracksColumns.DESCRIPTION, TracksColumns.CATEGORY, TracksColumns.STARTTIME,
TracksColumns.TOTALDISTANCE, TracksColumns.TOTALTIME, TracksColumns.ICON,
TracksColumns.SHAREDWITHME, TracksColumns.SHAREDOWNER };
// Callback when the trackRecordingServiceConnection binding changes.
private final Runnable bindChangedCallback = new Runnable() {
@Override
public void run() {
/*
* After binding changes (e.g., becomes available), update the total time
* in trackController.
*/
runOnUiThread(new Runnable() {
@Override
public void run() {
trackController.update(recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT,
recordingTrackPaused);
}
});
if (!startGps && !startNewRecording) {
return;
}
ITrackRecordingService service = trackRecordingServiceConnection.getServiceIfBound();
if (service == null) {
Log.d(TAG, "service not available to start gps or a new recording");
return;
}
if (startNewRecording) {
startGps = false;
try {
long trackId = service.startNewTrack();
startNewRecording = false;
Intent intent = IntentUtils.newIntent(TrackListActivity.this, TrackDetailActivity.class)
.putExtra(TrackDetailActivity.EXTRA_TRACK_ID, trackId);
startActivity(intent);
Toast.makeText(
TrackListActivity.this, R.string.track_list_record_success, Toast.LENGTH_SHORT)
.show();
} catch (RemoteException e) {
Toast.makeText(
TrackListActivity.this, R.string.track_list_record_error, Toast.LENGTH_LONG).show();
Log.e(TAG, "Unable to start a new recording.", e);
}
}
if (startGps) {
try {
service.startGps();
startGps = false;
} catch (RemoteException e) {
Toast.makeText(TrackListActivity.this, R.string.gps_starting_error, Toast.LENGTH_LONG)
.show();
Log.e(TAG, "Unable to start gps");
}
}
}
};
/*
* Note that sharedPreferenceChangeListenr cannot be an anonymous inner class.
* Anonymous inner class will get garbage collected.
*/
private final OnSharedPreferenceChangeListener
sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
if (key == null || key.equals(
PreferencesUtils.getKey(TrackListActivity.this, R.string.stats_units_key))) {
metricUnits = PreferencesUtils.isMetricUnits(TrackListActivity.this);
}
if (key == null || key.equals(
PreferencesUtils.getKey(TrackListActivity.this, R.string.recording_track_id_key))) {
recordingTrackId = PreferencesUtils.getLong(
TrackListActivity.this, R.string.recording_track_id_key);
if (key != null && recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT) {
trackRecordingServiceConnection.startAndBind();
}
}
if (key == null || key.equals(PreferencesUtils.getKey(
TrackListActivity.this, R.string.recording_track_paused_key))) {
recordingTrackPaused = PreferencesUtils.getBoolean(TrackListActivity.this,
R.string.recording_track_paused_key,
PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT);
}
if (key == null || key.equals(
PreferencesUtils.getKey(TrackListActivity.this, R.string.drive_sync_key))) {
driveSync = PreferencesUtils.getBoolean(TrackListActivity.this, R.string.drive_sync_key,
PreferencesUtils.DRIVE_SYNC_DEFAULT);
}
if (key != null) {
runOnUiThread(new Runnable() {
@Override
public void run() {
ApiAdapterFactory.getApiAdapter().invalidMenu(TrackListActivity.this);
getSupportLoaderManager().restartLoader(0, null, loaderCallbacks);
boolean isRecording = recordingTrackId
!= PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
trackController.update(isRecording, recordingTrackPaused);
}
});
}
}
};
// Callback when an item is selected in the contextual action mode
private final ContextualActionModeCallback
contextualActionModeCallback = new ContextualActionModeCallback() {
@Override
public void onPrepare(Menu menu, int[] positions, long[] ids, boolean showSelectAll) {
boolean isRecording = recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
boolean isSingleSelection = ids.length == 1;
boolean isSingleSelectionShareWithMe;
if (isSingleSelection) {
Track track = myTracksProviderUtils.getTrack(ids[0]);
isSingleSelectionShareWithMe = track.isSharedWithMe();
} else {
isSingleSelectionShareWithMe = false;
}
// Not recording
menu.findItem(R.id.list_context_menu_play).setVisible(!isRecording);
// Not recording, one item, not sharedWithMe item
menu.findItem(R.id.list_context_menu_share)
.setVisible(!isRecording && isSingleSelection && !isSingleSelectionShareWithMe);
// Always disable
menu.findItem(R.id.list_context_menu_show_on_map).setVisible(false);
// One item, not sharedWithMe item
menu.findItem(R.id.list_context_menu_edit)
.setVisible(isSingleSelection && !isSingleSelectionShareWithMe);
// delete is always enabled
menu.findItem(R.id.list_context_menu_select_all).setVisible(showSelectAll);
}
@Override
public boolean onClick(int itemId, int[] positions, long[] ids) {
return handleContextItem(itemId, ids);
};
};
private final OnClickListener recordListener = new OnClickListener() {
public void onClick(View v) {
if (recordingTrackId == PreferencesUtils.RECORDING_TRACK_ID_DEFAULT) {
// Not recording -> Recording
AnalyticsUtils.sendPageViews(TrackListActivity.this, AnalyticsUtils.ACTION_RECORD_TRACK);
updateMenuItems(false, true);
startRecording();
} else {
if (recordingTrackPaused) {
// Paused -> Resume
AnalyticsUtils.sendPageViews(TrackListActivity.this, AnalyticsUtils.ACTION_RESUME_TRACK);
updateMenuItems(false, true);
TrackRecordingServiceConnectionUtils.resumeTrack(trackRecordingServiceConnection);
trackController.update(true, false);
} else {
// Recording -> Paused
AnalyticsUtils.sendPageViews(TrackListActivity.this, AnalyticsUtils.ACTION_PAUSE_TRACK);
updateMenuItems(false, true);
TrackRecordingServiceConnectionUtils.pauseTrack(trackRecordingServiceConnection);
trackController.update(true, true);
}
}
}
};
private final OnClickListener stopListener = new OnClickListener() {
@Override
public void onClick(View v) {
AnalyticsUtils.sendPageViews(TrackListActivity.this, AnalyticsUtils.ACTION_STOP_RECORDING);
updateMenuItems(false, false);
TrackRecordingServiceConnectionUtils.stopRecording(
TrackListActivity.this, trackRecordingServiceConnection, true);
}
};
private final LoaderCallbacks<Cursor> loaderCallbacks = new LoaderCallbacks<Cursor>() {
@Override
public Loader<Cursor> onCreateLoader(int arg0, Bundle arg1) {
return new CursorLoader(TrackListActivity.this, TracksColumns.CONTENT_URI, PROJECTION, null,
null, TrackUtils.TRACK_SORT_ORDER);
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
sectionResourceCursorAdapter.swapCursor(cursor);
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
sectionResourceCursorAdapter.swapCursor(null);
}
};
// The following are set in onCreate
private MyTracksProviderUtils myTracksProviderUtils;
private SharedPreferences sharedPreferences;
private TrackRecordingServiceConnection trackRecordingServiceConnection;
private TrackController trackController;
private ListView listView;
private SectionResourceCursorAdapter sectionResourceCursorAdapter;
// Preferences
private boolean metricUnits = true;
private long recordingTrackId = PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
private boolean recordingTrackPaused = PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT;
private boolean driveSync = PreferencesUtils.DRIVE_SYNC_DEFAULT;
// Menu items
private MenuItem searchMenuItem;
private MenuItem startGpsMenuItem;
private MenuItem playMultipleItem;
private MenuItem syncNowMenuItem;
private MenuItem aggregatedStatisticsMenuItem;
private MenuItem exportAllMenuItem;
private MenuItem importAllMenuItem;
private MenuItem deleteAllMenuItem;
private boolean startGps = false; // true to start gps
private boolean startNewRecording = false; // true to start a new recording
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (BuildConfig.DEBUG) {
ApiAdapterFactory.getApiAdapter().enableStrictMode();
}
myTracksProviderUtils = MyTracksProviderUtils.Factory.get(this);
sharedPreferences = getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
trackRecordingServiceConnection = new TrackRecordingServiceConnection(
this, bindChangedCallback);
trackController = new TrackController(
this, trackRecordingServiceConnection, true, recordListener, stopListener);
setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
// Show trackController when search dialog is dismissed
SearchManager searchManager = (SearchManager) getSystemService(SEARCH_SERVICE);
searchManager.setOnDismissListener(new SearchManager.OnDismissListener() {
@Override
public void onDismiss() {
trackController.show();
}
});
listView = (ListView) findViewById(R.id.track_list);
listView.setEmptyView(findViewById(R.id.track_list_empty_view));
listView.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Intent newIntent = IntentUtils.newIntent(TrackListActivity.this, TrackDetailActivity.class)
.putExtra(TrackDetailActivity.EXTRA_TRACK_ID, id);
startActivity(newIntent);
}
});
sectionResourceCursorAdapter = new SectionResourceCursorAdapter(
this, R.layout.list_item, null, 0) {
@Override
public void bindView(View view, Context context, Cursor cursor) {
int idIndex = cursor.getColumnIndex(TracksColumns._ID);
int iconIndex = cursor.getColumnIndex(TracksColumns.ICON);
int nameIndex = cursor.getColumnIndex(TracksColumns.NAME);
int sharedOwnerIndex = cursor.getColumnIndex(TracksColumns.SHAREDOWNER);
int totalTimeIndex = cursor.getColumnIndexOrThrow(TracksColumns.TOTALTIME);
int totalDistanceIndex = cursor.getColumnIndexOrThrow(TracksColumns.TOTALDISTANCE);
int startTimeIndex = cursor.getColumnIndexOrThrow(TracksColumns.STARTTIME);
int categoryIndex = cursor.getColumnIndex(TracksColumns.CATEGORY);
int descriptionIndex = cursor.getColumnIndex(TracksColumns.DESCRIPTION);
long trackId = cursor.getLong(idIndex);
boolean isRecording = trackId == recordingTrackId;
String icon = cursor.getString(iconIndex);
int iconId = TrackIconUtils.getIconDrawable(icon);
String name = cursor.getString(nameIndex);
String sharedOwner = cursor.getString(sharedOwnerIndex);
String totalTime = StringUtils.formatElapsedTime(cursor.getLong(totalTimeIndex));
String totalDistance = StringUtils.formatDistance(
TrackListActivity.this, cursor.getDouble(totalDistanceIndex), metricUnits);
int markerCount = myTracksProviderUtils.getWaypointCount(trackId);
long startTime = cursor.getLong(startTimeIndex);
String category = icon != null && !icon.equals("") ? null : cursor.getString(categoryIndex);
String description = cursor.getString(descriptionIndex);
ListItemUtils.setListItem(TrackListActivity.this, view, isRecording, recordingTrackPaused,
iconId, R.string.image_track, name, sharedOwner, totalTime, totalDistance, markerCount,
startTime, true, category, description, null);
}
};
listView.setAdapter(sectionResourceCursorAdapter);
ApiAdapterFactory.getApiAdapter()
.configureListViewContextualMenu(this, listView, contextualActionModeCallback);
getSupportLoaderManager().initLoader(0, null, loaderCallbacks);
showStartupDialogs();
}
@Override
protected void onStart() {
super.onStart();
// Register shared preferences listener
sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
// Update shared preferences
sharedPreferenceChangeListener.onSharedPreferenceChanged(null, null);
// Update track recording service connection
TrackRecordingServiceConnectionUtils.startConnection(this, trackRecordingServiceConnection);
AnalyticsUtils.sendPageViews(this, AnalyticsUtils.PAGE_TRACK_LIST);
}
@Override
protected void onResume() {
super.onResume();
// Update UI
ApiAdapterFactory.getApiAdapter().invalidMenu(this);
getSupportLoaderManager().restartLoader(0, null, loaderCallbacks);
boolean isRecording = recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
trackController.onResume(isRecording, recordingTrackPaused);
}
@Override
protected void onPause() {
super.onPause();
// Update UI
trackController.onPause();
}
@Override
protected void onStop() {
super.onStop();
// Unregister shared preferences listener
sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
trackRecordingServiceConnection.unbind();
AnalyticsUtils.dispatch();
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == GOOGLE_PLAY_SERVICES_REQUEST_CODE) {
checkGooglePlayServices();
} else {
super.onActivityResult(requestCode, resultCode, data);
}
}
@Override
protected int getLayoutResId() {
return R.layout.track_list;
}
@Override
protected boolean configureActionBarHomeAsUp() {
return false;
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.track_list, menu);
menu.findItem(R.id.track_list_help_feedback).setTitle(
ApiAdapterFactory.getApiAdapter().isGoogleFeedbackAvailable() ? R.string.menu_help_feedback
: R.string.menu_help);
searchMenuItem = menu.findItem(R.id.track_list_search);
ApiAdapterFactory.getApiAdapter().configureSearchWidget(this, searchMenuItem, trackController);
startGpsMenuItem = menu.findItem(R.id.track_list_start_gps);
playMultipleItem = menu.findItem(R.id.track_list_play_mutliple);
syncNowMenuItem = menu.findItem(R.id.track_list_sync_now);
aggregatedStatisticsMenuItem = menu.findItem(R.id.track_list_aggregated_statistics);
exportAllMenuItem = menu.findItem(R.id.track_list_export_all);
importAllMenuItem = menu.findItem(R.id.track_list_import_all);
deleteAllMenuItem = menu.findItem(R.id.track_list_delete_all);
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
boolean isGpsStarted = TrackRecordingServiceConnectionUtils.isRecordingServiceRunning(this);
boolean isRecording = recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
updateMenuItems(isGpsStarted, isRecording);
return super.onPrepareOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
Intent intent;
switch (item.getItemId()) {
case R.id.track_list_search:
return ApiAdapterFactory.getApiAdapter().handleSearchMenuSelection(this);
case R.id.track_list_start_gps:
MyTracksLocationManager myTracksLocationManager = new MyTracksLocationManager(
this, Looper.myLooper(), false);
if (!myTracksLocationManager.isGpsProviderEnabled()) {
intent = GoogleLocationUtils.newLocationSettingsIntent(TrackListActivity.this);
startActivity(intent);
} else {
startGps = !TrackRecordingServiceConnectionUtils.isRecordingServiceRunning(
this);
// Show toast
Toast toast = Toast.makeText(
this, startGps ? R.string.gps_starting : R.string.gps_stopping, Toast.LENGTH_SHORT);
toast.setGravity(Gravity.CENTER, 0, 0);
toast.show();
// Invoke trackRecordingService
if (startGps) {
trackRecordingServiceConnection.startAndBind();
bindChangedCallback.run();
} else {
ITrackRecordingService trackRecordingService = trackRecordingServiceConnection
.getServiceIfBound();
if (trackRecordingService != null) {
try {
trackRecordingService.stopGps();
} catch (RemoteException e) {
Log.e(TAG, "Unable to stop gps.", e);
}
}
trackRecordingServiceConnection.unbindAndStop();
}
// Update menu after starting or stopping gps
ApiAdapterFactory.getApiAdapter().invalidMenu(this);
}
myTracksLocationManager.close();
return true;
case R.id.track_list_play_mutliple:
PlayMultipleDialogFragment.newInstance(-1L)
.show(getSupportFragmentManager(), PlayMultipleDialogFragment.PLAY_MULTIPLE_DIALOG_TAG);
return true;
case R.id.track_list_sync_now:
if (driveSync) {
SyncUtils.syncNow(this);
} else {
new ChooseAccountDialogFragment().show(
getSupportFragmentManager(), ChooseAccountDialogFragment.CHOOSE_ACCOUNT_DIALOG_TAG);
}
return true;
case R.id.track_list_aggregated_statistics:
intent = IntentUtils.newIntent(this, AggregatedStatsActivity.class);
startActivity(intent);
return true;
case R.id.track_list_export_all:
FileTypeDialogFragment.newInstance(R.id.track_list_export_all,
R.string.export_all_title, R.string.export_all_option, 4)
.show(getSupportFragmentManager(), FileTypeDialogFragment.FILE_TYPE_DIALOG_TAG);
return true;
case R.id.track_list_import_all:
FileTypeDialogFragment.newInstance(R.id.track_list_import_all,
R.string.import_selection_title, R.string.import_selection_option, 2)
.show(getSupportFragmentManager(), FileTypeDialogFragment.FILE_TYPE_DIALOG_TAG);
return true;
case R.id.track_list_delete_all:
deleteTracks(new long[] {-1L});
return true;
case R.id.track_list_settings:
intent = IntentUtils.newIntent(this, SettingsActivity.class);
startActivity(intent);
return true;
case R.id.track_list_help_feedback:
intent = IntentUtils.newIntent(this, HelpActivity.class);
startActivity(intent);
return true;
default:
return super.onOptionsItemSelected(item);
}
}
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
super.onCreateContextMenu(menu, v, menuInfo);
getMenuInflater().inflate(R.menu.list_context_menu, menu);
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
contextualActionModeCallback.onPrepare(
menu, new int[] { info.position }, new long[] { info.id }, false);
}
@Override
public boolean onContextItemSelected(MenuItem item) {
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
if (handleContextItem(item.getItemId(), new long[] { info.id })) {
return true;
}
return super.onContextItemSelected(item);
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_SEARCH && searchMenuItem != null) {
if (ApiAdapterFactory.getApiAdapter().handleSearchKey(searchMenuItem)) {
return true;
}
}
return super.onKeyUp(keyCode, event);
}
@Override
public boolean onSearchRequested() {
// Hide trackController when search dialog is shown
trackController.hide();
return super.onSearchRequested();
};
@Override
protected TrackRecordingServiceConnection getTrackRecordingServiceConnection() {
return trackRecordingServiceConnection;
}
@Override
protected void onDeleted() {
// Do nothing
}
@Override
public void onFileTypeDone(int menuId, TrackFileFormat trackFileFormat) {
Intent intent;
switch (menuId) {
case R.id.track_list_export_all:
AnalyticsUtils.sendPageViews(
this, AnalyticsUtils.ACTION_EXPORT_ALL_PREFIX + trackFileFormat.getExtension());
intent = IntentUtils.newIntent(this, SaveActivity.class)
.putExtra(SaveActivity.EXTRA_TRACK_IDS, new long[] {-1L})
.putExtra(SaveActivity.EXTRA_TRACK_FILE_FORMAT, (Parcelable) trackFileFormat);
startActivity(intent);
break;
case R.id.track_list_import_all:
AnalyticsUtils.sendPageViews(
this, AnalyticsUtils.ACTION_IMPORT_ALL_PREFIX + trackFileFormat.getExtension());
intent = IntentUtils.newIntent(this, ImportActivity.class)
.putExtra(ImportActivity.EXTRA_IMPORT_ALL, true)
.putExtra(ImportActivity.EXTRA_TRACK_FILE_FORMAT, (Parcelable) trackFileFormat);
startActivity(intent);
break;
default:
}
}
/**
* Shows start up dialogs.
*/
public void showStartupDialogs() {
if (!EulaUtils.hasAcceptedEula(this)) {
Fragment fragment = getSupportFragmentManager()
.findFragmentByTag(EulaDialogFragment.EULA_DIALOG_TAG);
if (fragment == null) {
EulaDialogFragment.newInstance(false)
.show(getSupportFragmentManager(), EulaDialogFragment.EULA_DIALOG_TAG);
}
} else {
// If stats_units_key is undefined, set it
if (PreferencesUtils.getString(this, R.string.stats_units_key, "").equals("")) {
String statsUnits = getString(
Locale.US.equals(Locale.getDefault()) ? R.string.stats_units_imperial
: R.string.stats_units_metric);
PreferencesUtils.setString(this, R.string.stats_units_key, statsUnits);
}
checkGooglePlayServices();
}
}
@Override
public void onEulaDone() {
if (EulaUtils.hasAcceptedEula(this)) {
showStartupDialogs();
return;
}
finish();
}
private void checkGooglePlayServices() {
int code = GooglePlayServicesUtil.isGooglePlayServicesAvailable(this);
if (code != ConnectionResult.SUCCESS) {
Dialog dialog = GooglePlayServicesUtil.getErrorDialog(
code, this, GOOGLE_PLAY_SERVICES_REQUEST_CODE, new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialogInterface) {
finish();
}
});
if (dialog != null) {
dialog.show();
return;
}
}
}
/**
* Updates the menu items.
*
* @param isGpsStarted true if gps is started
* @param isRecording true if recording
*/
private void updateMenuItems(boolean isGpsStarted, boolean isRecording) {
boolean hasTrack = listView != null && listView.getCount() != 0;
if (startGpsMenuItem != null) {
startGpsMenuItem.setVisible(!isRecording);
if (!isRecording) {
startGpsMenuItem.setTitle(isGpsStarted ? R.string.menu_stop_gps : R.string.menu_start_gps);
startGpsMenuItem.setIcon(
isGpsStarted ? R.drawable.ic_menu_stop_gps : R.drawable.ic_menu_start_gps);
TrackIconUtils.setMenuIconColor(startGpsMenuItem);
}
}
if (playMultipleItem != null) {
playMultipleItem.setVisible(hasTrack);
}
if (syncNowMenuItem != null) {
syncNowMenuItem.setTitle(driveSync ? R.string.menu_sync_now : R.string.menu_sync_drive);
}
if (aggregatedStatisticsMenuItem != null) {
aggregatedStatisticsMenuItem.setVisible(hasTrack);
}
if (exportAllMenuItem != null) {
exportAllMenuItem.setVisible(hasTrack && !isRecording);
}
if (importAllMenuItem != null) {
importAllMenuItem.setVisible(!isRecording);
}
if (deleteAllMenuItem != null) {
deleteAllMenuItem.setVisible(hasTrack && !isRecording);
}
}
/**
* Starts a new recording.
*/
private void startRecording() {
startNewRecording = true;
trackRecordingServiceConnection.startAndBind();
/*
* If the binding has happened, then invoke the callback to start a new
* recording. If the binding hasn't happened, then invoking the callback
* will have no effect. But when the binding occurs, the callback will get
* invoked.
*/
bindChangedCallback.run();
}
/**
* Handles a context item selection.
*
* @param itemId the menu item id
* @param trackIds the track ids
* @return true if handled.
*/
private boolean handleContextItem(int itemId, long[] trackIds) {
switch (itemId) {
case R.id.list_context_menu_play:
playTracks(trackIds);
return true;
case R.id.list_context_menu_share:
shareTrack(trackIds[0]);
return true;
case R.id.list_context_menu_edit:
Intent intent = IntentUtils.newIntent(this, TrackEditActivity.class)
.putExtra(TrackEditActivity.EXTRA_TRACK_ID, trackIds[0]);
startActivity(intent);
return true;
case R.id.list_context_menu_delete:
if (trackIds.length > 1 && trackIds.length == listView.getCount()) {
trackIds = new long[] {-1L};
}
deleteTracks(trackIds);
return true;
case R.id.list_context_menu_select_all:
int size = listView.getCount();
for (int i = 0; i < size; i++) {
listView.setItemChecked(i, true);
}
return false;
default:
return false;
}
}
@Override
public void onPlayMultipleDone(long[] trackIds) {
playTracks(trackIds);
}
@Override
public void onChooseAccountDone(String account) {
PreferencesUtils.setString(this, R.string.google_account_key, account);
if (PreferencesUtils.GOOGLE_ACCOUNT_DEFAULT.equals(account)) {
return;
} else {
new ConfirmSyncDialogFragment().show(
getSupportFragmentManager(), ConfirmSyncDialogFragment.CONFIRM_SYNC_DIALOG_TAG);
}
}
@Override
public void onConfirmSyncDone(boolean enable) {
if (enable) {
String googleAccount = PreferencesUtils.getString(
this, R.string.google_account_key, PreferencesUtils.GOOGLE_ACCOUNT_DEFAULT);
enableSync(new Account(googleAccount, Constants.ACCOUNT_TYPE));
}
}
}
@@ -0,0 +1,80 @@
/*
* Copyright 2011 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.content;
import com.google.android.apps.mytracks.Constants;
import android.content.ContentResolver;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.database.ContentObserver;
import android.net.Uri;
/**
* Data source on the phone.
*
* @author Rodrigo Damazio
*/
public class DataSource {
private final ContentResolver contentResolver;
private final SharedPreferences sharedPreferences;
public DataSource(Context context) {
contentResolver = context.getContentResolver();
sharedPreferences = context.getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
}
/**
* Registers a content observer.
*
* @param uri the uri
* @param observer the observer
*/
public void registerContentObserver(Uri uri, ContentObserver observer) {
contentResolver.registerContentObserver(uri, false, observer);
}
/**
* Unregisters a content observer.
*
* @param observer the observer
*/
public void unregisterContentObserver(ContentObserver observer) {
contentResolver.unregisterContentObserver(observer);
}
/**
* Registers a shared preference change listener.
*
* @param listener the listener
*/
public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
sharedPreferences.registerOnSharedPreferenceChangeListener(listener);
}
/**
* Unregisters a shared preference change listener.
*
* @param listener the listener
*/
public void unregisterOnSharedPreferenceChangeListener(
OnSharedPreferenceChangeListener listener) {
sharedPreferences.unregisterOnSharedPreferenceChangeListener(listener);
}
}
@@ -0,0 +1,48 @@
/*
* Copyright 2011 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.content;
/**
* Listener to be invoked when {@link DataSource} changes.
*
* @author Jimmy Shih
*/
public interface DataSourceListener {
/**
* Notifies when the tracks table is updated.
*/
public void notifyTracksTableUpdated();
/**
* Notifies when the waypoints table is updated.
*/
public void notifyWaypointsTableUpdated();
/**
* Notifies when the track points table is updated.
*/
public void notifyTrackPointsTableUpdated();
/**
* Notifies when a preference changes.
*
* @param key the preference key
*/
public void notifyPreferenceChanged(String key);
}
@@ -0,0 +1,231 @@
/*
* Copyright 2011 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.content;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.database.ContentObserver;
import android.os.Handler;
import android.util.Log;
import java.util.EnumSet;
import java.util.Set;
/**
* Data source manager. Creates observers/listeners and manages their
* registration with {@link DataSource}. The observers/listeners calls
* {@link DataSourceListener} when data changes.
*
* @author Rodrigo Damazio
*/
public class DataSourceManager {
private static final String TAG = DataSourceManager.class.getSimpleName();
/**
* Observer when the tracks table is updated.
*
* @author Jimmy Shih
*/
private class TracksTableObserver extends ContentObserver {
public TracksTableObserver() {
super(handler);
}
@Override
public void onChange(boolean selfChange) {
dataSourceListener.notifyTracksTableUpdated();
}
}
/**
* Observer when the waypoints table is updated.
*
* @author Jimmy Shih
*/
private class WaypointsTableObserver extends ContentObserver {
public WaypointsTableObserver() {
super(handler);
}
@Override
public void onChange(boolean selfChange) {
dataSourceListener.notifyWaypointsTableUpdated();
}
}
/**
* Observer when the track points table is updated.
*
* @author Jimmy Shih
*/
private class TrackPointsTableObserver extends ContentObserver {
public TrackPointsTableObserver() {
super(handler);
}
@Override
public void onChange(boolean selfChange) {
dataSourceListener.notifyTrackPointsTableUpdated();
}
}
/**
* Listener for preference changes.
*
* @author Jimmy Shih
*/
private class PreferenceListener implements OnSharedPreferenceChangeListener {
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
dataSourceListener.notifyPreferenceChanged(key);
}
}
private final DataSource dataSource;
private final DataSourceListener dataSourceListener;
// Registered listeners
private final Set<TrackDataType> registeredListeners = EnumSet.noneOf(TrackDataType.class);
private final Handler handler;
private final TracksTableObserver tracksTableObserver;
private final WaypointsTableObserver waypointsTableObserver;
private final TrackPointsTableObserver trackPointsTableObserver;
private final PreferenceListener preferenceListener;
public DataSourceManager(DataSource dataSource, DataSourceListener dataSourceListener) {
this.dataSource = dataSource;
this.dataSourceListener = dataSourceListener;
handler = new Handler();
tracksTableObserver = new TracksTableObserver();
waypointsTableObserver = new WaypointsTableObserver();
trackPointsTableObserver = new TrackPointsTableObserver();
preferenceListener = new PreferenceListener();
}
/**
* Updates listeners with data source.
*
* @param listeners the listeners
*/
public void updateListeners(EnumSet<TrackDataType> listeners) {
EnumSet<TrackDataType> neededListeners = EnumSet.copyOf(listeners);
/*
* Map SAMPLED_OUT_POINT_UPDATES to POINT_UPDATES since they correspond to
* the same internal listener
*/
if (neededListeners.contains(TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE)) {
neededListeners.remove(TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE);
neededListeners.add(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE);
}
Log.d(TAG, "Updating listeners " + neededListeners);
// Unnecessary = registered - needed
Set<TrackDataType> unnecessaryListeners = EnumSet.copyOf(registeredListeners);
unnecessaryListeners.removeAll(neededListeners);
// Missing = needed - registered
Set<TrackDataType> missingListeners = EnumSet.copyOf(neededListeners);
missingListeners.removeAll(registeredListeners);
// Remove unnecessary listeners
for (TrackDataType trackDataType : unnecessaryListeners) {
unregisterListener(trackDataType);
}
// Add missing listeners
for (TrackDataType trackDataType : missingListeners) {
registerListener(trackDataType);
}
// Update registered listeners
registeredListeners.clear();
registeredListeners.addAll(neededListeners);
}
/**
* Registers a listener with data source.
*
* @param trackDataType the listener data type
*/
private void registerListener(TrackDataType trackDataType) {
switch (trackDataType) {
case TRACKS_TABLE:
dataSource.registerContentObserver(TracksColumns.CONTENT_URI, tracksTableObserver);
break;
case WAYPOINTS_TABLE:
dataSource.registerContentObserver(WaypointsColumns.CONTENT_URI, waypointsTableObserver);
break;
case SAMPLED_IN_TRACK_POINTS_TABLE:
dataSource.registerContentObserver(
TrackPointsColumns.CONTENT_URI, trackPointsTableObserver);
break;
case SAMPLED_OUT_TRACK_POINTS_TABLE:
// Do nothing. SAMPLED_OUT_POINT_UPDATES is mapped to POINT_UPDATES.
break;
case PREFERENCE:
dataSource.registerOnSharedPreferenceChangeListener(preferenceListener);
break;
default:
break;
}
}
/**
* Unregisters a listener with data source.
*
* @param trackDataType listener data type
*/
private void unregisterListener(TrackDataType trackDataType) {
switch (trackDataType) {
case TRACKS_TABLE:
dataSource.unregisterContentObserver(tracksTableObserver);
break;
case WAYPOINTS_TABLE:
dataSource.unregisterContentObserver(waypointsTableObserver);
break;
case SAMPLED_IN_TRACK_POINTS_TABLE:
dataSource.unregisterContentObserver(trackPointsTableObserver);
break;
case SAMPLED_OUT_TRACK_POINTS_TABLE:
// Do nothing. SAMPLED_OUT_POINT_UPDATES is mapped to POINT_UPDATES.
break;
case PREFERENCE:
dataSource.unregisterOnSharedPreferenceChangeListener(preferenceListener);
break;
default:
break;
}
}
/**
* Unregisters all listeners with data source.
*/
public void unregisterAllListeners() {
for (TrackDataType trackDataType : TrackDataType.values()) {
unregisterListener(trackDataType);
}
}
}
@@ -0,0 +1,268 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.content;
import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.android.apps.mytracks.util.ChartURLGenerator;
import com.google.android.apps.mytracks.util.StringUtils;
import com.google.android.apps.mytracks.util.UnitConversions;
import com.google.android.maps.mytracks.R;
import com.google.common.annotations.VisibleForTesting;
import android.content.Context;
import java.util.Vector;
/**
* An implementation of {@link DescriptionGenerator} for My Tracks.
*
* @author Jimmy Shih
*/
public class DescriptionGeneratorImpl implements DescriptionGenerator {
private static final String HTML_LINE_BREAK = "<br>";
private static final String HTML_PARAGRAPH_SEPARATOR = "<p>";
private static final String TEXT_LINE_BREAK = "\n";
private static final String TEXT_PARAGRAPH_SEPARATOR = "\n\n";
private Context context;
public DescriptionGeneratorImpl(Context context) {
this.context = context;
}
@Override
public String generateTrackDescription(
Track track, Vector<Double> distances, Vector<Double> elevations, boolean html) {
String paragraphSeparator = html ? HTML_PARAGRAPH_SEPARATOR : TEXT_PARAGRAPH_SEPARATOR;
String lineBreak = html ? HTML_LINE_BREAK : TEXT_LINE_BREAK;
StringBuilder builder = new StringBuilder();
// Created by
String beginAnchor = html
? "<a href='http://" + context.getString(R.string.my_tracks_web_url) + "'>"
: "";
String endAnchor = html ? "</a>" : "";
builder.append(context.getString(R.string.send_google_by_my_tracks, beginAnchor, endAnchor));
builder.append(paragraphSeparator);
writeString(track.getName(), builder, R.string.generic_name_line, lineBreak);
writeString(track.getCategory(), builder, R.string.description_activity_type, lineBreak);
writeString(track.getDescription(), builder, R.string.generic_description_line, lineBreak);
builder.append(generateTripStatisticsDescription(track.getTripStatistics(), html));
// Elevation chart
if (html && distances != null && elevations != null) {
builder.append("<img border=\"0\" src=\""
+ ChartURLGenerator.getChartUrl(distances, elevations, track, context) + "\"/>");
builder.append(HTML_LINE_BREAK);
}
return builder.toString();
}
@Override
public String generateWaypointDescription(TripStatistics tripStatistics) {
return generateTripStatisticsDescription(tripStatistics, false);
}
/**
* Writes a string to a string builder.
*
* @param text the string
* @param builder the string builder
* @param resId the resource id containing one string placeholder
* @param lineBreak the line break
*/
private void writeString(String text, StringBuilder builder, int resId, String lineBreak) {
if (text == null || text.length() == 0) {
text = context.getString(R.string.value_unknown);
}
builder.append(context.getString(resId, text));
builder.append(lineBreak);
}
/**
* Generates a description for a {@link TripStatistics}.
*
* @param stats the trip statistics
* @param html true to use "<br>" for line break instead of "\n"
*/
private String generateTripStatisticsDescription(TripStatistics stats, boolean html) {
String lineBreak = html ? HTML_LINE_BREAK : TEXT_LINE_BREAK;
StringBuilder builder = new StringBuilder();
// Total distance
writeDistance(
stats.getTotalDistance(), builder, R.string.description_total_distance, lineBreak);
// Total time
writeTime(stats.getTotalTime(), builder, R.string.description_total_time, lineBreak);
// Moving time
writeTime(stats.getMovingTime(), builder, R.string.description_moving_time, lineBreak);
// Average speed
writeSpeed(stats.getAverageSpeed(), builder, R.string.description_average_speed, lineBreak);
// Average moving speed
writeSpeed(stats.getAverageMovingSpeed(), builder, R.string.description_average_moving_speed,
lineBreak);
// Max speed
writeSpeed(stats.getMaxSpeed(), builder, R.string.description_max_speed, lineBreak);
// Average pace
writePace(
stats.getAverageSpeed(), builder, R.string.description_average_pace_in_minute, lineBreak);
// Average moving pace
writePace(stats.getAverageMovingSpeed(), builder,
R.string.description_average_moving_pace_in_minute, lineBreak);
// Fastest pace
writePace(stats.getMaxSpeed(), builder, R.string.description_fastest_pace_in_minute, lineBreak);
// Max elevation
writeElevation(stats.getMaxElevation(), builder, R.string.description_max_elevation, lineBreak);
// Min elevation
writeElevation(stats.getMinElevation(), builder, R.string.description_min_elevation, lineBreak);
// Elevation gain
writeElevation(
stats.getTotalElevationGain(), builder, R.string.description_elevation_gain, lineBreak);
// Max grade
writeGrade(stats.getMaxGrade(), builder, R.string.description_max_grade, lineBreak);
// Min grade
writeGrade(stats.getMinGrade(), builder, R.string.description_min_grade, lineBreak);
// Recorded time
builder.append(
context.getString(R.string.description_recorded_time,
StringUtils.formatDateTime(context, stats.getStartTime())));
builder.append(lineBreak);
return builder.toString();
}
/**
* Writes distance.
*
* @param distance distance in meters
* @param builder StringBuilder to append distance
* @param resId resource id of distance string
* @param lineBreak line break string
*/
@VisibleForTesting
void writeDistance(double distance, StringBuilder builder, int resId, String lineBreak) {
double distanceInKm = distance * UnitConversions.M_TO_KM;
double distanceInMi = distanceInKm * UnitConversions.KM_TO_MI;
builder.append(context.getString(resId, distanceInKm, distanceInMi));
builder.append(lineBreak);
}
/**
* Writes time.
*
* @param time time in milliseconds.
* @param builder StringBuilder to append time
* @param resId resource id of time string
* @param lineBreak line break string
*/
@VisibleForTesting
void writeTime(long time, StringBuilder builder, int resId, String lineBreak) {
builder.append(context.getString(resId, StringUtils.formatElapsedTime(time)));
builder.append(lineBreak);
}
/**
* Writes speed.
*
* @param speed speed in meters per second
* @param builder StringBuilder to append speed
* @param resId resource id of speed string
* @param lineBreak line break string
*/
@VisibleForTesting
void writeSpeed(
double speed, StringBuilder builder, int resId, String lineBreak) {
double speedInKmHr = speed * UnitConversions.MS_TO_KMH;
double speedInMiHr = speedInKmHr * UnitConversions.KM_TO_MI;
builder.append(context.getString(resId, speedInKmHr, speedInMiHr));
builder.append(lineBreak);
}
/**
* Writes pace.
*
* @param speed speed in meters per second
* @param builder StringBuilder to append pace
* @param resId resource id of pace string
* @param lineBreak line break string
*/
@VisibleForTesting
void writePace(double speed, StringBuilder builder, int resId, String lineBreak) {
String[] paceInMetrics = StringUtils.getSpeedParts(context, speed, true, false);
String[] paceInImperial = StringUtils.getSpeedParts(context, speed, false, false);
if (paceInMetrics[0] == null) {
paceInMetrics[0] = context.getString(R.string.value_unknown);
}
if (paceInImperial[0] == null) {
paceInImperial[0] = context.getString(R.string.value_unknown);
}
builder.append(context.getString(resId, paceInMetrics[0], paceInImperial[0]));
builder.append(lineBreak);
}
/**
* Writes elevation.
*
* @param elevation elevation in meters
* @param builder StringBuilder to append elevation
* @param resId resource id of elevation string
* @param lineBreak line break string
*/
@VisibleForTesting
void writeElevation(
double elevation, StringBuilder builder, int resId, String lineBreak) {
long elevationInM = Math.round(elevation);
long elevationInFt = Math.round(elevation * UnitConversions.M_TO_FT);
builder.append(context.getString(resId, elevationInM, elevationInFt));
builder.append(lineBreak);
}
/**
* Writes grade.
*
* @param grade grade in fraction
* @param builder StringBuilder to append grade
* @param resId resource id grade string
* @param lineBreak line break string
*/
@VisibleForTesting
void writeGrade(double grade, StringBuilder builder, int resId, String lineBreak) {
long gradeInPercent = Double.isNaN(grade) || Double.isInfinite(grade) ? 0L
: Math.round(grade * 100);
builder.append(context.getString(resId, gradeInPercent));
builder.append(lineBreak);
}
}
@@ -0,0 +1,524 @@
/*
* 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.content;
import com.google.android.apps.mytracks.util.FileUtils;
import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.android.maps.mytracks.R;
import com.google.common.annotations.VisibleForTesting;
import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteOpenHelper;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.os.Binder;
import android.os.ParcelFileDescriptor;
import android.os.Process;
import android.text.TextUtils;
import android.util.Log;
import java.io.File;
import java.io.FileNotFoundException;
/**
* A {@link ContentProvider} that handles access to track points, tracks, and
* waypoints tables.
*
* @author Leif Hendrik Wilden
*/
public class MyTracksProvider extends ContentProvider {
private static final String TAG = MyTracksProvider.class.getSimpleName();
@VisibleForTesting
static final int DATABASE_VERSION = 22;
@VisibleForTesting
static final String DATABASE_NAME = "mytracks.db";
/**
* Database helper for creating and upgrading the database.
*/
@VisibleForTesting
static class DatabaseHelper extends SQLiteOpenHelper {
public DatabaseHelper(Context context) {
this(context, DATABASE_NAME);
}
@VisibleForTesting
public DatabaseHelper(Context context, String databaseName) {
super(context, databaseName, null, DATABASE_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(TrackPointsColumns.CREATE_TABLE);
db.execSQL(TracksColumns.CREATE_TABLE);
db.execSQL(WaypointsColumns.CREATE_TABLE);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
Log.w(TAG, "Upgrading database from version " + oldVersion + " to " + newVersion);
if (oldVersion < 17) {
Log.w(TAG, "Deleting all old data.");
db.execSQL("DROP TABLE IF EXISTS " + TrackPointsColumns.TABLE_NAME);
db.execSQL("DROP TABLE IF EXISTS " + TracksColumns.TABLE_NAME);
db.execSQL("DROP TABLE IF EXISTS " + WaypointsColumns.TABLE_NAME);
onCreate(db);
} else {
// Incremental upgrades. One if statement per DB version.
// Add track point SENSOR column
if (oldVersion <= 17) {
Log.w(TAG, "Upgrade DB: Adding track point sensor column.");
db.execSQL("ALTER TABLE " + TrackPointsColumns.TABLE_NAME + " ADD "
+ TrackPointsColumns.SENSOR + " BLOB");
}
// Add track TABLEID column
if (oldVersion <= 18) {
Log.w(TAG, "Upgrade DB: Adding track tableid column.");
db.execSQL("ALTER TABLE " + TracksColumns.TABLE_NAME + " ADD " + TracksColumns.TABLEID
+ " STRING");
}
// Add track ICON column
if (oldVersion <= 19) {
Log.w(TAG, "Upgrade DB: Adding track icon column.");
db.execSQL(
"ALTER TABLE " + TracksColumns.TABLE_NAME + " ADD " + TracksColumns.ICON + " STRING");
}
// Add track DRIVEID, MODIFIEDTIME, and SHAREDWITHME columns
if (oldVersion <= 20) {
Log.w(TAG, "Upgrade DB: Adding track driveid column.");
db.execSQL("ALTER TABLE " + TracksColumns.TABLE_NAME + " ADD " + TracksColumns.DRIVEID
+ " STRING");
Log.w(TAG, "Upgrade DB: Adding track modifiedtime column.");
db.execSQL("ALTER TABLE " + TracksColumns.TABLE_NAME + " ADD "
+ TracksColumns.MODIFIEDTIME + " INTEGER");
Log.w(TAG, "Upgrade DB: Adding track sharedwithme column.");
db.execSQL("ALTER TABLE " + TracksColumns.TABLE_NAME + " ADD "
+ TracksColumns.SHAREDWITHME + " INTEGER");
Log.w(TAG, "Upgrade DB: Adding track sharedowner column.");
db.execSQL("ALTER TABLE " + TracksColumns.TABLE_NAME + " ADD " + TracksColumns.SHAREDOWNER
+ " STRING");
}
// Add waypoint CALORIE and PHOTOURL columns. Add track CALORIE column.
if (oldVersion <= 21) {
Log.w(TAG, "Upgrade DB: Adding waypoint calorie column.");
db.execSQL("ALTER TABLE " + WaypointsColumns.TABLE_NAME + " ADD "
+ WaypointsColumns.CALORIE + " FLOAT");
Log.w(TAG, "Upgrade DB: Adding waypoint photo url column.");
db.execSQL("ALTER TABLE " + WaypointsColumns.TABLE_NAME + " ADD "
+ WaypointsColumns.PHOTOURL + " STRING");
Log.w(TAG, "Upgrade DB: Adding track calorie column.");
db.execSQL("ALTER TABLE " + TracksColumns.TABLE_NAME + " ADD " + TracksColumns.CALORIE
+ " FLOAT");
}
}
}
}
/**
* Types of url.
*
* @author Jimmy Shih
*/
@VisibleForTesting
enum UrlType {
TRACKPOINTS, TRACKPOINTS_ID, TRACKS, TRACKS_ID, WAYPOINTS, WAYPOINTS_ID
}
private final UriMatcher uriMatcher;
private SQLiteDatabase db;
public MyTracksProvider() {
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
uriMatcher.addURI(MyTracksProviderUtils.AUTHORITY, TrackPointsColumns.TABLE_NAME,
UrlType.TRACKPOINTS.ordinal());
uriMatcher.addURI(MyTracksProviderUtils.AUTHORITY, TrackPointsColumns.TABLE_NAME + "/#",
UrlType.TRACKPOINTS_ID.ordinal());
uriMatcher.addURI(
MyTracksProviderUtils.AUTHORITY, TracksColumns.TABLE_NAME, UrlType.TRACKS.ordinal());
uriMatcher.addURI(MyTracksProviderUtils.AUTHORITY, TracksColumns.TABLE_NAME + "/#",
UrlType.TRACKS_ID.ordinal());
uriMatcher.addURI(
MyTracksProviderUtils.AUTHORITY, WaypointsColumns.TABLE_NAME, UrlType.WAYPOINTS.ordinal());
uriMatcher.addURI(MyTracksProviderUtils.AUTHORITY, WaypointsColumns.TABLE_NAME + "/#",
UrlType.WAYPOINTS_ID.ordinal());
}
@Override
public boolean onCreate() {
return onCreate(getContext());
}
/**
* Helper method to make onCreate is testable.
*
* @param context context to creates database
* @return true means run successfully
*/
@VisibleForTesting
boolean onCreate(Context context) {
if (!canAccess()) {
return false;
}
DatabaseHelper databaseHelper = new DatabaseHelper(context);
try {
db = databaseHelper.getWritableDatabase();
} catch (SQLiteException e) {
Log.e(TAG, "Unable to open database for writing.", e);
}
return db != null;
}
@Override
public int delete(Uri url, String where, String[] selectionArgs) {
if (!canAccess()) {
return 0;
}
String table;
boolean shouldVacuum = false;
switch (getUrlType(url)) {
case TRACKPOINTS:
table = TrackPointsColumns.TABLE_NAME;
break;
case TRACKS:
table = TracksColumns.TABLE_NAME;
shouldVacuum = true;
break;
case WAYPOINTS:
table = WaypointsColumns.TABLE_NAME;
break;
default:
throw new IllegalArgumentException("Unknown URL " + url);
}
Log.w(MyTracksProvider.TAG, "Deleting table " + table);
int count;
try {
db.beginTransaction();
count = db.delete(table, where, selectionArgs);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
getContext().getContentResolver().notifyChange(url, null, false);
if (shouldVacuum) {
// If a potentially large amount of data was deleted, reclaim its space.
Log.i(TAG, "Vacuuming the database.");
db.execSQL("VACUUM");
}
return count;
}
@Override
public String getType(Uri url) {
if (!canAccess()) {
return null;
}
switch (getUrlType(url)) {
case TRACKPOINTS:
return TrackPointsColumns.CONTENT_TYPE;
case TRACKPOINTS_ID:
return TrackPointsColumns.CONTENT_ITEMTYPE;
case TRACKS:
return TracksColumns.CONTENT_TYPE;
case TRACKS_ID:
return TracksColumns.CONTENT_ITEMTYPE;
case WAYPOINTS:
return WaypointsColumns.CONTENT_TYPE;
case WAYPOINTS_ID:
return WaypointsColumns.CONTENT_ITEMTYPE;
default:
throw new IllegalArgumentException("Unknown URL " + url);
}
}
@Override
public Uri insert(Uri url, ContentValues initialValues) {
if (!canAccess()) {
return null;
}
if (initialValues == null) {
initialValues = new ContentValues();
}
Uri result = null;
try {
db.beginTransaction();
result = insertContentValues(url, getUrlType(url), initialValues);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
getContext().getContentResolver().notifyChange(url, null, false);
return result;
}
@Override
public int bulkInsert(Uri url, ContentValues[] valuesBulk) {
if (!canAccess()) {
return 0;
}
int numInserted = 0;
try {
// Use a transaction in order to make the insertions run as a single batch
db.beginTransaction();
UrlType urlType = getUrlType(url);
for (numInserted = 0; numInserted < valuesBulk.length; numInserted++) {
ContentValues contentValues = valuesBulk[numInserted];
if (contentValues == null) {
contentValues = new ContentValues();
}
insertContentValues(url, urlType, contentValues);
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
getContext().getContentResolver().notifyChange(url, null, false);
return numInserted;
}
@Override
public Cursor query(
Uri url, String[] projection, String selection, String[] selectionArgs, String sort) {
if (!canAccess()) {
return null;
}
SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
String sortOrder = null;
switch (getUrlType(url)) {
case TRACKPOINTS:
queryBuilder.setTables(TrackPointsColumns.TABLE_NAME);
sortOrder = sort != null ? sort : TrackPointsColumns.DEFAULT_SORT_ORDER;
break;
case TRACKPOINTS_ID:
queryBuilder.setTables(TrackPointsColumns.TABLE_NAME);
queryBuilder.appendWhere("_id=" + url.getPathSegments().get(1));
break;
case TRACKS:
queryBuilder.setTables(TracksColumns.TABLE_NAME);
sortOrder = sort != null ? sort : TracksColumns.DEFAULT_SORT_ORDER;
break;
case TRACKS_ID:
queryBuilder.setTables(TracksColumns.TABLE_NAME);
queryBuilder.appendWhere("_id=" + url.getPathSegments().get(1));
break;
case WAYPOINTS:
queryBuilder.setTables(WaypointsColumns.TABLE_NAME);
sortOrder = sort != null ? sort : WaypointsColumns.DEFAULT_SORT_ORDER;
break;
case WAYPOINTS_ID:
queryBuilder.setTables(WaypointsColumns.TABLE_NAME);
queryBuilder.appendWhere("_id=" + url.getPathSegments().get(1));
break;
default:
throw new IllegalArgumentException("Unknown url " + url);
}
Cursor cursor = queryBuilder.query(
db, projection, selection, selectionArgs, null, null, sortOrder);
cursor.setNotificationUri(getContext().getContentResolver(), url);
return cursor;
}
@Override
public int update(Uri url, ContentValues values, String where, String[] selectionArgs) {
if (!canAccess()) {
return 0;
}
String table;
String whereClause;
switch (getUrlType(url)) {
case TRACKPOINTS:
table = TrackPointsColumns.TABLE_NAME;
whereClause = where;
break;
case TRACKPOINTS_ID:
table = TrackPointsColumns.TABLE_NAME;
whereClause = TrackPointsColumns._ID + "=" + url.getPathSegments().get(1);
if (!TextUtils.isEmpty(where)) {
whereClause += " AND (" + where + ")";
}
break;
case TRACKS:
table = TracksColumns.TABLE_NAME;
whereClause = where;
break;
case TRACKS_ID:
table = TracksColumns.TABLE_NAME;
whereClause = TracksColumns._ID + "=" + url.getPathSegments().get(1);
if (!TextUtils.isEmpty(where)) {
whereClause += " AND (" + where + ")";
}
break;
case WAYPOINTS:
table = WaypointsColumns.TABLE_NAME;
whereClause = where;
break;
case WAYPOINTS_ID:
table = WaypointsColumns.TABLE_NAME;
whereClause = WaypointsColumns._ID + "=" + url.getPathSegments().get(1);
if (!TextUtils.isEmpty(where)) {
whereClause += " AND (" + where + ")";
}
break;
default:
throw new IllegalArgumentException("Unknown url " + url);
}
int count;
try {
db.beginTransaction();
count = db.update(table, values, whereClause, selectionArgs);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
getContext().getContentResolver().notifyChange(url, null, false);
return count;
}
@Override
public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
try {
File file = new File(uri.getPath());
if (file.exists()) {
return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
}
throw new FileNotFoundException(uri.getPath());
} finally {
File dir = new File(getContext().getCacheDir(), FileUtils.PLAY_TRACKS_DIR);
for (File file : dir.listFiles()) {
file.delete();
}
getContext().revokeUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
}
}
/**
* Returns true if the caller can access the content provider.
*/
private boolean canAccess() {
if (Binder.getCallingPid() == Process.myPid()) {
return true;
} else {
return PreferencesUtils.getBoolean(
getContext(), R.string.allow_access_key, PreferencesUtils.ALLOW_ACCESS_DEFAULT);
}
}
/**
* Gets the {@link UrlType} for a url.
*
* @param url the url
*/
private UrlType getUrlType(Uri url) {
return UrlType.values()[uriMatcher.match(url)];
}
/**
* Inserts a content based on the url type.
*
* @param url the content url
* @param urlType the url type
* @param contentValues the content values
*/
private Uri insertContentValues(Uri url, UrlType urlType, ContentValues contentValues) {
switch (urlType) {
case TRACKPOINTS:
return insertTrackPoint(url, contentValues);
case TRACKS:
return insertTrack(url, contentValues);
case WAYPOINTS:
return insertWaypoint(url, contentValues);
default:
throw new IllegalArgumentException("Unknown url " + url);
}
}
/**
* Inserts a track point.
*
* @param url the content url
* @param values the content values
*/
private Uri insertTrackPoint(Uri url, ContentValues values) {
boolean hasLatitude = values.containsKey(TrackPointsColumns.LATITUDE);
boolean hasLongitude = values.containsKey(TrackPointsColumns.LONGITUDE);
boolean hasTime = values.containsKey(TrackPointsColumns.TIME);
if (!hasLatitude || !hasLongitude || !hasTime) {
throw new IllegalArgumentException("Latitude, longitude, and time values are required.");
}
long rowId = db.insert(TrackPointsColumns.TABLE_NAME, TrackPointsColumns._ID, values);
if (rowId >= 0) {
return ContentUris.appendId(TrackPointsColumns.CONTENT_URI.buildUpon(), rowId).build();
}
throw new SQLiteException("Failed to insert a track point " + url);
}
/**
* Inserts a track.
*
* @param url the content url
* @param contentValues the content values
*/
private Uri insertTrack(Uri url, ContentValues contentValues) {
boolean hasStartTime = contentValues.containsKey(TracksColumns.STARTTIME);
boolean hasStartId = contentValues.containsKey(TracksColumns.STARTID);
if (!hasStartTime || !hasStartId) {
throw new IllegalArgumentException("Both start time and start id values are required.");
}
long rowId = db.insert(TracksColumns.TABLE_NAME, TracksColumns._ID, contentValues);
if (rowId >= 0) {
return ContentUris.appendId(TracksColumns.CONTENT_URI.buildUpon(), rowId).build();
}
throw new SQLException("Failed to insert a track " + url);
}
/**
* Inserts a waypoint.
*
* @param url the content url
* @param contentValues the content values
*/
private Uri insertWaypoint(Uri url, ContentValues contentValues) {
long rowId = db.insert(WaypointsColumns.TABLE_NAME, WaypointsColumns._ID, contentValues);
if (rowId >= 0) {
return ContentUris.appendId(WaypointsColumns.CONTENT_URI.buildUpon(), rowId).build();
}
throw new SQLException("Failed to insert a waypoint " + url);
}
}
@@ -0,0 +1,410 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.content;
import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.android.apps.mytracks.util.LocationUtils;
import com.google.android.apps.mytracks.util.UnitConversions;
import android.database.Cursor;
import android.location.Location;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.Locale;
import java.util.SortedSet;
import java.util.TreeSet;
/**
* Engine for searching for tracks and waypoints by text.
*
* @author Rodrigo Damazio
*/
public class SearchEngine {
/** WHERE query to get tracks by name. */
private static final String TRACK_SELECTION_QUERY =
TracksColumns.NAME + " LIKE ? OR " +
TracksColumns.DESCRIPTION + " LIKE ? OR " +
TracksColumns.CATEGORY + " LIKE ?";
/** WHERE query to get waypoints by name. */
private static final String WAYPOINT_SELECTION_QUERY =
WaypointsColumns.NAME + " LIKE ? OR " +
WaypointsColumns.DESCRIPTION + " LIKE ? OR " +
WaypointsColumns.CATEGORY + " LIKE ?";
/** Order of track results. */
private static final String TRACK_SELECTION_ORDER = TracksColumns._ID + " DESC LIMIT 1000";
/** Order of waypoint results. */
private static final String WAYPOINT_SELECTION_ORDER = WaypointsColumns._ID + " DESC";
/** How much we promote a match in the track category. */
private static final double TRACK_CATEGORY_PROMOTION = 2.0;
/** How much we promote a match in the track description. */
private static final double TRACK_DESCRIPTION_PROMOTION = 8.0;
/** How much we promote a match in the track name. */
private static final double TRACK_NAME_PROMOTION = 16.0;
/** How much we promote a waypoint result if it's in the currently-selected track. */
private static final double CURRENT_TRACK_WAYPOINT_PROMOTION = 2.0;
/** How much we promote a track result if it's the currently-selected track. */
private static final double CURRENT_TRACK_DEMOTION = 0.5;
/** Maximum number of waypoints which will be retrieved and scored. */
private static final int MAX_SCORED_WAYPOINTS = 100;
/** Oldest timestamp for which we rank based on time (2000-01-01 00:00:00.000) */
private static final long OLDEST_ALLOWED_TIMESTAMP = 946692000000L;
/**
* Description of a search query, along with all contextual data needed to execute it.
*/
public static class SearchQuery {
public SearchQuery(String textQuery, Location currentLocation, long currentTrackId,
long currentTimestamp) {
this.textQuery = textQuery.toLowerCase(Locale.getDefault());
this.currentLocation = currentLocation;
this.currentTrackId = currentTrackId;
this.currentTimestamp = currentTimestamp;
}
public final String textQuery;
public final Location currentLocation;
public final long currentTrackId;
public final long currentTimestamp;
}
/**
* Description of a search result which has been retrieved and scored.
*/
public static class ScoredResult {
ScoredResult(Track track, double score) {
this.track = track;
this.waypoint = null;
this.score = score;
}
ScoredResult(Waypoint waypoint, double score) {
this.track = null;
this.waypoint = waypoint;
this.score = score;
}
public final Track track;
public final Waypoint waypoint;
public final double score;
@Override
public String toString() {
return "ScoredResult ["
+ (track != null ? ("trackId=" + track.getId() + ", ") : "")
+ (waypoint != null ? ("wptId=" + waypoint.getId() + ", ") : "")
+ "score=" + score + "]";
}
}
/** Comparador for scored results. */
private static final Comparator<ScoredResult> SCORED_RESULT_COMPARATOR =
new Comparator<ScoredResult>() {
@Override
public int compare(ScoredResult r1, ScoredResult r2) {
// Score ordering.
int scoreDiff = Double.compare(r2.score, r1.score);
if (scoreDiff != 0) {
return scoreDiff;
}
// Make tracks come before waypoints.
if (r1.waypoint != null && r2.track != null) {
return 1;
} else if (r1.track != null && r2.waypoint != null) {
return -1;
}
// Finally, use arbitrary ordering, by ID.
long id1 = r1.track != null ? r1.track.getId() : r1.waypoint.getId();
long id2 = r2.track != null ? r2.track.getId() : r2.waypoint.getId();
long idDiff = id2 - id1;
return Long.signum(idDiff);
}
};
private final MyTracksProviderUtils providerUtils;
public SearchEngine(MyTracksProviderUtils providerUtils) {
this.providerUtils = providerUtils;
}
/**
* Executes a search query and returns a set of sorted results.
*
* @param query the query to execute
* @return a set of results, sorted according to their score
*/
public SortedSet<ScoredResult> search(SearchQuery query) {
ArrayList<Track> tracks = new ArrayList<Track>();
ArrayList<Waypoint> waypoints = new ArrayList<Waypoint>();
TreeSet<ScoredResult> scoredResults = new TreeSet<ScoredResult>(SCORED_RESULT_COMPARATOR);
retrieveTracks(query, tracks);
retrieveWaypoints(query, waypoints);
scoreTrackResults(tracks, query, scoredResults);
scoreWaypointResults(waypoints, query, scoredResults);
return scoredResults;
}
/**
* Retrieves tracks matching the given query from the database.
*
* @param query the query to retrieve for
* @param tracks list to fill with the resulting tracks
*/
private void retrieveTracks(SearchQuery query, ArrayList<Track> tracks) {
String queryLikeSelection = "%" + query.textQuery + "%";
String[] trackSelectionArgs = new String[] {
queryLikeSelection,
queryLikeSelection,
queryLikeSelection };
Cursor cursor = null;
try {
cursor = providerUtils.getTrackCursor(
TRACK_SELECTION_QUERY, trackSelectionArgs, TRACK_SELECTION_ORDER);
if (cursor != null) {
tracks.ensureCapacity(cursor.getCount());
while (cursor.moveToNext()) {
tracks.add(providerUtils.createTrack(cursor));
}
}
} finally {
if (cursor != null) {
cursor.close();
}
}
}
/**
* Retrieves waypoints matching the given query from the database.
*
* @param query the query to retrieve for
* @param waypoints list to fill with the resulting waypoints
*/
private void retrieveWaypoints(SearchQuery query, ArrayList<Waypoint> waypoints) {
String queryLikeSelection2 = "%" + query.textQuery + "%";
String[] waypointSelectionArgs = new String[] {
queryLikeSelection2,
queryLikeSelection2,
queryLikeSelection2 };
Cursor cursor = null;
try {
cursor = providerUtils.getWaypointCursor(WAYPOINT_SELECTION_QUERY, waypointSelectionArgs,
WAYPOINT_SELECTION_ORDER, MAX_SCORED_WAYPOINTS);
if (cursor != null) {
waypoints.ensureCapacity(cursor.getCount());
while (cursor.moveToNext()) {
Waypoint waypoint = providerUtils.createWaypoint(cursor);
if (LocationUtils.isValidLocation(waypoint.getLocation())) {
waypoints.add(waypoint);
}
}
}
} finally {
if (cursor != null) {
cursor.close();
}
}
}
/**
* Scores a collection of track results.
*
* @param tracks the results to score
* @param query the query to score for
* @param output the collection to fill with scored results
*/
private void scoreTrackResults(Collection<Track> tracks, SearchQuery query, Collection<ScoredResult> output) {
for (Track track : tracks) {
// Calculate the score.
double score = scoreTrackResult(query, track);
// Add to the output.
output.add(new ScoredResult(track, score));
}
}
/**
* Scores a single track result.
*
* @param query the query to score for
* @param track the results to score
* @return the score for the track
*/
private double scoreTrackResult(SearchQuery query, Track track) {
double score = 1.0;
score *= getTitleBoost(query, track.getName(), track.getDescription(), track.getCategory());
TripStatistics statistics = track.getTripStatistics();
// TODO: Also boost for proximity to the currently-centered position on the map.
score *= getDistanceBoost(query, statistics.getMeanLatitude(), statistics.getMeanLongitude());
long meanTimestamp = (statistics.getStartTime() + statistics.getStopTime()) / 2L;
score *= getTimeBoost(query, meanTimestamp);
// Score the currently-selected track lower (user is already there, wouldn't be searching for it).
if (track.getId() == query.currentTrackId) {
score *= CURRENT_TRACK_DEMOTION;
}
return score;
}
/**
* Scores a collection of waypoint results.
*
* @param waypoints the results to score
* @param query the query to score for
* @param output the collection to fill with scored results
*/
private void scoreWaypointResults(Collection<Waypoint> waypoints, SearchQuery query, Collection<ScoredResult> output) {
for (Waypoint waypoint : waypoints) {
// Calculate the score.
double score = scoreWaypointResult(query, waypoint);
// Add to the output.
output.add(new ScoredResult(waypoint, score));
}
}
/**
* Scores a single waypoint result.
*
* @param query the query to score for
* @param waypoint the results to score
* @return the score for the waypoint
*/
private double scoreWaypointResult(SearchQuery query, Waypoint waypoint) {
double score = 1.0;
Location location = waypoint.getLocation();
score *= getTitleBoost(query, waypoint.getName(), waypoint.getDescription(), waypoint.getCategory());
// TODO: Also boost for proximity to the currently-centered position on the map.
score *= getDistanceBoost(query, location.getLatitude(), location.getLongitude());
score *= getTimeBoost(query, location.getTime());
// Score waypoints in the currently-selected track higher (searching inside the current track).
if (query.currentTrackId != -1 && waypoint.getTrackId() == query.currentTrackId) {
score *= CURRENT_TRACK_WAYPOINT_PROMOTION;
}
return score;
}
/**
* Calculates the boosting of the score due to the field(s) in which the match occured.
*
* @param query the query to boost for
* @param name the name of the track or waypoint
* @param description the description of the track or waypoint
* @param category the category of the track or waypoint
* @return the total boost to be applied to the result
*/
private double getTitleBoost(SearchQuery query,
String name, String description, String category) {
// Title boost: track name > description > category.
double boost = 1.0;
if (name.toLowerCase(Locale.getDefault()).contains(query.textQuery)) {
boost *= TRACK_NAME_PROMOTION;
}
if (description.toLowerCase(Locale.getDefault()).contains(query.textQuery)) {
boost *= TRACK_DESCRIPTION_PROMOTION;
}
if (category.toLowerCase(Locale.getDefault()).contains(query.textQuery)) {
boost *= TRACK_CATEGORY_PROMOTION;
}
return boost;
}
/**
* Calculates the boosting of the score due to the recency of the matched entity.
*
* @param query the query to boost for
* @param timestamp the timestamp to calculate the boost for
* @return the total boost to be applied to the result
*/
private double getTimeBoost(SearchQuery query, long timestamp) {
if (timestamp < OLDEST_ALLOWED_TIMESTAMP) {
// Safety: if timestamp is too old or invalid, don't rank based on time.
return 1.0;
}
// Score recent tracks higher.
long timeAgoHours = (long) ((query.currentTimestamp - timestamp) * UnitConversions.MS_TO_S
* UnitConversions.S_TO_MIN * UnitConversions.MIN_TO_HR);
if (timeAgoHours > 0L) {
return squash(timeAgoHours);
} else {
// Should rarely happen (track recorded in the last hour).
return Double.POSITIVE_INFINITY;
}
}
/**
* Calculates the boosting of the score due to proximity to a location.
*
* @param query the query to boost for
* @param latitude the latitude to calculate the boost for
* @param longitude the longitude to calculate the boost for
* @return the total boost to be applied to the result
*/
private double getDistanceBoost(SearchQuery query, double latitude, double longitude) {
if (query.currentLocation == null) {
return 1.0;
}
float[] distanceResults = new float[1];
Location.distanceBetween(
latitude, longitude,
query.currentLocation.getLatitude(), query.currentLocation.getLongitude(),
distanceResults);
// Score tracks close to the current location higher.
double distanceKm = distanceResults[0] * UnitConversions.M_TO_KM;
if (distanceKm > 0.0) {
// Use the inverse of the amortized distance.
return squash(distanceKm);
} else {
// Should rarely happen (distance is exactly 0).
return Double.POSITIVE_INFINITY;
}
}
/**
* Squashes a number by calculating 1 / log (1 + x).
*/
private static double squash(double x) {
return 1.0 / Math.log1p(x);
}
}
@@ -0,0 +1,44 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.content;
import android.content.Context;
import android.content.SearchRecentSuggestionsProvider;
import android.provider.SearchRecentSuggestions;
/**
* Content provider for search suggestions.
*
* @author Rodrigo Damazio
*/
public class SearchEngineProvider extends SearchRecentSuggestionsProvider {
private static final String AUTHORITY = "com.google.android.maps.mytracks.search";
private static final int MODE = DATABASE_MODE_QUERIES;
public SearchEngineProvider() {
setupSuggestions(AUTHORITY, MODE);
}
// TODO: Also add suggestions from the database.
/**
* Creates and returns a helper for adding recent queries or clearing the recent query history.
*/
public static SearchRecentSuggestions newHelper(Context context) {
return new SearchRecentSuggestions(context, AUTHORITY, MODE);
}
}
@@ -0,0 +1,644 @@
/*
* Copyright 2011 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.content;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils.LocationIterator;
import com.google.android.apps.mytracks.util.LocationUtils;
import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.android.maps.mytracks.R;
import com.google.common.annotations.VisibleForTesting;
import android.content.Context;
import android.database.Cursor;
import android.location.Location;
import android.os.Handler;
import android.os.HandlerThread;
import android.util.Log;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Set;
/**
* Track data hub. Receives data from {@link DataSource} and distributes it to
* {@link TrackDataListener} after some processing.
*
* @author Rodrigo Damazio
*/
public class TrackDataHub implements DataSourceListener {
private static final String TAG = TrackDataHub.class.getSimpleName();
/**
* Maximum number of waypoints to displayed.
*/
@VisibleForTesting
static final int MAX_DISPLAYED_WAYPOINTS = 128;
/**
* 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;
private final Context context;
private final TrackDataManager trackDataManager;
private final MyTracksProviderUtils myTracksProviderUtils;
private final int targetNumPoints;
private boolean started;
private HandlerThread handlerThread;
private Handler handler;
private DataSource dataSource;
private DataSourceManager dataSourceManager;
// Preference values
private long selectedTrackId;
private long recordingTrackId;
private boolean recordingTrackPaused;
private boolean metricUnits;
private boolean reportSpeed;
private int recordingGpsAccuracy;
private int recordingDistanceInterval;
private int mapType;
// Track points sampling state
private int numLoadedPoints;
private long firstSeenLocationId;
private long lastSeenLocationId;
/**
* Creates a new instance.
*/
public synchronized static TrackDataHub newInstance(Context context) {
return new TrackDataHub(context, new TrackDataManager(), MyTracksProviderUtils.Factory.get(
context), TARGET_DISPLAYED_TRACK_POINTS);
}
/**
* Constructor.
*
* @param context the context
* @param trackDataManager the track data manager
* @param myTracksProviderUtils the my tracks provider utils
* @param targetNumPoints the target number of points
*/
@VisibleForTesting
TrackDataHub(Context context, TrackDataManager trackDataManager,
MyTracksProviderUtils myTracksProviderUtils, int targetNumPoints) {
this.context = context;
this.trackDataManager = trackDataManager;
this.myTracksProviderUtils = myTracksProviderUtils;
this.targetNumPoints = targetNumPoints;
resetSamplingState();
}
/**
* Starts.
*/
public void start() {
if (started) {
Log.i(TAG, "TrackDataHub already started, ignoring start.");
return;
}
started = true;
handlerThread = new HandlerThread("TrackDataHubHandlerThread");
handlerThread.start();
handler = new Handler(handlerThread.getLooper());
dataSource = newDataSource();
dataSourceManager = new DataSourceManager(dataSource, this);
notifyPreferenceChanged(null);
runInHanderThread(new Runnable() {
@Override
public void run() {
if (dataSourceManager != null) {
dataSourceManager.updateListeners(trackDataManager.getRegisteredTrackDataTypes());
loadDataForAll();
}
}
});
}
/**
* Stops.
*/
public void stop() {
if (!started) {
Log.i(TAG, "TrackDataHub not started, ignoring stop.");
return;
}
started = false;
dataSourceManager.unregisterAllListeners();
if (handlerThread != null) {
handlerThread.getLooper().quit();
handlerThread = null;
}
handler = null;
dataSource = null;
dataSourceManager = null;
}
/**
* Loads a track.
*
* @param trackId the track id
*/
public void loadTrack(final long trackId) {
runInHanderThread(new Runnable() {
@Override
public void run() {
if (trackId == selectedTrackId) {
Log.i(TAG, "Not reloading track " + trackId);
return;
}
selectedTrackId = trackId;
loadDataForAll();
}
});
}
/**
* Registers a {@link TrackDataListener}.
*
* @param trackDataListener the track data listener
* @param trackDataTypes the track data types
*/
public void registerTrackDataListener(
final TrackDataListener trackDataListener, final EnumSet<TrackDataType> trackDataTypes) {
runInHanderThread(new Runnable() {
@Override
public void run() {
trackDataManager.registerListener(trackDataListener, trackDataTypes);
if (dataSourceManager != null) {
dataSourceManager.updateListeners(trackDataManager.getRegisteredTrackDataTypes());
loadDataForListener(trackDataListener);
}
}
});
}
/**
* Unregisters a {@link TrackDataListener}.
*
* @param trackDataListener the track data listener
*/
public void unregisterTrackDataListener(final TrackDataListener trackDataListener) {
runInHanderThread(new Runnable() {
@Override
public void run() {
trackDataManager.unregisterListener(trackDataListener);
if (dataSourceManager != null) {
dataSourceManager.updateListeners(trackDataManager.getRegisteredTrackDataTypes());
}
}
});
}
/**
* Reloads data for a {@link TrackDataListener}.
*/
public void reloadDataForListener(final TrackDataListener trackDataListener) {
runInHanderThread(new Runnable() {
@Override
public void run() {
loadDataForListener(trackDataListener);
}
});
}
/**
* Returns true if the selected track is recording.
*/
public boolean isSelectedTrackRecording() {
return selectedTrackId == recordingTrackId
&& recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
}
/**
* Returns true if the selected track is paused.
*/
public boolean isSelectedTrackPaused() {
return selectedTrackId == recordingTrackId && recordingTrackPaused;
}
@Override
public void notifyTracksTableUpdated() {
runInHanderThread(new Runnable() {
@Override
public void run() {
notifyTracksTableUpdate(trackDataManager.getListeners(TrackDataType.TRACKS_TABLE));
}
});
}
@Override
public void notifyWaypointsTableUpdated() {
runInHanderThread(new Runnable() {
@Override
public void run() {
notifyWaypointsTableUpdate(trackDataManager.getListeners(TrackDataType.WAYPOINTS_TABLE));
}
});
}
@Override
public void notifyTrackPointsTableUpdated() {
runInHanderThread(new Runnable() {
@Override
public void run() {
notifyTrackPointsTableUpdate(
true, trackDataManager.getListeners(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE),
trackDataManager.getListeners(TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE));
}
});
}
@Override
public void notifyPreferenceChanged(final String key) {
runInHanderThread(new Runnable() {
@Override
public void run() {
if (key == null
|| key.equals(PreferencesUtils.getKey(context, R.string.recording_track_id_key))) {
recordingTrackId = PreferencesUtils.getLong(context, R.string.recording_track_id_key);
}
if (key == null
|| key.equals(PreferencesUtils.getKey(context, R.string.recording_track_paused_key))) {
recordingTrackPaused = PreferencesUtils.getBoolean(
context, R.string.recording_track_paused_key,
PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT);
}
if (key == null
|| key.equals(PreferencesUtils.getKey(context, R.string.stats_units_key))) {
metricUnits = PreferencesUtils.isMetricUnits(context);
if (key != null) {
for (TrackDataListener trackDataListener :
trackDataManager.getListeners(TrackDataType.PREFERENCE)) {
if (trackDataListener.onMetricUnitsChanged(metricUnits)) {
loadDataForListener(trackDataListener);
}
}
}
}
if (key == null
|| key.equals(PreferencesUtils.getKey(context, R.string.stats_rate_key))) {
reportSpeed = PreferencesUtils.isReportSpeed(context);
if (key != null) {
for (TrackDataListener trackDataListener :
trackDataManager.getListeners(TrackDataType.PREFERENCE)) {
if (trackDataListener.onReportSpeedChanged(reportSpeed)) {
loadDataForListener(trackDataListener);
}
}
}
}
if (key == null
|| key.equals(PreferencesUtils.getKey(context, R.string.recording_gps_accuracy_key))) {
recordingGpsAccuracy = PreferencesUtils.getInt(context, R.string.recording_gps_accuracy_key,
PreferencesUtils.RECORDING_GPS_ACCURACY_DEFAULT);
if (key != null) {
for (TrackDataListener trackDataListener :
trackDataManager.getListeners(TrackDataType.PREFERENCE)) {
if (trackDataListener.onRecordingGpsAccuracy(recordingGpsAccuracy)) {
loadDataForListener(trackDataListener);
}
}
}
}
if (key == null || key.equals(
PreferencesUtils.getKey(context, R.string.recording_distance_interval_key))) {
recordingDistanceInterval = PreferencesUtils.getInt(
context, R.string.recording_distance_interval_key,
PreferencesUtils.RECORDING_DISTANCE_INTERVAL_DEFAULT);
if (key != null) {
for (TrackDataListener trackDataListener :
trackDataManager.getListeners(TrackDataType.PREFERENCE)) {
if (trackDataListener.onRecordingDistanceIntervalChanged(recordingDistanceInterval)) {
loadDataForListener(trackDataListener);
}
}
}
}
if (key == null || key.equals(PreferencesUtils.getKey(context, R.string.map_type_key))) {
mapType = PreferencesUtils.getInt(
context, R.string.map_type_key, PreferencesUtils.MAP_TYPE_DEFAUlT);
if (key != null) {
for (TrackDataListener trackDataListener :
trackDataManager.getListeners(TrackDataType.PREFERENCE)) {
if (trackDataListener.onMapTypeChanged(mapType)) {
loadDataForListener(trackDataListener);
}
}
}
}
}
});
}
/**
* Loads data for all listeners. To be run in the {@link #handler} thread.
*/
private void loadDataForAll() {
resetSamplingState();
if (trackDataManager.getNumberOfListeners() == 0) {
return;
}
for (TrackDataListener trackDataListener :
trackDataManager.getListeners(TrackDataType.PREFERENCE)) {
trackDataListener.onMetricUnitsChanged(metricUnits);
trackDataListener.onReportSpeedChanged(reportSpeed);
trackDataListener.onRecordingGpsAccuracy(recordingGpsAccuracy);
trackDataListener.onRecordingDistanceIntervalChanged(recordingDistanceInterval);
trackDataListener.onMapTypeChanged(mapType);
}
notifyTracksTableUpdate(trackDataManager.getListeners(TrackDataType.TRACKS_TABLE));
for (TrackDataListener listener :
trackDataManager.getListeners(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE)) {
listener.clearTrackPoints();
}
notifyTrackPointsTableUpdate(true,
trackDataManager.getListeners(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE),
trackDataManager.getListeners(TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE));
notifyWaypointsTableUpdate(trackDataManager.getListeners(TrackDataType.WAYPOINTS_TABLE));
}
/**
* Loads data for a listener. To be run in the {@link #handler} thread.
*
* @param trackDataListener the track data listener.
*/
private void loadDataForListener(TrackDataListener trackDataListener) {
Set<TrackDataListener> trackDataListeners = Collections.singleton(trackDataListener);
EnumSet<TrackDataType> trackDataTypes = trackDataManager.getTrackDataTypes(trackDataListener);
if (trackDataTypes.contains(TrackDataType.PREFERENCE)) {
trackDataListener.onMetricUnitsChanged(metricUnits);
trackDataListener.onReportSpeedChanged(reportSpeed);
trackDataListener.onRecordingGpsAccuracy(recordingGpsAccuracy);
trackDataListener.onRecordingDistanceIntervalChanged(recordingDistanceInterval);
trackDataListener.onMapTypeChanged(mapType);
}
if (trackDataTypes.contains(TrackDataType.TRACKS_TABLE)) {
notifyTracksTableUpdate(trackDataListeners);
}
boolean hasSampledIn = trackDataTypes.contains(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE);
boolean hasSampledOut = trackDataTypes.contains(TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE);
if (hasSampledIn || hasSampledOut) {
trackDataListener.clearTrackPoints();
boolean isOnlyListener = trackDataManager.getNumberOfListeners() == 1;
if (isOnlyListener) {
resetSamplingState();
}
Set<TrackDataListener> sampledInListeners = trackDataListeners;
Set<TrackDataListener> sampledOutListeners = hasSampledOut ? trackDataListeners
: Collections.<TrackDataListener> emptySet();
notifyTrackPointsTableUpdate(isOnlyListener, sampledInListeners, sampledOutListeners);
}
if (trackDataTypes.contains(TrackDataType.WAYPOINTS_TABLE)) {
notifyWaypointsTableUpdate(trackDataListeners);
}
}
/**
* Notifies track table update. To be run in the {@link #handler} thread.
*
* @param trackDataListeners the track data listeners to notify
*/
private void notifyTracksTableUpdate(Set<TrackDataListener> trackDataListeners) {
if (trackDataListeners.isEmpty()) {
return;
}
Track track = myTracksProviderUtils.getTrack(selectedTrackId);
for (TrackDataListener trackDataListener : trackDataListeners) {
trackDataListener.onTrackUpdated(track);
}
}
/**
* Notifies waypoint table update. Currently, reloads all the waypoints up to
* {@link #MAX_DISPLAYED_WAYPOINTS}. To be run in the {@link #handler}
* thread.
*
* @param trackDataListeners the track data listeners to notify
*/
private void notifyWaypointsTableUpdate(Set<TrackDataListener> trackDataListeners) {
if (trackDataListeners.isEmpty()) {
return;
}
for (TrackDataListener trackDataListener : trackDataListeners) {
trackDataListener.clearWaypoints();
}
Cursor cursor = null;
try {
cursor = myTracksProviderUtils.getWaypointCursor(
selectedTrackId, -1L, MAX_DISPLAYED_WAYPOINTS);
if (cursor != null && cursor.moveToFirst()) {
do {
Waypoint waypoint = myTracksProviderUtils.createWaypoint(cursor);
if (!LocationUtils.isValidLocation(waypoint.getLocation())) {
continue;
}
for (TrackDataListener trackDataListener : trackDataListeners) {
trackDataListener.onNewWaypoint(waypoint);
}
} while (cursor.moveToNext());
}
} finally {
if (cursor != null) {
cursor.close();
}
}
for (TrackDataListener trackDataListener : trackDataListeners) {
trackDataListener.onNewWaypointsDone();
}
}
/**
* Notifies track points table update. To be run in the {@link #handler}
* thread.
*
* @param updateSamplingState true to update the sampling state
* @param sampledInListeners the sampled-in listeners
* @param sampledOutListeners the sampled-out listeners
*/
private void notifyTrackPointsTableUpdate(boolean updateSamplingState,
Set<TrackDataListener> sampledInListeners, Set<TrackDataListener> sampledOutListeners) {
if (sampledInListeners.isEmpty() && sampledOutListeners.isEmpty()) {
return;
}
if (updateSamplingState && numLoadedPoints >= targetNumPoints) {
// Reload and resample the track at a lower frequency.
Log.i(TAG, "Resampling track after " + numLoadedPoints + " points.");
resetSamplingState();
for (TrackDataListener listener : sampledInListeners) {
listener.clearTrackPoints();
}
}
int localNumLoadedPoints = updateSamplingState ? numLoadedPoints : 0;
long localFirstSeenLocationId = updateSamplingState ? firstSeenLocationId : -1L;
long localLastSeenLocationId = updateSamplingState ? lastSeenLocationId : -1L;
long maxPointId = updateSamplingState ? -1L : lastSeenLocationId;
long lastTrackPointId = myTracksProviderUtils.getLastTrackPointId(selectedTrackId);
int samplingFrequency = -1;
boolean includeNextPoint = false;
LocationIterator locationIterator = null;
try {
locationIterator = myTracksProviderUtils.getTrackPointLocationIterator(selectedTrackId,
localLastSeenLocationId + 1, false, MyTracksProviderUtils.DEFAULT_LOCATION_FACTORY);
while (locationIterator.hasNext()) {
Location location = locationIterator.next();
long locationId = locationIterator.getLocationId();
// Stop if past the last wanted point
if (maxPointId != -1L && locationId > maxPointId) {
break;
}
if (localFirstSeenLocationId == -1) {
localFirstSeenLocationId = locationId;
}
if (samplingFrequency == -1) {
long numTotalPoints = Math.max(0L, lastTrackPointId - localFirstSeenLocationId);
samplingFrequency = 1 + (int) (numTotalPoints / targetNumPoints);
}
if (!LocationUtils.isValidLocation(location)) {
// TODO: also include the last valid point before a split
for (TrackDataListener trackDataListener : sampledInListeners) {
trackDataListener.onSegmentSplit(location);
includeNextPoint = true;
}
} else {
// Also include the last point if the selected track is not recording.
if (includeNextPoint || (localNumLoadedPoints % samplingFrequency == 0)
|| (locationId == lastTrackPointId && !isSelectedTrackRecording())) {
includeNextPoint = false;
for (TrackDataListener trackDataListener : sampledInListeners) {
trackDataListener.onSampledInTrackPoint(location);
}
} else {
for (TrackDataListener trackDataListener : sampledOutListeners) {
trackDataListener.onSampledOutTrackPoint(location);
}
}
}
localNumLoadedPoints++;
localLastSeenLocationId = locationId;
}
} finally {
if (locationIterator != null) {
locationIterator.close();
}
}
if (updateSamplingState) {
numLoadedPoints = localNumLoadedPoints;
firstSeenLocationId = localFirstSeenLocationId;
lastSeenLocationId = localLastSeenLocationId;
}
for (TrackDataListener listener : sampledInListeners) {
listener.onNewTrackPointsDone();
}
}
/**
* Resets the track points sampling states.
*/
private void resetSamplingState() {
numLoadedPoints = 0;
firstSeenLocationId = -1L;
lastSeenLocationId = -1L;
}
/**
* Creates a {@link DataSource}.
*/
@VisibleForTesting
protected DataSource newDataSource() {
return new DataSource(context);
}
/**
* Run in the handler thread.
*
* @param runnable the runnable
*/
@VisibleForTesting
protected void runInHanderThread(Runnable runnable) {
if (handler == null) {
// Use a Throwable to ensure the stack trace is logged.
Log.d(TAG, "handler is null.", new Throwable());
return;
}
handler.post(runnable);
}
/**
* Gets the value selectedTrackId.
*
* @return the selectedTrackId
*/
public long getSelectedTrackId() {
return selectedTrackId;
}
/**
* Gets the recordingGpsAccuracy.
*/
@VisibleForTesting
int getRecordingGpsAccuracy() {
return recordingGpsAccuracy;
}
/**
* Gets the metricUnits.
*
* @return the metricUnits
*/
@VisibleForTesting
boolean isMetricUnits() {
return metricUnits;
}
/**
* Gets the reportSpeed.
*
* @return the reportSpeed
*/
@VisibleForTesting
boolean isReportSpeed() {
return reportSpeed;
}
}
@@ -0,0 +1,134 @@
/*
* Copyright 2011 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.content;
import android.location.Location;
/**
* Listener for track data changes.
*
* @author Rodrigo Damazio
*/
public interface TrackDataListener {
/**
* Location state.
*
* @author Jimmy Shih
*/
public enum LocationState {
DISABLED, NO_FIX, BAD_FIX, GOOD_FIX;
}
/**
* Called when the track or its statistics has been updated.
*
* @param track the track
*/
public void onTrackUpdated(Track track);
/**
* Called to clear previously-sent track points.
*/
public void clearTrackPoints();
/**
* Called when a sampled in track point is read.
*
* @param location the location
*/
public void onSampledInTrackPoint(Location location);
/**
* Called when a sampled out track point is read.
*
* @param location the location
*/
public void onSampledOutTrackPoint(Location location);
/**
* Called when an invalid track point representing a segment split is read.
*/
public void onSegmentSplit(Location location);
/**
* Called when finish sending new track points. This gets called after every
* batch of calls to {@link #onSampledInTrackPoint(Location)},
* {@link #onSampledOutTrackPoint(Location)} and
* {@link #onSegmentSplit(Location)}.
*/
public void onNewTrackPointsDone();
/**
* Called to clear previously sent waypoints.
*/
public void clearWaypoints();
/**
* Called when a new waypoint is read.
*
* @param waypoint the waypoint
*/
public void onNewWaypoint(Waypoint waypoint);
/**
* Called when finish sending new waypoints. This gets called after every
* batch of calls to {@link #clearWaypoints()} and
* {@link #onNewWaypoint(Waypoint)}.
*/
public void onNewWaypointsDone();
/**
* Called when the metric units preference value is change.
*
* @param metricUnits true to use metric units, false to use imperial units
* @return true to reload all the data, false otherwise.
*/
public boolean onMetricUnitsChanged(boolean metricUnits);
/**
* Called when the report speed preference value is changed.
*
* @param reportSpeed true to report speed, false to report pace
* @return true to reload all the data, false otherwise.
*/
public boolean onReportSpeedChanged(boolean reportSpeed);
/**
* Called when the recording gps accuracy preference value is changed.
*
* @param recordingGpsAccuracy the new value
* @return true to reload all the data, false otherwise
*/
public boolean onRecordingGpsAccuracy(int recordingGpsAccuracy);
/**
* Called when the recording distance interval preference value is changed.
*
* @param recordingDistanceInterval the new value
* @return true to reload all the data, false otherwise.
*/
public boolean onRecordingDistanceIntervalChanged(int recordingDistanceInterval);
/**
* Called when the map type preference value is changed.
*
* @param mapType the new value
* @return true to reload all the data, false otherwise.
*/
public boolean onMapTypeChanged(int mapType);
}
@@ -0,0 +1,123 @@
/*
* Copyright 2011 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.content;
import android.util.Log;
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
/**
* Manages register/unregister {@link TrackDataListener} and keeping the state
* for each registered listener.
*
* @author Rodrigo Damazio
*/
public class TrackDataManager {
private static final String TAG = TrackDataManager.class.getSimpleName();
// Map of listener to its track data types
private final Map<TrackDataListener, EnumSet<TrackDataType>>
listenerToTypesMap = new HashMap<TrackDataListener, EnumSet<TrackDataType>>();
// Map of track data type to listeners
private final Map<TrackDataType, Set<TrackDataListener>>
typeToListenersMap = new EnumMap<TrackDataType, Set<TrackDataListener>>(TrackDataType.class);
public TrackDataManager() {
for (TrackDataType trackDataType : TrackDataType.values()) {
typeToListenersMap.put(trackDataType, new LinkedHashSet<TrackDataListener>());
}
}
/**
* Registers a listener.
*
* @param listener the listener
* @param trackDataTypes the track data types the listener is interested
*/
public void registerListener(
TrackDataListener listener, EnumSet<TrackDataType> trackDataTypes) {
if (listenerToTypesMap.containsKey(listener)) {
Log.w(TAG, "Tried to register a listener that is already registered. Ignore.");
return;
}
listenerToTypesMap.put(listener, trackDataTypes);
for (TrackDataType trackDataType : trackDataTypes) {
typeToListenersMap.get(trackDataType).add(listener);
}
}
/**
* Unregisters a listener.
*
* @param listener the listener
*/
public void unregisterListener(TrackDataListener listener) {
EnumSet<TrackDataType> removedTypes = listenerToTypesMap.remove(listener);
if (removedTypes == null) {
Log.w(TAG, "Tried to unregister a listener that is not registered. Ignore.");
return;
}
// Remove the listener from the typeToListenersMap
for (TrackDataType trackDataType : removedTypes) {
typeToListenersMap.get(trackDataType).remove(listener);
}
}
/**
* Gets the number of {@link TrackDataListener}.
*/
public int getNumberOfListeners() {
return listenerToTypesMap.size();
}
/**
* Gets the track data types for a listener.
*
* @param listener the listener
*/
public EnumSet<TrackDataType> getTrackDataTypes(TrackDataListener listener) {
return listenerToTypesMap.get(listener);
}
/**
* Gets the listeners for a {@link TrackDataType}.
*
* @param type the type
*/
public Set<TrackDataListener> getListeners(TrackDataType type) {
return typeToListenersMap.get(type);
}
/**
* Gets all the registered {@link TrackDataType}.
*/
public EnumSet<TrackDataType> getRegisteredTrackDataTypes() {
EnumSet<TrackDataType> types = EnumSet.noneOf(TrackDataType.class);
for (EnumSet<TrackDataType> value : listenerToTypesMap.values()) {
types.addAll(value);
}
return types;
}
}
@@ -0,0 +1,30 @@
/*
* Copyright 2011 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.content;
/**
* Types of track data.
*
* @author Jimmy Shih
*/
public enum TrackDataType {
TRACKS_TABLE, // tracks table changes
WAYPOINTS_TABLE, // waypoints table changes
SAMPLED_IN_TRACK_POINTS_TABLE, // sampled-in track points table changes
SAMPLED_OUT_TRACK_POINTS_TABLE, // sampled-out track points table changes
PREFERENCE // preference changes
}
@@ -0,0 +1,47 @@
/*
* Copyright 2013 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.fragments;
import com.google.android.apps.mytracks.util.DialogUtils;
import android.app.Dialog;
import android.content.DialogInterface;
import android.os.Bundle;
import android.support.v4.app.DialogFragment;
/**
* Abstract My Tracks DialogFragment.
*
* @author Jimmy Shih
*/
public abstract class AbstractMyTracksDialogFragment extends DialogFragment {
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
final Dialog dialog = createDialog();
dialog.setOnShowListener(new DialogInterface.OnShowListener() {
@Override
public void onShow(DialogInterface dialogInterface) {
DialogUtils.setDialogTitleDivider(getActivity(), dialog);
}
});
return dialog;
}
protected abstract Dialog createDialog();
}
@@ -0,0 +1,545 @@
/*
* Copyright 2009 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.fragments;
import com.google.android.apps.mytracks.ChartView;
import com.google.android.apps.mytracks.TrackDetailActivity;
import com.google.android.apps.mytracks.content.MyTracksLocation;
import com.google.android.apps.mytracks.content.Sensor;
import com.google.android.apps.mytracks.content.Sensor.SensorDataSet;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.TrackDataHub;
import com.google.android.apps.mytracks.content.TrackDataListener;
import com.google.android.apps.mytracks.content.TrackDataType;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.android.apps.mytracks.stats.TripStatisticsUpdater;
import com.google.android.apps.mytracks.util.CalorieUtils.ActivityType;
import com.google.android.apps.mytracks.util.LocationUtils;
import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.android.apps.mytracks.util.UnitConversions;
import com.google.android.maps.mytracks.R;
import com.google.common.annotations.VisibleForTesting;
import android.location.Location;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.widget.ZoomControls;
import java.util.ArrayList;
import java.util.EnumSet;
/**
* A fragment to display track chart to the user.
*
* @author Sandor Dornbush
* @author Rodrigo Damazio
*/
public class ChartFragment extends Fragment implements TrackDataListener {
public static final String CHART_FRAGMENT_TAG = "chartFragment";
private final ArrayList<double[]> pendingPoints = new ArrayList<double[]>();
private TrackDataHub trackDataHub;
// Stats gathered from the received data
private TripStatisticsUpdater tripStatisticsUpdater;
private long startTime;
private boolean metricUnits = true;
private boolean reportSpeed = true;
private int recordingDistanceInterval = PreferencesUtils.RECORDING_DISTANCE_INTERVAL_DEFAULT;
// Modes of operation
private boolean chartByDistance = true;
private boolean[] chartShow = new boolean[] { true, true, true, true, true, true };
// UI elements
private ChartView chartView;
private ZoomControls zoomControls;
/**
* A runnable that will enable/disable zoom controls and orange pointer as
* appropriate and redraw.
*/
private final Runnable updateChart = new Runnable() {
@Override
public void run() {
if (!isResumed() || trackDataHub == null) {
return;
}
zoomControls.setIsZoomInEnabled(chartView.canZoomIn());
zoomControls.setIsZoomOutEnabled(chartView.canZoomOut());
chartView.setShowPointer(isSelectedTrackRecording());
chartView.invalidate();
}
};
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
/*
* Create a chartView here to store data thus won't need to reload all the
* data on every onStart or onResume.
*/
chartView = new ChartView(getActivity());
};
@Override
public View onCreateView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.chart, container, false);
zoomControls = (ZoomControls) view.findViewById(R.id.chart_zoom_controls);
zoomControls.setOnZoomInClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
zoomIn();
}
});
zoomControls.setOnZoomOutClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
zoomOut();
}
});
return view;
}
@Override
public void onStart() {
super.onStart();
ViewGroup layout = (ViewGroup) getActivity().findViewById(R.id.chart_view_layout);
LayoutParams layoutParams = new LayoutParams(
LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
layout.addView(chartView, layoutParams);
}
@Override
public void onResume() {
super.onResume();
resumeTrackDataHub();
checkChartSettings();
getActivity().runOnUiThread(updateChart);
}
@Override
public void onPause() {
super.onPause();
pauseTrackDataHub();
}
@Override
public void onStop() {
super.onStop();
ViewGroup layout = (ViewGroup) getActivity().findViewById(R.id.chart_view_layout);
layout.removeView(chartView);
}
@Override
public void onTrackUpdated(Track track) {
if (isResumed()) {
if (track == null || track.getTripStatistics() == null) {
startTime = -1L;
return;
}
startTime = track.getTripStatistics().getStartTime();
}
}
@Override
public void clearTrackPoints() {
if (isResumed()) {
tripStatisticsUpdater = startTime != -1L ? new TripStatisticsUpdater(startTime) : null;
pendingPoints.clear();
chartView.reset();
runOnUiThread(new Runnable() {
@Override
public void run() {
if (isResumed()) {
chartView.resetScroll();
}
}
});
}
}
@Override
public void onSampledInTrackPoint(Location location) {
if (isResumed()) {
double[] data = new double[ChartView.NUM_SERIES + 1];
fillDataPoint(location, data);
pendingPoints.add(data);
}
}
@Override
public void onSampledOutTrackPoint(Location location) {
if (isResumed()) {
fillDataPoint(location, null);
}
}
@Override
public void onSegmentSplit(Location location) {
if (isResumed()) {
fillDataPoint(location, null);
}
}
@Override
public void onNewTrackPointsDone() {
if (isResumed()) {
chartView.addDataPoints(pendingPoints);
pendingPoints.clear();
runOnUiThread(updateChart);
}
}
@Override
public void clearWaypoints() {
if (isResumed()) {
chartView.clearWaypoints();
}
}
@Override
public void onNewWaypoint(Waypoint waypoint) {
if (isResumed() && waypoint != null && LocationUtils.isValidLocation(waypoint.getLocation())) {
chartView.addWaypoint(waypoint);
}
}
@Override
public void onNewWaypointsDone() {
if (isResumed()) {
runOnUiThread(updateChart);
}
}
@Override
public boolean onMetricUnitsChanged(boolean metric) {
if (isResumed()) {
if (metricUnits == metric) {
return false;
}
metricUnits = metric;
chartView.setMetricUnits(metricUnits);
runOnUiThread(new Runnable() {
@Override
public void run() {
if (isResumed()) {
chartView.requestLayout();
}
}
});
return true;
}
return false;
}
@Override
public boolean onReportSpeedChanged(boolean speed) {
if (isResumed()) {
if (reportSpeed == speed) {
return false;
}
reportSpeed = speed;
chartView.setReportSpeed(reportSpeed);
boolean chartShowSpeed = PreferencesUtils.getBoolean(
getActivity(), R.string.chart_show_speed_key, PreferencesUtils.CHART_SHOW_SPEED_DEFAULT);
setSeriesEnabled(ChartView.SPEED_SERIES, chartShowSpeed && reportSpeed);
setSeriesEnabled(ChartView.PACE_SERIES, chartShowSpeed && !reportSpeed);
runOnUiThread(new Runnable() {
@Override
public void run() {
if (isResumed()) {
chartView.requestLayout();
}
}
});
return true;
}
return false;
}
@Override
public boolean onRecordingGpsAccuracy(int minRequiredAccuracy) {
// We don't care.
return false;
}
@Override
public boolean onRecordingDistanceIntervalChanged(int value) {
if (isResumed()) {
if (recordingDistanceInterval == value) {
return false;
}
recordingDistanceInterval = value;
return true;
}
return false;
}
@Override
public boolean onMapTypeChanged(int mapType) {
// We don't care.
return false;
}
/**
* Checks the chart settings.
*/
private void checkChartSettings() {
boolean needUpdate = false;
if (chartByDistance != PreferencesUtils.isChartByDistance(getActivity())) {
chartByDistance = !chartByDistance;
chartView.setChartByDistance(chartByDistance);
reloadTrackDataHub();
needUpdate = true;
}
if (setSeriesEnabled(ChartView.ELEVATION_SERIES, PreferencesUtils.getBoolean(getActivity(),
R.string.chart_show_elevation_key, PreferencesUtils.CHART_SHOW_ELEVATION_DEFAULT))) {
needUpdate = true;
}
boolean chartShowSpeed = PreferencesUtils.getBoolean(
getActivity(), R.string.chart_show_speed_key, PreferencesUtils.CHART_SHOW_SPEED_DEFAULT);
if (setSeriesEnabled(ChartView.SPEED_SERIES, chartShowSpeed && reportSpeed)) {
needUpdate = true;
}
if (setSeriesEnabled(ChartView.PACE_SERIES, chartShowSpeed && !reportSpeed)) {
needUpdate = true;
}
if (setSeriesEnabled(ChartView.POWER_SERIES, PreferencesUtils.getBoolean(
getActivity(), R.string.chart_show_power_key, PreferencesUtils.CHART_SHOW_POWER_DEFAULT))) {
needUpdate = true;
}
if (setSeriesEnabled(ChartView.CADENCE_SERIES, PreferencesUtils.getBoolean(getActivity(),
R.string.chart_show_cadence_key, PreferencesUtils.CHART_SHOW_CADENCE_DEFAULT))) {
needUpdate = true;
}
if (setSeriesEnabled(ChartView.HEART_RATE_SERIES, PreferencesUtils.getBoolean(getActivity(),
R.string.chart_show_heart_rate_key, PreferencesUtils.CHART_SHOW_HEART_RATE_DEFAULT))) {
needUpdate = true;
}
if (needUpdate) {
chartView.postInvalidate();
}
}
/**
* Sets the series enabled value.
*
* @param index the series index
* @param value the value
* @return true if changed
*/
private boolean setSeriesEnabled(int index, boolean value) {
if (chartShow[index] != value) {
chartShow[index] = value;
chartView.setChartValueSeriesEnabled(index, value);
return true;
} else {
return false;
}
}
/**
* Resumes the trackDataHub. Needs to be synchronized because trackDataHub can
* be accessed by multiple threads.
*/
private synchronized void resumeTrackDataHub() {
trackDataHub = ((TrackDetailActivity) getActivity()).getTrackDataHub();
trackDataHub.registerTrackDataListener(this, EnumSet.of(TrackDataType.TRACKS_TABLE,
TrackDataType.WAYPOINTS_TABLE, TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE,
TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE, TrackDataType.PREFERENCE));
}
/**
* Pauses the trackDataHub. Needs to be synchronized because trackDataHub can
* be accessed by multiple threads.
*/
private synchronized void pauseTrackDataHub() {
trackDataHub.unregisterTrackDataListener(this);
trackDataHub = null;
}
/**
* Returns true if the selected track is recording. Needs to be synchronized
* because trackDataHub can be accessed by multiple threads.
*/
private synchronized boolean isSelectedTrackRecording() {
return trackDataHub != null && trackDataHub.isSelectedTrackRecording();
}
/**
* Reloads the trackDataHub. Needs to be synchronized because trackDataHub can
* be accessed by multiple threads.
*/
private synchronized void reloadTrackDataHub() {
if (trackDataHub != null) {
trackDataHub.reloadDataForListener(this);
}
}
/**
* To zoom in.
*/
private void zoomIn() {
chartView.zoomIn();
zoomControls.setIsZoomInEnabled(chartView.canZoomIn());
zoomControls.setIsZoomOutEnabled(chartView.canZoomOut());
}
/**
* To zoom out.
*/
private void zoomOut() {
chartView.zoomOut();
zoomControls.setIsZoomInEnabled(chartView.canZoomIn());
zoomControls.setIsZoomOutEnabled(chartView.canZoomOut());
}
/**
* Runs a runnable on the UI thread if possible.
*
* @param runnable the runnable
*/
private void runOnUiThread(Runnable runnable) {
FragmentActivity fragmentActivity = getActivity();
if (fragmentActivity != null) {
fragmentActivity.runOnUiThread(runnable);
}
}
/**
* Given a location, fill in a data point, an array of double[]. <br>
* data[0] = time/distance <br>
* data[1] = elevation <br>
* data[2] = speed <br>
* data[3] = pace <br>
* data[4] = heart rate <br>
* data[5] = cadence <br>
* data[6] = power <br>
*
* @param location the location
* @param data the data point to fill in, can be null
*/
@VisibleForTesting
void fillDataPoint(Location location, double data[]) {
double timeOrDistance = Double.NaN;
double elevation = Double.NaN;
double speed = Double.NaN;
double pace = Double.NaN;
double heartRate = Double.NaN;
double cadence = Double.NaN;
double power = Double.NaN;
if (tripStatisticsUpdater != null) {
tripStatisticsUpdater.addLocation(
location, recordingDistanceInterval, false, ActivityType.INVALID, 0.0);
TripStatistics tripStatistics = tripStatisticsUpdater.getTripStatistics();
if (chartByDistance) {
double distance = tripStatistics.getTotalDistance() * UnitConversions.M_TO_KM;
if (!metricUnits) {
distance *= UnitConversions.KM_TO_MI;
}
timeOrDistance = distance;
} else {
timeOrDistance = tripStatistics.getTotalTime();
}
elevation = tripStatisticsUpdater.getSmoothedElevation();
if (!metricUnits) {
elevation *= UnitConversions.M_TO_FT;
}
speed = tripStatisticsUpdater.getSmoothedSpeed() * UnitConversions.MS_TO_KMH;
if (!metricUnits) {
speed *= UnitConversions.KM_TO_MI;
}
pace = speed == 0 ? 0.0 : 60.0 / speed;
}
if (location instanceof MyTracksLocation
&& ((MyTracksLocation) location).getSensorDataSet() != null) {
SensorDataSet sensorDataSet = ((MyTracksLocation) location).getSensorDataSet();
if (sensorDataSet.hasHeartRate()
&& sensorDataSet.getHeartRate().getState() == Sensor.SensorState.SENDING
&& sensorDataSet.getHeartRate().hasValue()) {
heartRate = sensorDataSet.getHeartRate().getValue();
}
if (sensorDataSet.hasCadence()
&& sensorDataSet.getCadence().getState() == Sensor.SensorState.SENDING
&& sensorDataSet.getCadence().hasValue()) {
cadence = sensorDataSet.getCadence().getValue();
}
if (sensorDataSet.hasPower()
&& sensorDataSet.getPower().getState() == Sensor.SensorState.SENDING
&& sensorDataSet.getPower().hasValue()) {
power = sensorDataSet.getPower().getValue();
}
}
if (data != null) {
data[0] = timeOrDistance;
data[1] = elevation;
data[2] = speed;
data[3] = pace;
data[4] = heartRate;
data[5] = cadence;
data[6] = power;
}
}
@VisibleForTesting
ChartView getChartView() {
return chartView;
}
@VisibleForTesting
void setTripStatisticsUpdater(long time) {
tripStatisticsUpdater = new TripStatisticsUpdater(time);
}
@VisibleForTesting
void setChartView(ChartView view) {
chartView = view;
}
@VisibleForTesting
void setMetricUnits(boolean value) {
metricUnits = value;
}
@VisibleForTesting
void setReportSpeed(boolean value) {
reportSpeed = value;
}
@VisibleForTesting
void setChartByDistance(boolean value) {
chartByDistance = value;
}
}
@@ -0,0 +1,123 @@
/*
* Copyright 2013 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.fragments;
import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.android.maps.mytracks.R;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
/**
* A DialogFragment to choose an account.
*
* @author Jimmy Shih
*/
public class ChooseAccountDialogFragment extends AbstractMyTracksDialogFragment {
/**
* Interface for caller of this dialog fragment.
*
* @author Jimmy Shih
*/
public interface ChooseAccountCaller {
/**
* Called when choose account is done.
*
* @param account the account chosen
*/
public void onChooseAccountDone(String account);
}
public static final String CHOOSE_ACCOUNT_DIALOG_TAG = "chooseAccount";
private ChooseAccountCaller caller;
private Account[] accounts;
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
try {
caller = (ChooseAccountCaller) activity;
} catch (ClassCastException e) {
throw new ClassCastException(
activity.toString() + " must implement " + ChooseAccountCaller.class.getSimpleName());
}
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
FragmentActivity fragmentActivity = getActivity();
accounts = AccountManager.get(fragmentActivity).getAccountsByType(Constants.ACCOUNT_TYPE);
if (accounts.length == 1) {
dismiss();
caller.onChooseAccountDone(accounts[0].name);
return;
}
}
@Override
protected Dialog createDialog() {
if (accounts.length == 0) {
return new AlertDialog.Builder(getActivity()).setMessage(
R.string.send_google_no_account_message).setTitle(R.string.send_google_no_account_title)
.setPositiveButton(R.string.generic_ok, new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
caller.onChooseAccountDone(PreferencesUtils.GOOGLE_ACCOUNT_DEFAULT);
}
}).create();
}
String[] choices = new String[accounts.length];
for (int i = 0; i < accounts.length; i++) {
choices[i] = accounts[i].name;
}
return new AlertDialog.Builder(getActivity()).setNegativeButton(
R.string.generic_cancel, new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
caller.onChooseAccountDone(PreferencesUtils.GOOGLE_ACCOUNT_DEFAULT);
}
}).setPositiveButton(R.string.generic_ok, new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
int position = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
caller.onChooseAccountDone(accounts[position].name);
}
}).setSingleChoiceItems(choices, 0, null).setTitle(R.string.send_google_choose_account_title)
.create();
}
@Override
public void onCancel(DialogInterface dialog) {
caller.onChooseAccountDone(PreferencesUtils.GOOGLE_ACCOUNT_DEFAULT);
}
}
@@ -0,0 +1,268 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.fragments;
import com.google.android.apps.mytracks.util.IntentUtils;
import com.google.android.maps.mytracks.R;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.graphics.drawable.Drawable;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.ShareCompat;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
/**
* A DialogFragment to choose an activity.
*
* @author Jimmy Shih
*/
public class ChooseActivityDialogFragment extends AbstractMyTracksDialogFragment {
/**
* Interface for caller of this dialog fragment.
*
* @author Jimmy Shih
*/
public interface ChooseActivityCaller {
/**
* Called when choose activity is done.
*/
public void onChooseActivityDone(String packageName, String className);
}
private static class ViewHolder {
TextView text1;
TextView text2;
ImageView icon;
}
public static final String CHOOSE_ACTIVITY_DIALOG_TAG = "chooseActivityDialog";
private ChooseActivityCaller caller;
private PackageManager packageManager;
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
try {
caller = (ChooseActivityCaller) activity;
} catch (ClassCastException e) {
throw new ClassCastException(
activity.toString() + " must implement " + ChooseActivityCaller.class.getSimpleName());
}
}
@Override
protected Dialog createDialog() {
FragmentActivity fragmentActivity = getActivity();
packageManager = fragmentActivity.getPackageManager();
List<DisplayInfo> displayInfos = getDisplayInfos(fragmentActivity);
ArrayAdapter<DisplayInfo> arrayAdapter = new ArrayAdapter<DisplayInfo>(fragmentActivity,
R.layout.choose_activity_list_item, R.id.choose_activity_list_item_text1, displayInfos) {
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View view;
ViewHolder viewHolder;
if (convertView == null) {
view = getActivity().getLayoutInflater()
.inflate(R.layout.choose_activity_list_item, parent, false);
viewHolder = new ViewHolder();
viewHolder.text1 = (TextView) view.findViewById(R.id.choose_activity_list_item_text1);
viewHolder.text2 = (TextView) view.findViewById(R.id.choose_activity_list_item_text2);
viewHolder.icon = (ImageView) view.findViewById(R.id.choose_activity_list_item_icon);
view.setTag(viewHolder);
} else {
view = convertView;
viewHolder = (ViewHolder) view.getTag();
}
DisplayInfo displayInfo = getItem(position);
viewHolder.text1.setText(displayInfo.primaryLabel);
boolean hasSecondaryLabel = displayInfo.secondaryLabel != null;
viewHolder.text2.setVisibility(hasSecondaryLabel ? View.VISIBLE : View.GONE);
if (hasSecondaryLabel) {
viewHolder.text2.setText(displayInfo.secondaryLabel);
}
viewHolder.icon.setImageDrawable(displayInfo.icon);
return view;
}
};
return new AlertDialog.Builder(fragmentActivity).setSingleChoiceItems(
arrayAdapter, 0, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
AlertDialog alertDialog = (AlertDialog) dialog;
DisplayInfo displayInfo = (DisplayInfo) alertDialog.getListView()
.getItemAtPosition(which);
ActivityInfo activityInfo = displayInfo.resolveInfo.activityInfo;
dismiss();
caller.onChooseActivityDone(
activityInfo.applicationInfo.packageName, activityInfo.name);
}
}).setTitle(R.string.share_track_share_url_title).create();
}
@Override
public void onCancel(DialogInterface dialog) {
super.onCancel(dialog);
caller.onChooseActivityDone(null, null);
}
/**
* Gets the display info.
*/
private List<DisplayInfo> getDisplayInfos(FragmentActivity fragmentActivity) {
List<DisplayInfo> displayInfos = new ArrayList<DisplayInfo>();
Intent intent = ShareCompat.IntentBuilder.from(fragmentActivity)
.setType(IntentUtils.TEXT_PLAIN_TYPE).getIntent();
List<ResolveInfo> resolveInfos = packageManager.queryIntentActivities(
intent, PackageManager.MATCH_DEFAULT_ONLY);
if (resolveInfos != null && resolveInfos.size() > 0) {
int size = resolveInfos.size();
ResolveInfo firstResolveInfo = resolveInfos.get(0);
for (int i = 1; i < size; i++) {
ResolveInfo resolveInfo = resolveInfos.get(i);
if (firstResolveInfo.priority != resolveInfo.priority
|| firstResolveInfo.isDefault != resolveInfo.isDefault) {
while (i < size) {
resolveInfos.remove(i);
size--;
}
}
}
if (size > 1) {
ResolveInfo.DisplayNameComparator displayNameComparator = new ResolveInfo.DisplayNameComparator(
packageManager);
Collections.sort(resolveInfos, displayNameComparator);
}
firstResolveInfo = resolveInfos.get(0);
int start = 0;
CharSequence firstLabel = firstResolveInfo.loadLabel(packageManager);
for (int i = 1; i < size; i++) {
if (firstLabel == null) {
firstLabel = firstResolveInfo.activityInfo.packageName;
}
ResolveInfo resolveInfo = resolveInfos.get(i);
CharSequence label = resolveInfo.loadLabel(packageManager);
if (label == null) {
label = resolveInfo.activityInfo.packageName;
}
if (label.equals(firstLabel)) {
continue;
}
processGroup(resolveInfos, displayInfos, start, i - 1);
firstResolveInfo = resolveInfo;
firstLabel = label;
start = i;
}
// Process last group
processGroup(resolveInfos, displayInfos, start, size - 1);
}
return displayInfos;
}
/**
* Contains display info.
*
* @author Jimmy Shih
*/
private final class DisplayInfo {
private ResolveInfo resolveInfo;
private CharSequence primaryLabel;
private CharSequence secondaryLabel;
private Drawable icon;
public DisplayInfo(ResolveInfo resolveInfo, CharSequence primaryLabel,
CharSequence secondaryLabel, Drawable icon) {
this.resolveInfo = resolveInfo;
this.primaryLabel = primaryLabel;
this.secondaryLabel = secondaryLabel;
this.icon = icon;
}
}
/**
* Processes a group of items with the same label.
*
* @param resolveInfos list of resolve infos
* @param displayInfos list of display infos
* @param start start index
* @param end end index
*/
private void processGroup(
List<ResolveInfo> resolveInfos, List<DisplayInfo> displayInfos, int start, int end) {
ResolveInfo startResolveInfo = resolveInfos.get(start);
CharSequence primaryLabel = startResolveInfo.loadLabel(packageManager);
Drawable icon = startResolveInfo.loadIcon(packageManager);
int num = end - start + 1;
if (num == 1) {
// Only one, set the secondary label to null
displayInfos.add(new DisplayInfo(startResolveInfo, primaryLabel, null, icon));
} else {
// Decide package name or application name for the secondary label
boolean usePackageName = false;
CharSequence appName = startResolveInfo.activityInfo.applicationInfo.loadLabel(
packageManager);
if (appName == null) {
usePackageName = true;
} else {
// Use HashSet to track duplicates
HashSet<CharSequence> duplicates = new HashSet<CharSequence>();
duplicates.add(appName);
for (int i = start + 1; i <= end; i++) {
ResolveInfo resolveInfo = resolveInfos.get(i);
CharSequence name = resolveInfo.activityInfo.applicationInfo.loadLabel(packageManager);
if ((name == null) || (duplicates.contains(name))) {
usePackageName = true;
break;
} else {
duplicates.add(name);
}
}
// Clear HashSet for later use
duplicates.clear();
}
for (int i = start; i <= end; i++) {
ResolveInfo resolveInfo = resolveInfos.get(i);
CharSequence secondaryLabel = usePackageName ? resolveInfo.activityInfo.packageName
: resolveInfo.activityInfo.applicationInfo.loadLabel(packageManager);
displayInfos.add(new DisplayInfo(resolveInfo, primaryLabel, secondaryLabel, icon));
}
}
}
}
@@ -0,0 +1,192 @@
/*
* Copyright 2013 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.fragments;
import com.google.android.apps.mytracks.util.DialogUtils;
import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.android.apps.mytracks.util.StringUtils;
import com.google.android.apps.mytracks.util.TrackIconUtils;
import com.google.android.maps.mytracks.R;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.DialogInterface;
import android.graphics.BitmapFactory;
import android.graphics.BitmapFactory.Options;
import android.os.Bundle;
import android.support.v4.app.DialogFragment;
import android.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.GridView;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.List;
/**
* A DialogFragment to choose an activity type.
*
* @author apoorvn
*/
public class ChooseActivityTypeDialogFragment extends DialogFragment {
/**
* Interface for caller of this dialog fragment.
*
* @author apoorvn
*/
public interface ChooseActivityTypeCaller {
/**
* Called when choose activity type is done.
*/
public void onChooseActivityTypeDone(String iconValue, boolean newWeight);
}
public static final String CHOOSE_ACTIVITY_TYPE_DIALOG_TAG = "chooseActivityType";
private static final String KEY_CATEGORY = "category";
private ChooseActivityTypeCaller caller;
public static ChooseActivityTypeDialogFragment newInstance(String category) {
Bundle bundle = new Bundle();
bundle.putString(KEY_CATEGORY, category);
ChooseActivityTypeDialogFragment fragment = new ChooseActivityTypeDialogFragment();
fragment.setArguments(bundle);
return fragment;
}
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
try {
caller = (ChooseActivityTypeCaller) activity;
} catch (ClassCastException e) {
throw new ClassCastException(activity.toString() + " must implement "
+ ChooseActivityTypeCaller.class.getSimpleName());
}
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
return getDialog(getActivity(), getArguments().getString(KEY_CATEGORY), caller);
}
public static Dialog getDialog(
final Activity activity, final String category, final ChooseActivityTypeCaller caller) {
View view = activity.getLayoutInflater().inflate(R.layout.choose_activity_type, null);
GridView gridView = (GridView) view.findViewById(R.id.choose_activity_type_grid_view);
final View weightContainer = view.findViewById(R.id.choose_activity_type_weight_container);
TextView weightLabel = (TextView) view.findViewById(R.id.choose_activity_type_weight_label);
weightLabel.setText(
PreferencesUtils.isMetricUnits(activity) ? R.string.description_weight_metric
: R.string.description_weight_imperial);
final TextView weight = (TextView) view.findViewById(R.id.choose_activity_type_weight);
List<Integer> imageIds = new ArrayList<Integer>();
for (String iconValue : TrackIconUtils.getAllIconValues()) {
imageIds.add(TrackIconUtils.getIconDrawable(iconValue));
}
Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(activity.getResources(), R.drawable.ic_track_airplane, options);
int padding = 32;
int width = options.outWidth + 2 * padding;
int height = options.outHeight + 2 * padding;
gridView.setColumnWidth(width);
final ChooseActivityTypeImageAdapter imageAdapter = new ChooseActivityTypeImageAdapter(
activity, imageIds, width, height, padding);
gridView.setAdapter(imageAdapter);
final String weightValue = StringUtils.formatWeight(
PreferencesUtils.getWeightDisplayValue(activity));
final AlertDialog alertDialog = new AlertDialog.Builder(activity).setNegativeButton(
R.string.generic_cancel, null)
.setPositiveButton(R.string.generic_ok, new Dialog.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
boolean newWeight = false;
if (weightContainer.getVisibility() == View.VISIBLE) {
String newValue = weight.getText().toString();
if (!newValue.equals(weightValue)) {
newWeight = true;
PreferencesUtils.storeWeightValue(activity, newValue);
}
}
int selected = imageAdapter.getSelected();
caller.onChooseActivityTypeDone(
TrackIconUtils.getAllIconValues().get(selected), newWeight);
}
}).setTitle(R.string.track_edit_activity_type_hint).setView(view).create();
alertDialog.setOnShowListener(new DialogInterface.OnShowListener() {
@Override
public void onShow(DialogInterface dialog) {
int position = getPosition(activity, category);
alertDialog.getButton(Dialog.BUTTON_POSITIVE).setEnabled(position != -1);
if (position != -1) {
imageAdapter.setSelected(position);
imageAdapter.notifyDataSetChanged();
}
updateWeightContainer(weightContainer, position);
weight.setText(weightValue);
DialogUtils.setDialogTitleDivider(activity, alertDialog);
}
});
gridView.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
alertDialog.getButton(Dialog.BUTTON_POSITIVE).setEnabled(true);
imageAdapter.setSelected(position);
imageAdapter.notifyDataSetChanged();
updateWeightContainer(weightContainer, position);
}
});
return alertDialog;
}
private static int getPosition(Activity activity, String category) {
if (category == null) {
return -1;
}
String iconValue = TrackIconUtils.getIconValue(activity, category);
if (iconValue.equals("")) {
return -1;
}
List<String> iconValues = TrackIconUtils.getAllIconValues();
for (int i = 0; i < iconValues.size(); i++) {
if (iconValues.get(i).equals(iconValue)) {
return i;
}
}
return -1;
}
private static void updateWeightContainer(View weightContainer, int position) {
boolean showWeight = position == 0 || position == 1 || position == 2;
weightContainer.setVisibility(showWeight ? View.VISIBLE : View.GONE);
}
}
@@ -0,0 +1,95 @@
/*
* Copyright 2013 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.fragments;
import com.google.android.maps.mytracks.R;
import android.content.Context;
import android.graphics.Color;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import java.util.List;
/**
* Image adapter for choosing an activity type.
*
* @author apoorvn
*/
public class ChooseActivityTypeImageAdapter extends BaseAdapter {
private final Context context;
private final List<Integer> imageIds;
private final int width;
private final int height;
private final int padding;
private int selected = -1;
public ChooseActivityTypeImageAdapter(
Context context, List<Integer> imageIds, int width, int height, int padding) {
this.context = context;
this.imageIds = imageIds;
this.width = width;
this.height = height;
this.padding = padding;
}
@Override
public int getCount() {
return imageIds.size();
}
@Override
public Object getItem(int position) {
return null;
}
@Override
public long getItemId(int position) {
return 0;
}
public int getSelected() {
return selected;
}
public void setSelected(int position) {
selected = position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ImageView imageView;
if (convertView == null) {
imageView = new ImageView(context);
} else {
imageView = (ImageView) convertView;
}
if (position == selected) {
imageView.setBackgroundResource(R.drawable.list_selector_background_transition_holo_dark);
} else {
imageView.setBackgroundColor(Color.TRANSPARENT);
}
imageView.setImageResource(imageIds.get(position));
imageView.setMinimumHeight(height);
imageView.setMinimumWidth(width);
imageView.setPadding(padding, padding, padding, padding);
return imageView;
}
}
@@ -0,0 +1,107 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.fragments;
import com.google.android.apps.mytracks.util.DialogUtils;
import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.android.maps.mytracks.R;
import android.app.Activity;
import android.app.Dialog;
import android.content.DialogInterface;
import android.os.Bundle;
/**
* A DialogFragment to confirm delete.
*
* @author Jimmy Shih
*/
public class ConfirmDeleteDialogFragment extends AbstractMyTracksDialogFragment {
/**
* Interface for caller of this dialog fragment.
*
* @author Jimmy Shih
*/
public interface ConfirmDeleteCaller {
/**
* Called when confirm delete is done.
*
* @param trackIds list of track ids to delete. To delete all, set to size 1
* with trackIds[0] == -1L
*/
public void onConfirmDeleteDone(long[] trackIds);
}
public static final String CONFIRM_DELETE_DIALOG_TAG = "confirmDeleteDialog";
private static final String KEY_TRACK_IDS = "trackIds";
/**
* Create a new instance.
*
* @param trackIds list of track ids to delete. To delete all, set to size 1
* with trackIds[0] == -1L
*/
public static ConfirmDeleteDialogFragment newInstance(long[] trackIds) {
Bundle bundle = new Bundle();
bundle.putLongArray(KEY_TRACK_IDS, trackIds);
ConfirmDeleteDialogFragment deleteTrackDialogFragment = new ConfirmDeleteDialogFragment();
deleteTrackDialogFragment.setArguments(bundle);
return deleteTrackDialogFragment;
}
private ConfirmDeleteCaller caller;
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
try {
caller = (ConfirmDeleteCaller) activity;
} catch (ClassCastException e) {
throw new ClassCastException(
activity.toString() + " must implement " + ConfirmDeleteCaller.class.getSimpleName());
}
}
@Override
protected Dialog createDialog() {
final long[] trackIds = getArguments().getLongArray(KEY_TRACK_IDS);
int titleId;
int messageId;
if (trackIds.length == 1 && trackIds[0] == -1L) {
boolean driveSync = PreferencesUtils.getBoolean(
getActivity(), R.string.drive_sync_key, PreferencesUtils.DRIVE_SYNC_DEFAULT);
titleId = R.string.generic_delete_all_confirm_title;
messageId = driveSync ? R.string.track_delete_all_confirm_message_sync_on
: R.string.track_delete_all_confirm_message_sync_off;
} else {
titleId = trackIds.length > 1 ? R.string.generic_delete_selected_confirm_title
: R.string.track_delete_one_confirm_title;
messageId = trackIds.length > 1 ? R.string.track_delete_multiple_confirm_message
: R.string.track_delete_one_confirm_message;
}
return DialogUtils.createConfirmationDialog(
getActivity(), titleId, getString(messageId), new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
caller.onConfirmDeleteDone(trackIds);
}
});
}
}
@@ -0,0 +1,89 @@
/*
* Copyright 2013 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.fragments;
import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.android.maps.mytracks.R;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.support.v4.app.FragmentActivity;
/**
* A DialogFrament to confirm sync to Google Drive.
*
* @author Jimmy Shih
*/
public class ConfirmSyncDialogFragment extends AbstractMyTracksDialogFragment {
/**
* Interface for caller of this dialog fragment.
*
* @author Jimmy Shih
*/
public interface ConfirmSyncCaller {
/**
* Called when confirm sync is done.
*/
public void onConfirmSyncDone(boolean enable);
}
public static final String CONFIRM_SYNC_DIALOG_TAG = "confirmSyncDialog";
private ConfirmSyncCaller caller;
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
try {
caller = (ConfirmSyncCaller) activity;
} catch (ClassCastException e) {
throw new ClassCastException(
activity.toString() + " must implement " + ConfirmSyncCaller.class.getSimpleName());
}
}
@Override
protected Dialog createDialog() {
FragmentActivity fragmentActivity = getActivity();
String googleAccount = PreferencesUtils.getString(
fragmentActivity, R.string.google_account_key, PreferencesUtils.GOOGLE_ACCOUNT_DEFAULT);
String message = getString(
R.string.sync_drive_confirm_message, googleAccount, getString(R.string.my_tracks_app_name));
return new AlertDialog.Builder(fragmentActivity).setMessage(message)
.setNegativeButton(R.string.generic_no, new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
caller.onConfirmSyncDone(false);
}
}).setPositiveButton(R.string.generic_yes, new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
caller.onConfirmSyncDone(true);
}
}).setTitle(R.string.sync_drive_confirm_title).create();
}
@Override
public void onCancel(DialogInterface arg0) {
caller.onConfirmSyncDone(false);
}
}
@@ -0,0 +1,109 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.fragments;
import com.google.android.apps.mytracks.content.DescriptionGeneratorImpl;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.util.DialogUtils;
import com.google.android.maps.mytracks.R;
import android.app.Activity;
import android.app.Dialog;
import android.content.DialogInterface;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
/**
* A DialogFragment to delete marker.
*
* @author Jimmy Shih
*/
public class DeleteMarkerDialogFragment extends AbstractMyTracksDialogFragment {
/**
* Interface for caller of this dialog fragment.
*
* @author Jimmy Shih
*/
public interface DeleteMarkerCaller {
/**
* Called when delete marker is done.
*/
public void onDeleteMarkerDone();
}
public static final String DELETE_MARKER_DIALOG_TAG = "deleteMarkerDialog";
private static final String KEY_MARKER_IDS = "markerIds";
public static DeleteMarkerDialogFragment newInstance(long[] markerIds) {
Bundle bundle = new Bundle();
bundle.putLongArray(KEY_MARKER_IDS, markerIds);
DeleteMarkerDialogFragment deleteMarkerDialogFragment = new DeleteMarkerDialogFragment();
deleteMarkerDialogFragment.setArguments(bundle);
return deleteMarkerDialogFragment;
}
private DeleteMarkerCaller caller;
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
try {
caller = (DeleteMarkerCaller) activity;
} catch (ClassCastException e) {
throw new ClassCastException(
activity.toString() + " must implement " + DeleteMarkerCaller.class.getSimpleName());
}
}
@Override
protected Dialog createDialog() {
final FragmentActivity fragmentActivity = getActivity();
final long[] markerIds = getArguments().getLongArray(KEY_MARKER_IDS);
int titleId;
int messageId;
if (markerIds.length == 1 && markerIds[0] == -1L) {
titleId = R.string.generic_delete_all_confirm_title;
messageId = R.string.marker_delete_all_confirm_message;
} else {
titleId = markerIds.length > 1 ? R.string.generic_delete_selected_confirm_title
: R.string.marker_delete_one_confirm_title;
messageId = markerIds.length > 1 ? R.string.marker_delete_multiple_confirm_message
: R.string.marker_delete_one_confirm_message;
}
return DialogUtils.createConfirmationDialog(
fragmentActivity, titleId, getString(messageId), new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
new Thread(new Runnable() {
@Override
public void run() {
MyTracksProviderUtils myTracksProviderUtils = MyTracksProviderUtils.Factory.get(
fragmentActivity);
for (long markerId : markerIds) {
myTracksProviderUtils.deleteWaypoint(fragmentActivity,
markerId, new DescriptionGeneratorImpl(fragmentActivity));
}
caller.onDeleteMarkerDone();
}
}).start();
}
});
}
}
@@ -0,0 +1,82 @@
/*
* Copyright 2013 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.fragments;
import com.google.android.maps.mytracks.R;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
/**
* A DialogFrament to prompt user to sync to Google Drive after saving first track.
*
* @author Jimmy Shih
*/
public class EnableSyncDialogFragment extends AbstractMyTracksDialogFragment {
/**
* Interface for caller of this dialog fragment.
*
* @author Jimmy Shih
*/
public interface EnableSyncCaller {
/**
* Called when enable sync is done.
*/
public void onEnableSyncDone(boolean enable);
}
public static final String ENABLE_SYNC_DIALOG_TAG = "enableSyncDialog";
private EnableSyncCaller caller;
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
try {
caller = (EnableSyncCaller) activity;
} catch (ClassCastException e) {
throw new ClassCastException(
activity.toString() + " must implement " + EnableSyncCaller.class.getSimpleName());
}
}
@Override
protected Dialog createDialog() {
return new AlertDialog.Builder(getActivity()).setMessage(R.string.sync_drive_enable_message)
.setNegativeButton(R.string.generic_no, new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
caller.onEnableSyncDone(false);
}
}).setPositiveButton(R.string.generic_yes, new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
caller.onEnableSyncDone(true);
}
}).setTitle(R.string.sync_drive_enable_title).create();
}
@Override
public void onCancel(DialogInterface arg0) {
caller.onEnableSyncDone(false);
}
}
@@ -0,0 +1,148 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.fragments;
import com.google.android.apps.mytracks.util.EulaUtils;
import com.google.android.maps.mytracks.R;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.DialogInterface;
import android.os.Bundle;
import android.text.SpannableString;
import android.text.method.LinkMovementMethod;
import android.text.util.Linkify;
import android.view.KeyEvent;
import android.widget.TextView;
/**
* A DialogFragment to show EULA.
*
* @author Jimmy Shih
*/
public class EulaDialogFragment extends AbstractMyTracksDialogFragment {
/**
* Interface for caller of this dialog fragment.
*
* @author Jimmy Shih
*/
public interface EulaCaller {
/**
* Called when eula is done.
*/
public void onEulaDone();
}
public static final String EULA_DIALOG_TAG = "eulaDialog";
private static final String KEY_HAS_ACCEPTED = "hasAccepted";
private static final String GOOGLE_URL = "m.google.com";
private static final String KOREAN = "ko";
/**
* Creates a new instance of {@link EulaDialogFragment}.
*
* @param hasAccepted true if the user has accepted the eula.
*/
public static EulaDialogFragment newInstance(boolean hasAccepted) {
Bundle bundle = new Bundle();
bundle.putBoolean(KEY_HAS_ACCEPTED, hasAccepted);
EulaDialogFragment eulaDialogFragment = new EulaDialogFragment();
eulaDialogFragment.setArguments(bundle);
return eulaDialogFragment;
}
private EulaCaller caller;
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
try {
caller = (EulaCaller) activity;
} catch (ClassCastException e) {
throw new ClassCastException(
activity.toString() + " must implement " + EulaCaller.class.getSimpleName());
}
}
@Override
protected Dialog createDialog() {
boolean hasAccepted = getArguments().getBoolean(KEY_HAS_ACCEPTED);
SpannableString message = new SpannableString(getEulaText());
Linkify.addLinks(message, Linkify.WEB_URLS);
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()).setMessage(message)
.setTitle(R.string.menu_help_eula);
if (hasAccepted) {
builder.setPositiveButton(R.string.generic_ok, null);
} else {
builder.setNegativeButton(R.string.eula_decline, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
caller.onEulaDone();
}
}).setOnKeyListener(new DialogInterface.OnKeyListener() {
@Override
public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_SEARCH) {
return true;
}
return false;
}
}).setPositiveButton(R.string.eula_accept, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
EulaUtils.setAcceptedEula(getActivity());
caller.onEulaDone();
}
});
}
return builder.create();
}
@Override
public void onStart() {
super.onStart();
TextView textView = (TextView) getDialog().findViewById(android.R.id.message);
textView.setMovementMethod(LinkMovementMethod.getInstance());
textView.setTextAppearance(getActivity(), R.style.TextSmall);
}
@Override
public void onCancel(DialogInterface arg0) {
caller.onEulaDone();
}
/**
* Gets the EULA text.
*/
private String getEulaText() {
String tos = getString(R.string.eula_date) + "\n\n" + getString(R.string.eula_body, GOOGLE_URL)
+ "\n\n" + getString(R.string.eula_footer, GOOGLE_URL) + "\n\n"
+ getString(R.string.eula_copyright_year);
boolean isKorean = getResources().getConfiguration().locale.getLanguage().equals(KOREAN);
if (isKorean) {
tos += "\n\n" + getString(R.string.eula_korean);
}
return tos;
}
}
@@ -0,0 +1,309 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.fragments;
import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.io.file.TrackFileFormat;
import com.google.android.apps.mytracks.util.AccountUtils;
import com.google.android.apps.mytracks.util.FileUtils;
import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.android.maps.mytracks.R;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.DialogInterface;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.Spinner;
import java.util.ArrayList;
/**
* A DialogFragment to export a track.
*
* @author Jimmy Shih
*/
public class ExportDialogFragment extends AbstractMyTracksDialogFragment {
/**
* Export types.
*
* @author Jimmy Shih
*/
public enum ExportType {
GOOGLE_DRIVE(R.string.export_google_drive),
GOOGLE_MAPS(R.string.export_google_maps),
GOOGLE_FUSION_TABLES(R.string.export_google_fusion_tables),
GOOGLE_SPREADSHEET(R.string.export_google_spreadsheets),
EXTERNAL_STORAGE(R.string.export_external_storage);
final int resId;
ExportType(int resId) {
this.resId = resId;
}
}
/**
* Interface for caller of this dialog fragment.
*
* @author Jimmy Shih
*/
public interface ExportCaller {
/**
* Called when export is done.
*
* @param exportType the export type
* @param trackFileFormat the track file format
* @param account the account
*/
public void onExportDone(
ExportType exportType, TrackFileFormat trackFileFormat, Account account);
}
public static final String EXPORT_DIALOG_TAG = "export";
private static final String KEY_HIDE_DRIVE = "hideDrive";
public static ExportDialogFragment newInstance(boolean hideDrive) {
Bundle bundle = new Bundle();
bundle.putBoolean(KEY_HIDE_DRIVE, hideDrive);
ExportDialogFragment fragment = new ExportDialogFragment();
fragment.setArguments(bundle);
return fragment;
}
private ExportCaller caller;
private Account[] accounts;
private ArrayList<ExportType> exportTypeOptionsList;
// UI elements
private Spinner exportTypeOptions;
private RadioGroup exportGoogleMapsOptions;
private RadioGroup exportGoogleFusionTablesOptions;
private RadioGroup exportExternalStorageOptions;
private Spinner accountSpinner;
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
try {
caller = (ExportCaller) activity;
} catch (ClassCastException e) {
throw new ClassCastException(
activity.toString() + " must implement " + ExportCaller.class.getSimpleName());
}
}
@Override
protected Dialog createDialog() {
FragmentActivity fragmentActivity = getActivity();
accounts = AccountManager.get(fragmentActivity).getAccountsByType(Constants.ACCOUNT_TYPE);
// Get views
View view = fragmentActivity.getLayoutInflater().inflate(R.layout.export, null);
exportTypeOptions = (Spinner) view.findViewById(R.id.export_type_options);
exportGoogleMapsOptions = (RadioGroup) view.findViewById(R.id.export_google_maps_options);
exportGoogleFusionTablesOptions = (RadioGroup) view.findViewById(
R.id.export_google_fusion_tables_options);
exportExternalStorageOptions = (RadioGroup) view.findViewById(
R.id.export_external_storage_options);
accountSpinner = (Spinner) view.findViewById(R.id.export_account);
// Setup exportTypeOptions
setupExportTypeOptions(fragmentActivity);
// Setup exportGoogleMapsOptions
boolean exportGoogleMapsPublic = PreferencesUtils.getBoolean(fragmentActivity,
R.string.export_google_maps_public_key, PreferencesUtils.EXPORT_GOOGLE_MAPS_PUBLIC_DEFAULT);
exportGoogleMapsOptions.check(
exportGoogleMapsPublic ? R.id.export_google_maps_public : R.id.export_google_maps_unlisted);
// Setup exportGoogleFusionTablesOptions
boolean exportGoogleFusionTablesPublic = PreferencesUtils.getBoolean(fragmentActivity,
R.string.export_google_fusion_tables_public_key,
PreferencesUtils.EXPORT_GOOGLE_FUSION_TABLES_PUBLIC_DEFAULT);
exportGoogleFusionTablesOptions.check(
exportGoogleFusionTablesPublic ? R.id.export_google_fusion_tables_public
: R.id.export_google_fusion_tables_private);
// Setup exportExternalStorageOptions
setExternalStorageOption(
(RadioButton) view.findViewById(R.id.export_external_storage_kml), TrackFileFormat.KML);
setExternalStorageOption(
(RadioButton) view.findViewById(R.id.export_external_storage_gpx), TrackFileFormat.GPX);
setExternalStorageOption(
(RadioButton) view.findViewById(R.id.export_external_storage_csv), TrackFileFormat.CSV);
setExternalStorageOption(
(RadioButton) view.findViewById(R.id.export_external_storage_tcx), TrackFileFormat.TCX);
TrackFileFormat trackFileFormat = TrackFileFormat.valueOf(PreferencesUtils.getString(
fragmentActivity, R.string.export_external_storage_format_key,
PreferencesUtils.EXPORT_EXTERNAL_STORAGE_FORMAT_DEFAULT));
exportExternalStorageOptions.check(getExternalStorageFormatId(trackFileFormat));
// Setup accountSpinner
AccountUtils.setupAccountSpinner(fragmentActivity, accountSpinner, accounts);
return new AlertDialog.Builder(fragmentActivity).setNegativeButton(
R.string.generic_cancel, null)
.setPositiveButton(R.string.menu_export, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
FragmentActivity context = getActivity();
ExportType type = exportTypeOptionsList.get(
exportTypeOptions.getSelectedItemPosition());
TrackFileFormat format = null;
PreferencesUtils.setString(context, R.string.export_type_key, type.name());
if (type == ExportType.GOOGLE_MAPS) {
PreferencesUtils.setBoolean(context, R.string.export_google_maps_public_key,
exportGoogleMapsOptions.getCheckedRadioButtonId()
== R.id.export_google_maps_public);
} else if (type == ExportType.GOOGLE_FUSION_TABLES) {
PreferencesUtils.setBoolean(context, R.string.export_google_fusion_tables_public_key,
exportGoogleFusionTablesOptions.getCheckedRadioButtonId()
== R.id.export_google_fusion_tables_public);
} else if (type == ExportType.EXTERNAL_STORAGE) {
format = getTrackFileFormat(exportExternalStorageOptions.getCheckedRadioButtonId());
PreferencesUtils.setString(
context, R.string.export_external_storage_format_key, format.name());
}
Account account;
if (accounts.length == 0) {
account = null;
} else if (accounts.length == 1) {
account = accounts[0];
} else {
account = accounts[accountSpinner.getSelectedItemPosition()];
}
AccountUtils.updateShareTrackAccountPreference(context, account);
caller.onExportDone(type, format, account);
}
}).setTitle(R.string.export_title).setView(view).create();
}
private void setupExportTypeOptions(FragmentActivity fragmentActivity) {
boolean hideDrive = getArguments().getBoolean(KEY_HIDE_DRIVE);
ExportType exportType = ExportType.valueOf(PreferencesUtils.getString(
fragmentActivity, R.string.export_type_key, PreferencesUtils.EXPORT_TYPE_DEFAULT));
if (hideDrive && exportType == ExportType.GOOGLE_DRIVE) {
exportType = ExportType.GOOGLE_MAPS;
}
exportTypeOptionsList = new ArrayList<ExportDialogFragment.ExportType>();
ArrayAdapter<CharSequence> adapter = new ArrayAdapter<CharSequence>(
getActivity(), android.R.layout.simple_spinner_item);
if (accounts.length > 0) {
if (!hideDrive) {
exportTypeOptionsList.add(ExportType.GOOGLE_DRIVE);
}
exportTypeOptionsList.add(ExportType.GOOGLE_MAPS);
exportTypeOptionsList.add(ExportType.GOOGLE_FUSION_TABLES);
exportTypeOptionsList.add(ExportType.GOOGLE_SPREADSHEET);
}
exportTypeOptionsList.add(ExportType.EXTERNAL_STORAGE);
int selection = 0;
for (int i = 0; i < exportTypeOptionsList.size(); i++) {
ExportType type = exportTypeOptionsList.get(i);
adapter.add(getString(type.resId));
if (type == exportType) {
selection = i;
}
}
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
exportTypeOptions.setAdapter(adapter);
exportTypeOptions.setSelection(selection);
exportTypeOptions.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
ExportType type = exportTypeOptionsList.get(position);
exportGoogleMapsOptions.setVisibility(
type == ExportType.GOOGLE_MAPS ? View.VISIBLE : View.GONE);
exportGoogleFusionTablesOptions.setVisibility(
type == ExportType.GOOGLE_FUSION_TABLES ? View.VISIBLE : View.GONE);
exportExternalStorageOptions.setVisibility(
type == ExportType.EXTERNAL_STORAGE ? View.VISIBLE : View.GONE);
accountSpinner.setVisibility(
accounts.length > 1 && type != ExportType.EXTERNAL_STORAGE ? View.VISIBLE : View.GONE);
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
// Safely ignore
}
});
}
/**
* Sets an external storage option.
*
* @param radioButton the radio button
* @param trackFileFormat the track file format
*/
private void setExternalStorageOption(RadioButton radioButton, TrackFileFormat trackFileFormat) {
radioButton.setText(getString(R.string.export_external_storage_option, trackFileFormat.name(),
FileUtils.getPathDisplayName(trackFileFormat.getExtension())));
}
/**
* Gets the external storage format id from a track file format.
*
* @param trackFileFormat the track file format
*/
private int getExternalStorageFormatId(TrackFileFormat trackFileFormat) {
switch (trackFileFormat) {
case KML:
return R.id.export_external_storage_kml;
case GPX:
return R.id.export_external_storage_gpx;
case CSV:
return R.id.export_external_storage_csv;
default:
return R.id.export_external_storage_tcx;
}
}
/**
* Gets the track file format from an external storage format id.
*
* @param externalStorageFormatId the external storage format id
*/
private TrackFileFormat getTrackFileFormat(int externalStorageFormatId) {
switch (externalStorageFormatId) {
case R.id.export_external_storage_kml:
return TrackFileFormat.KML;
case R.id.export_external_storage_gpx:
return TrackFileFormat.GPX;
case R.id.export_external_storage_csv:
return TrackFileFormat.CSV;
default:
return TrackFileFormat.TCX;
}
}
}
@@ -0,0 +1,105 @@
/*
* Copyright 2013 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.fragments;
import com.google.android.apps.mytracks.io.file.TrackFileFormat;
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.Dialog;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.os.Bundle;
/**
* A DialogFragment to select a file type, gpx, kml, etc.
*
* @author Jimmy Shih
*/
public class FileTypeDialogFragment extends AbstractMyTracksDialogFragment {
/**
* Interface for caller of this dialog fragment.
*
* @author Jimmy Shih
*/
public interface FileTypeCaller {
/**
* Called when file type selection is done.
*/
public void onFileTypeDone(int menuId, TrackFileFormat trackFileFormat);
}
public static final String FILE_TYPE_DIALOG_TAG = "fileType";
private static final String KEY_MENU_ID = "menuId";
private static final String KEY_TITLE_ID = "titleId";
private static final String KEY_OPTION_ID = "optionId";
private static final String KEY_SIZE = "size";
public static FileTypeDialogFragment newInstance(
int menuId, int titleId, int optionId, int size) {
Bundle bundle = new Bundle();
bundle.putInt(KEY_MENU_ID, menuId);
bundle.putInt(KEY_TITLE_ID, titleId);
bundle.putInt(KEY_OPTION_ID, optionId);
bundle.putInt(KEY_SIZE, size);
FileTypeDialogFragment fileTypeDialogFragment = new FileTypeDialogFragment();
fileTypeDialogFragment.setArguments(bundle);
return fileTypeDialogFragment;
}
private FileTypeCaller caller;
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
try {
caller = (FileTypeCaller) activity;
} catch (ClassCastException e) {
throw new ClassCastException(
activity.toString() + " must implement " + FileTypeCaller.class.getSimpleName());
}
}
@Override
protected Dialog createDialog() {
int size = getArguments().getInt(KEY_SIZE);
int optionId = getArguments().getInt(KEY_OPTION_ID);
final int titleId = getArguments().getInt(KEY_TITLE_ID);
final int menuId = getArguments().getInt(KEY_MENU_ID);
String[] choices = new String[size];
TrackFileFormat[] trackFileFormats = TrackFileFormat.values();
for (int i = 0; i < choices.length; i++) {
TrackFileFormat trackFileFormat = trackFileFormats[i];
choices[i] = getString(optionId, trackFileFormat.name(),
FileUtils.getPathDisplayName(trackFileFormat.getExtension()));
}
return new AlertDialog.Builder(getActivity()).setNegativeButton(R.string.generic_cancel, null)
.setPositiveButton(R.string.generic_ok, new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
int position = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
caller.onFileTypeDone(menuId, TrackFileFormat.values()[position]);
}
}).setSingleChoiceItems(choices, 0, null).setTitle(titleId).create();
}
}
@@ -0,0 +1,106 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.fragments;
import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.android.apps.mytracks.util.StringUtils;
import com.google.android.maps.mytracks.R;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.DialogInterface;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
/**
* A DialogFragment to configure frequency.
*
* @author Jimmy Shih
*/
public class FrequencyDialogFragment extends AbstractMyTracksDialogFragment {
public static final String FREQUENCY_DIALOG_TAG = "frequencyDialog";
private static final String KEY_PREFERENCE_ID = "preferenceId";
private static final String KEY_DEFAULT_VALUE = "defaultValue";
private static final String KEY_TITLE_ID = "titleId";
public static FrequencyDialogFragment newInstance(
int preferenceId, int defaultValue, int titleId) {
Bundle bundle = new Bundle();
bundle.putInt(KEY_PREFERENCE_ID, preferenceId);
bundle.putInt(KEY_DEFAULT_VALUE, defaultValue);
bundle.putInt(KEY_TITLE_ID, titleId);
FrequencyDialogFragment frequencyDialogFragment = new FrequencyDialogFragment();
frequencyDialogFragment.setArguments(bundle);
return frequencyDialogFragment;
}
@Override
protected Dialog createDialog() {
FragmentActivity fragmentActivity = getActivity();
final int preferenceId = getArguments().getInt(KEY_PREFERENCE_ID);
int defaultValue = getArguments().getInt(KEY_DEFAULT_VALUE);
int titleId = getArguments().getInt(KEY_TITLE_ID);
int frequencyValue = PreferencesUtils.getInt(fragmentActivity, preferenceId, defaultValue);
return new AlertDialog.Builder(fragmentActivity).setPositiveButton(
R.string.generic_ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
int listIndex = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
PreferencesUtils.setInt(getActivity(), preferenceId, getFrequencyValue(listIndex));
}
}).setSingleChoiceItems(
getFrequencyDisplayOptions(fragmentActivity), getListIndex(frequencyValue), null)
.setTitle(titleId).create();
}
/**
* Gets the frequency display options.
*/
private String[] getFrequencyDisplayOptions(FragmentActivity fragmentActivity) {
boolean metricUnits = PreferencesUtils.isMetricUnits(fragmentActivity);
return StringUtils.getFrequencyOptions(fragmentActivity, metricUnits);
}
/**
* Gets the list index for a frequency value. Returns 0 if the value is not on
* the list.
*/
private int getListIndex(int frequencyValue) {
String[] values = getResources().getStringArray(R.array.frequency_values);
for (int i = 0; i < values.length; i++) {
if (frequencyValue == Integer.parseInt(values[i])) {
return i;
}
}
return 0;
}
/**
* Gets the frequency value from a list index.
*
* @param listIndex the list index
*/
private int getFrequencyValue(int listIndex) {
String[] values = getResources().getStringArray(R.array.frequency_values);
return Integer.parseInt(values[listIndex]);
}
}
@@ -0,0 +1,58 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.fragments;
import com.google.android.apps.mytracks.util.GoogleEarthUtils;
import com.google.android.maps.mytracks.R;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.ActivityNotFoundException;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.widget.Toast;
/**
* A DialogFragment to install Google Earth.
*
* @author Jimmy Shih
*/
public class InstallEarthDialogFragment extends AbstractMyTracksDialogFragment {
public static final String INSTALL_EARTH_DIALOG_TAG = "installEarthDialog";
@Override
protected Dialog createDialog() {
return new AlertDialog.Builder(getActivity()).setMessage(
R.string.track_detail_install_earth_message).setNegativeButton(R.string.generic_no, null)
.setPositiveButton(R.string.generic_yes, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Intent intent = new Intent().setData(
Uri.parse(GoogleEarthUtils.GOOGLE_EARTH_MARKET_URL));
try {
startActivity(intent);
} catch (ActivityNotFoundException e) {
Toast.makeText(
getActivity(), R.string.track_detail_install_earth_error, Toast.LENGTH_LONG)
.show();
}
}
}).setTitle(R.string.track_detail_install_earth_title).create();
}
}
@@ -0,0 +1,71 @@
/*
* Copyright 2014 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.fragments;
import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.maps.mytracks.R;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
/**
* A DialogFragment to select a map layer.
*
* @author Jimmy Shih
*/
public class MapLayerDialogFragment extends AbstractMyTracksDialogFragment {
public static final String MAP_LAYER_DIALOG_TAG = "mapLayer";
private static final int[] LAYERS = { R.string.menu_map, R.string.menu_satellite,
R.string.menu_satellite_with_streets, R.string.menu_terrain };
private static final int[] MAP_TYPES = { GoogleMap.MAP_TYPE_NORMAL, GoogleMap.MAP_TYPE_SATELLITE,
GoogleMap.MAP_TYPE_HYBRID, GoogleMap.MAP_TYPE_TERRAIN };
@Override
protected Dialog createDialog() {
String[] choices = new String[LAYERS.length];
for (int i = 0; i < LAYERS.length; i++) {
choices[i] = getString(LAYERS[i]);
}
int mapType = PreferencesUtils.getInt(
getActivity(), R.string.map_type_key, PreferencesUtils.MAP_TYPE_DEFAUlT);
return new AlertDialog.Builder(getActivity()).setNegativeButton(R.string.generic_cancel, null)
.setPositiveButton(R.string.generic_ok, new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
int position = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
PreferencesUtils.setInt(getActivity(), R.string.map_type_key, MAP_TYPES[position]);
}
}).setSingleChoiceItems(choices, getPositionFromMapType(mapType), null)
.setTitle(R.string.menu_map_layer).create();
}
private int getPositionFromMapType(int mapType) {
for (int i = 0; i < MAP_TYPES.length; i++) {
if (MAP_TYPES[i] == mapType) {
return i;
}
}
return 0;
}
}
@@ -0,0 +1,326 @@
/*
* Copyright 2013 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.fragments;
import com.google.android.apps.mytracks.MarkerEditActivity;
import com.google.android.apps.mytracks.TrackDetailActivity;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.content.Waypoint.WaypointType;
import com.google.android.apps.mytracks.util.CalorieUtils;
import com.google.android.apps.mytracks.util.CalorieUtils.ActivityType;
import com.google.android.apps.mytracks.util.IntentUtils;
import com.google.android.apps.mytracks.util.ListItemUtils;
import com.google.android.apps.mytracks.util.PhotoUtils;
import com.google.android.apps.mytracks.util.StatsUtils;
import com.google.android.apps.mytracks.util.StringUtils;
import com.google.android.apps.mytracks.util.TrackIconUtils;
import com.google.android.maps.mytracks.R;
import android.content.Intent;
import android.location.Location;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity;
import android.util.Log;
import android.view.Display;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.view.animation.Animation.AnimationListener;
import android.view.animation.AnimationUtils;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
/**
* A fragment to show marker details.
*
* @author Jimmy Shih
*/
public class MarkerDetailFragment extends Fragment {
public static MarkerDetailFragment newInstance(long markerId, String title) {
Bundle bundle = new Bundle();
bundle.putLong(KEY_MARKER_ID, markerId);
bundle.putString(KEY_TITLE, title);
MarkerDetailFragment fragment = new MarkerDetailFragment();
fragment.setArguments(bundle);
return fragment;
}
private static final String TAG = MarkerDetailFragment.class.getSimpleName();
private static final String KEY_MARKER_ID = "markerId";
private static final String KEY_TITLE = "title";
private static final long HIDE_TEXT_DELAY = 4000L; // 4 seconds
private MyTracksProviderUtils myTracksProviderUtils;
private Handler handler;
private ImageView photo;
private ImageView textGradient;
private LinearLayout waypointInfo;
private Waypoint waypoint;
private Runnable hideText = new Runnable() {
@Override
public void run() {
Animation animation = AnimationUtils.loadAnimation(getActivity(), R.anim.fadeout);
animation.setAnimationListener(new AnimationListener() {
@Override
public void onAnimationStart(Animation anim) {}
@Override
public void onAnimationRepeat(Animation anim) {}
@Override
public void onAnimationEnd(Animation anim) {
textGradient.setVisibility(View.GONE);
waypointInfo.setVisibility(View.GONE);
}
});
textGradient.startAnimation(animation);
waypointInfo.startAnimation(animation);
}
};
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
long markerId = getArguments().getLong(KEY_MARKER_ID);
if (markerId == -1L) {
Log.d(TAG, "invalid marker id");
getFragmentManager().popBackStack();
return;
}
myTracksProviderUtils = MyTracksProviderUtils.Factory.get(getActivity());
handler = new Handler();
setHasOptionsMenu(true);
}
@Override
public View onCreateView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.marker_detail_fragment, container, false);
photo = (ImageView) view.findViewById(R.id.marker_detail_waypoint_photo);
textGradient = (ImageView) view.findViewById(R.id.marker_detail_waypoint_text_gradient);
waypointInfo = (LinearLayout) view.findViewById(R.id.marker_detail_waypoint_info);
photo.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
handler.removeCallbacks(hideText);
int visibility = waypointInfo.getVisibility() == View.GONE ? View.VISIBLE : View.GONE;
textGradient.setVisibility(visibility);
waypointInfo.setVisibility(visibility);
if (visibility == View.VISIBLE) {
handler.postDelayed(hideText, HIDE_TEXT_DELAY);
}
}
});
return view;
};
@Override
public void onResume() {
super.onResume();
// Need to update the waypoint in case returning after an edit
updateWaypoint(true);
updateUi();
}
@Override
public void onPause() {
super.onPause();
handler.removeCallbacks(hideText);
}
@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
super.setUserVisibleHint(isVisibleToUser);
if (isVisibleToUser) {
getActivity().setTitle(getArguments().getString(KEY_TITLE));
}
}
@Override
public void setMenuVisibility(boolean menuVisible) {
super.setMenuVisibility(menuVisible);
/*
* View pager caches the neighboring fragments in the resumed state. If
* becoming visible from the resumed state, update the UI to display the
* text above the image.
*/
if (isResumed()) {
if (menuVisible) {
updateUi();
} else {
handler.removeCallbacks(hideText);
}
}
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.marker_detail, menu);
updateWaypoint(false);
Track track = myTracksProviderUtils.getTrack(waypoint.getTrackId());
boolean isSharedWithMe = track != null ? track.isSharedWithMe() : true;
menu.findItem(R.id.marker_detail_edit).setVisible(!isSharedWithMe);
menu.findItem(R.id.marker_detail_delete).setVisible(!isSharedWithMe);
String photoUrl = waypoint.getPhotoUrl();
boolean hasPhoto = photoUrl != null && !photoUrl.equals("");
menu.findItem(R.id.marker_detail_view_photo).setVisible(hasPhoto);
TrackIconUtils.setMenuIconColor(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
long markerId = getArguments().getLong(KEY_MARKER_ID);
FragmentActivity fragmentActivity = getActivity();
Intent intent;
switch (item.getItemId()) {
case R.id.marker_detail_show_on_map:
intent = IntentUtils.newIntent(fragmentActivity, TrackDetailActivity.class)
.putExtra(TrackDetailActivity.EXTRA_MARKER_ID, markerId);
startActivity(intent);
return true;
case R.id.marker_detail_edit:
intent = IntentUtils.newIntent(fragmentActivity, MarkerEditActivity.class)
.putExtra(MarkerEditActivity.EXTRA_MARKER_ID, markerId);
startActivity(intent);
return true;
case R.id.marker_detail_delete:
DeleteMarkerDialogFragment.newInstance(new long[] { markerId })
.show(getChildFragmentManager(), DeleteMarkerDialogFragment.DELETE_MARKER_DIALOG_TAG);
return true;
case R.id.marker_detail_view_photo:
intent = new Intent();
intent.setAction(Intent.ACTION_VIEW);
intent.setDataAndType(Uri.parse(waypoint.getPhotoUrl()), "image/*");
startActivity(intent);
return true;
default:
return super.onOptionsItemSelected(item);
}
}
/**
* Updates the waypoint.
*
* @param refresh true to always update
*/
private void updateWaypoint(boolean refresh) {
if (refresh || waypoint == null) {
waypoint = myTracksProviderUtils.getWaypoint(getArguments().getLong(KEY_MARKER_ID));
if (waypoint == null) {
Log.d(TAG, "waypoint is null");
getFragmentManager().popBackStack();
}
}
}
/**
* Updates the UI.
*/
@SuppressWarnings("deprecation")
private void updateUi() {
View waypointView = getView().findViewById(R.id.marker_detail_waypoint);
View statisticsView = getView().findViewById(R.id.marker_detail_statistics);
boolean isWaypoint = waypoint.getType() == WaypointType.WAYPOINT;
waypointView.setVisibility(isWaypoint ? View.VISIBLE : View.GONE);
statisticsView.setVisibility(isWaypoint ? View.GONE : View.VISIBLE);
if (isWaypoint) {
String photoUrl = waypoint.getPhotoUrl();
boolean hasPhoto = photoUrl != null && !photoUrl.equals("");
photo.setVisibility(hasPhoto ? View.VISIBLE : View.GONE);
textGradient.setVisibility(hasPhoto ? View.VISIBLE : View.GONE);
waypointInfo.setVisibility(View.VISIBLE);
if (hasPhoto) {
handler.removeCallbacks(hideText);
Display defaultDisplay = getActivity().getWindowManager().getDefaultDisplay();
PhotoUtils.setImageVew(photo, Uri.parse(photoUrl), defaultDisplay.getWidth(),
defaultDisplay.getHeight(), true);
handler.postDelayed(hideText, HIDE_TEXT_DELAY);
}
setName(R.id.marker_detail_waypoint_name, hasPhoto);
TextView category = (TextView) getView().findViewById(R.id.marker_detail_waypoint_category);
ListItemUtils.setTextView(
getActivity(), category, StringUtils.getCategory(waypoint.getCategory()), hasPhoto);
TextView description = (TextView) getView()
.findViewById(R.id.marker_detail_waypoint_description);
ListItemUtils.setTextView(getActivity(), description, waypoint.getDescription(), hasPhoto);
setLocation(R.id.marker_detail_waypoint_location, hasPhoto);
} else {
setName(R.id.marker_detail_statistics_name, false);
setLocation(R.id.marker_detail_statistics_location, false);
Track track = myTracksProviderUtils.getTrack(waypoint.getTrackId());
ActivityType activityType = track != null ? CalorieUtils.getActivityType(
getActivity(), track.getCategory())
: ActivityType.INVALID;
StatsUtils.setTripStatisticsValues(
getActivity(), null, getView(), waypoint.getTripStatistics(), activityType, null);
StatsUtils.setLocationValues(getActivity(), null, getView(), waypoint.getLocation(), false);
}
}
private void setName(int resId, boolean addShadow) {
TextView textView = (TextView) getView().findViewById(resId);
ListItemUtils.setTextView(getActivity(), textView, waypoint.getName(), addShadow);
}
private void setLocation(int resId, boolean addShadow) {
TextView textView = (TextView) getView().findViewById(resId);
Location location = waypoint.getLocation();
String value;
if (location == null) {
value = null;
} else {
value = "[" + getString(R.string.stats_latitude) + " "
+ StringUtils.formatCoordinate(location.getLatitude()) + ", "
+ getString(R.string.stats_longitude) + " "
+ StringUtils.formatCoordinate(location.getLongitude()) + "]";
}
ListItemUtils.setTextView(getActivity(), textView, value, addShadow);
}
}
@@ -0,0 +1,764 @@
/*
* 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.fragments;
import com.google.android.apps.mytracks.MapOverlay;
import com.google.android.apps.mytracks.MarkerDetailActivity;
import com.google.android.apps.mytracks.TrackDetailActivity;
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.TrackDataHub;
import com.google.android.apps.mytracks.content.TrackDataListener;
import com.google.android.apps.mytracks.content.TrackDataType;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.services.MyTracksLocationManager;
import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import com.google.android.apps.mytracks.util.GoogleLocationUtils;
import com.google.android.apps.mytracks.util.IntentUtils;
import com.google.android.apps.mytracks.util.LocationUtils;
import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.android.apps.mytracks.util.TrackIconUtils;
import com.google.android.gms.location.LocationListener;
import com.google.android.gms.maps.CameraUpdate;
import com.google.android.gms.maps.CameraUpdateFactory;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.GoogleMap.OnCameraChangeListener;
import com.google.android.gms.maps.GoogleMap.OnMarkerClickListener;
import com.google.android.gms.maps.LocationSource;
import com.google.android.gms.maps.LocationSource.OnLocationChangedListener;
import com.google.android.gms.maps.SupportMapFragment;
import com.google.android.gms.maps.model.CameraPosition;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.LatLngBounds;
import com.google.android.gms.maps.model.Marker;
import com.google.android.gms.maps.model.Polyline;
import com.google.android.maps.mytracks.R;
import android.content.Context;
import android.content.Intent;
import android.location.Location;
import android.os.Bundle;
import android.os.Looper;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.view.ViewTreeObserver.OnGlobalLayoutListener;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;
import java.util.ArrayList;
import java.util.EnumSet;
/**
* A fragment to display map to the user.
*
* @author Leif Hendrik Wilden
* @author Rodrigo Damazio
*/
public class MyTracksMapFragment extends SupportMapFragment implements TrackDataListener {
public static final String MAP_FRAGMENT_TAG = "mapFragment";
private static final String CURRENT_LOCATION_KEY = "current_location_key";
private static final String
KEEP_CURRENT_LOCATION_VISIBLE_KEY = "keep_current_location_visible_key";
private static final float DEFAULT_ZOOM_LEVEL = 18f;
// Google's latitude and longitude
private static final double DEFAULT_LATITUDE = 37.423;
private static final double DEFAULT_LONGITUDE = -122.084;
private static final int MAP_VIEW_PADDING = 32;
// States from TrackDetailActivity, set in onResume
private TrackDataHub trackDataHub;
// Current location
private Location currentLocation;
private Location lastTrackPoint;
private int recordingGpsAccuracy = PreferencesUtils.RECORDING_GPS_ACCURACY_DEFAULT;
/**
* True to continue keeping the current location visible on the screen.
* <p>
* Set to true when <br>
* 1. user clicks on the my location button <br>
* 2. first location during a recording <br>
* Set to false when <br>
* 1. showing a marker <br>
* 2. user manually zooms/pans
*/
private boolean keepCurrentLocationVisible;
private MyTracksLocationManager myTracksLocationManager;
// LocationListener for periodic location request
private LocationListener locationListener;
private OnLocationChangedListener onLocationChangedListener;
// Current track
private Track currentTrack;
// Current paths
private ArrayList<Polyline> paths = new ArrayList<Polyline>();
boolean reloadPaths = true;
// UI elements
private GoogleMap googleMap;
private MapOverlay mapOverlay;
private View mapView;
private ImageButton myLocationImageButton;
private TextView messageTextView;
@Override
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
setHasOptionsMenu(true);
mapOverlay = new MapOverlay(getActivity());
}
@Override
public View onCreateView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
mapView = super.onCreateView(inflater, container, savedInstanceState);
View layout = inflater.inflate(R.layout.map, container, false);
RelativeLayout mapContainer = (RelativeLayout) layout.findViewById(R.id.map_container);
mapContainer.addView(mapView, 0);
/*
* For Froyo (2.2) and Gingerbread (2.3), need a transparent FrameLayout on
* top for view pager to work correctly.
*/
FrameLayout frameLayout = new FrameLayout(getActivity());
frameLayout.setBackgroundColor(getResources().getColor(android.R.color.transparent));
mapContainer.addView(frameLayout,
new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
myLocationImageButton = (ImageButton) layout.findViewById(R.id.map_my_location);
myLocationImageButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (!myTracksLocationManager.isGpsProviderEnabled()) {
String message = GoogleLocationUtils.getGpsDisabledMyLocationMessage(getActivity());
Toast.makeText(getActivity(), message, Toast.LENGTH_LONG).show();
} else {
keepCurrentLocationVisible = true;
if (locationListener != null) {
myTracksLocationManager.removeLocationUpdates(locationListener);
locationListener = null;
}
if (isSelectedTrackRecording()) {
myTracksLocationManager.requestLastLocation(new LocationListener() {
@Override
public void onLocationChanged(Location location) {
if (isResumed()) {
setCurrentLocation(location);
updateCurrentLocation(true);
}
}
});
} else {
locationListener = new LocationListener() {
@Override
public void onLocationChanged(Location location) {
if (isResumed()) {
boolean isFirst = setCurrentLocation(location);
updateCurrentLocation(isFirst);
}
}
};
/*
* Set currentLocation to null to cause the first requested location
* to force zoom to the default level.
*/
currentLocation = null;
myTracksLocationManager.requestLocationUpdates(0, 0f, locationListener);
}
}
}
});
messageTextView = (TextView) layout.findViewById(R.id.map_message);
return layout;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (savedInstanceState != null) {
keepCurrentLocationVisible = savedInstanceState.getBoolean(
KEEP_CURRENT_LOCATION_VISIBLE_KEY, false);
if (keepCurrentLocationVisible) {
Location location = (Location) savedInstanceState.getParcelable(CURRENT_LOCATION_KEY);
if (location != null) {
setCurrentLocation(location);
}
}
}
/*
* At this point, after onCreateView, getMap will not return null and we can
* initialize googleMap. However, onActivityCreated can be called multiple
* times, e.g., when the user switches tabs. With
* GoogleMapOptions.useViewLifecycleInFragment == false, googleMap lifecycle
* is tied to the fragment lifecycle and the same googleMap object is
* returned in getMap. Thus we only need to initialize googleMap once, when
* it is null.
*/
if (googleMap == null) {
googleMap = getMap();
googleMap.setMyLocationEnabled(true);
/*
* My Tracks needs to handle the onClick event when the my location button
* is clicked. Currently, the API doesn't allow handling onClick event,
* thus hiding the default my location button and providing our own.
*/
googleMap.getUiSettings().setMyLocationButtonEnabled(false);
googleMap.setIndoorEnabled(true);
googleMap.setOnMarkerClickListener(new OnMarkerClickListener() {
@Override
public boolean onMarkerClick(Marker marker) {
if (isResumed()) {
String title = marker.getTitle();
if (title != null && title.length() > 0) {
long id = Long.valueOf(title);
Context context = getActivity();
Intent intent = IntentUtils.newIntent(context, MarkerDetailActivity.class)
.putExtra(MarkerDetailActivity.EXTRA_MARKER_ID, id);
context.startActivity(intent);
}
}
return true;
}
});
googleMap.setLocationSource(new LocationSource() {
@Override
public void activate(OnLocationChangedListener listener) {
onLocationChangedListener = listener;
}
@Override
public void deactivate() {
onLocationChangedListener = null;
}
});
googleMap.setOnCameraChangeListener(new OnCameraChangeListener() {
@Override
public void onCameraChange(CameraPosition cameraPosition) {
if (isResumed() && keepCurrentLocationVisible && currentLocation != null
&& !isLocationVisible(currentLocation)) {
keepCurrentLocationVisible = false;
}
}
});
}
}
@Override
public void onResume() {
super.onResume();
// First obtain the states from TrackDetailActivity
long trackId = ((TrackDetailActivity) getActivity()).getTrackId();
long markerId = ((TrackDetailActivity) getActivity()).getMarkerId();
resumeTrackDataHub();
myTracksLocationManager = new MyTracksLocationManager(getActivity(), Looper.myLooper(), true);
boolean isGpsProviderEnabled = myTracksLocationManager.isGpsProviderEnabled();
if (googleMap != null) {
// Disable my location if gps is disabled
googleMap.setMyLocationEnabled(isGpsProviderEnabled);
}
// setWarningMessage depends on resumeTrackDataHub being invoked beforehand
setWarningMessage(isGpsProviderEnabled);
currentTrack = MyTracksProviderUtils.Factory.get(getActivity()).getTrack(trackId);
mapOverlay.setShowEndMarker(!isSelectedTrackRecording());
if (markerId != -1L) {
showMarker(markerId);
} else {
if (keepCurrentLocationVisible && currentLocation != null && isSelectedTrackRecording()) {
updateCurrentLocation(true);
} else {
/*
* Clear the current location in case the current location is no longer
* being updated continuously.
*/
if (onLocationChangedListener != null) {
onLocationChangedListener.onLocationChanged(new Location(""));
}
showTrack();
}
}
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBoolean(KEEP_CURRENT_LOCATION_VISIBLE_KEY, keepCurrentLocationVisible);
if (currentLocation != null) {
/*
* currentLocation is a MyTracksLocation object, which cannot be
* unmarshalled. Thus creating a Location object before placing it in the
* bundle.
*/
outState.putParcelable(CURRENT_LOCATION_KEY, new Location(currentLocation));
}
}
@Override
public void onPause() {
super.onPause();
pauseTrackDataHub();
if (locationListener != null) {
myTracksLocationManager.removeLocationUpdates(locationListener);
locationListener = null;
}
myTracksLocationManager.close();
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflator) {
menuInflator.inflate(R.menu.map, menu);
TrackIconUtils.setMenuIconColor(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem menuItem) {
if (menuItem.getItemId() == R.id.menu_map_layer) {
((TrackDetailActivity) getActivity()).showMapLayerDialog();
return true;
}
return super.onOptionsItemSelected(menuItem);
}
@Override
public void onTrackUpdated(Track track) {
currentTrack = track;
}
@Override
public void clearTrackPoints() {
lastTrackPoint = null;
if (isResumed()) {
mapOverlay.clearPoints();
reloadPaths = true;
}
}
@Override
public void onSampledInTrackPoint(final Location location) {
lastTrackPoint = location;
if (isResumed()) {
mapOverlay.addLocation(location);
}
}
@Override
public void onSampledOutTrackPoint(Location location) {
lastTrackPoint = location;
}
@Override
public void onSegmentSplit(Location location) {
if (isResumed()) {
mapOverlay.addSegmentSplit();
}
}
@Override
public void onNewTrackPointsDone() {
if (isResumed()) {
getActivity().runOnUiThread(new Runnable() {
public void run() {
if (isResumed() && googleMap != null && currentTrack != null) {
boolean hasStartMarker = mapOverlay.update(
googleMap, paths, currentTrack.getTripStatistics(), reloadPaths);
/*
* If has the start marker, then don't need to reload the paths each
* time
*/
if (hasStartMarker) {
reloadPaths = false;
}
if (lastTrackPoint != null && isSelectedTrackRecording()) {
boolean firstLocation = setCurrentLocation(lastTrackPoint);
if (firstLocation) {
keepCurrentLocationVisible = true;
}
updateCurrentLocation(firstLocation);
setWarningMessage(true);
}
}
}
});
}
}
@Override
public void clearWaypoints() {
if (isResumed()) {
mapOverlay.clearWaypoints();
}
}
@Override
public void onNewWaypoint(Waypoint waypoint) {
if (isResumed() && waypoint != null && LocationUtils.isValidLocation(waypoint.getLocation())) {
mapOverlay.addWaypoint(waypoint);
}
}
@Override
public void onNewWaypointsDone() {
if (isResumed()) {
getActivity().runOnUiThread(new Runnable() {
public void run() {
if (isResumed() && googleMap != null && currentTrack != null) {
mapOverlay.update(googleMap, paths, currentTrack.getTripStatistics(), true);
}
}
});
}
}
@Override
public boolean onMetricUnitsChanged(boolean metric) {
// We don't care.
return false;
}
@Override
public boolean onReportSpeedChanged(boolean reportSpeed) {
// We don't care.
return false;
}
@Override
public boolean onRecordingGpsAccuracy(int newValue) {
recordingGpsAccuracy = newValue;
return false;
}
@Override
public boolean onRecordingDistanceIntervalChanged(int minRecordingDistance) {
// We don't care.
return false;
}
@Override
public boolean onMapTypeChanged(final int mapType) {
if (isResumed()) {
getActivity().runOnUiThread(new Runnable() {
public void run() {
if (isResumed() && googleMap != null) {
googleMap.setMapType(mapType);
}
}
});
}
return false;
}
/**
* Resumes the trackDataHub. Needs to be synchronized because the trackDataHub
* can be accessed by multiple threads.
*/
private synchronized void resumeTrackDataHub() {
trackDataHub = ((TrackDetailActivity) getActivity()).getTrackDataHub();
trackDataHub.registerTrackDataListener(this, EnumSet.of(TrackDataType.TRACKS_TABLE,
TrackDataType.WAYPOINTS_TABLE, TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE,
TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE, TrackDataType.PREFERENCE));
}
/**
* Pauses the trackDataHub. Needs to be synchronized because the trackDataHub
* can be accessed by multiple threads.
*/
private synchronized void pauseTrackDataHub() {
if (trackDataHub != null) {
trackDataHub.unregisterTrackDataListener(this);
}
trackDataHub = null;
}
/**
* Returns true if the selected track is recording. Needs to be synchronized
* because the trackDataHub can be accessed by multiple threads.
*/
private synchronized boolean isSelectedTrackRecording() {
return trackDataHub != null && trackDataHub.isSelectedTrackRecording();
}
/**
* Sets the current location.
*
* @param location the location
* @return true if this is the first location
*/
private boolean setCurrentLocation(Location location) {
boolean isFirst = false;
if (currentLocation == null && location != null) {
isFirst = true;
}
currentLocation = location;
return isFirst;
}
/**
* Updates the current location.
*
* @param forceZoom true to force zoom to the current location regardless of
* the keepCurrentLocationVisible policy
*/
private void updateCurrentLocation(final boolean forceZoom) {
getActivity().runOnUiThread(new Runnable() {
public void run() {
if (!isResumed() || googleMap == null || onLocationChangedListener == null
|| currentLocation == null) {
return;
}
onLocationChangedListener.onLocationChanged(currentLocation);
if (forceZoom || (keepCurrentLocationVisible && !isLocationVisible(currentLocation))) {
LatLng latLng = new LatLng(currentLocation.getLatitude(), currentLocation.getLongitude());
CameraUpdate cameraUpdate = forceZoom ? CameraUpdateFactory.newLatLngZoom(
latLng, DEFAULT_ZOOM_LEVEL)
: CameraUpdateFactory.newLatLng(latLng);
googleMap.animateCamera(cameraUpdate);
}
};
});
}
/**
* Shows the current track by moving the camera over the track.
*/
private void showTrack() {
if (googleMap == null || currentTrack == null) {
return;
}
if (currentTrack.getNumberOfPoints() < 2) {
googleMap.moveCamera(
CameraUpdateFactory.newLatLngZoom(getDefaultLatLng(), googleMap.getMinZoomLevel()));
return;
}
if (mapView == null) {
return;
}
if (mapView.getWidth() == 0 || mapView.getHeight() == 0) {
if (mapView.getViewTreeObserver().isAlive()) {
mapView.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
if (!isResumed()) {
return;
}
ApiAdapterFactory.getApiAdapter()
.removeGlobalLayoutListener(mapView.getViewTreeObserver(), this);
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
if (isResumed()) {
moveCameraOverTrack();
}
}
});
}
});
}
return;
}
moveCameraOverTrack();
}
/**
* Moves the camera over the current track.
*/
private void moveCameraOverTrack() {
/**
* Check all the required variables.
*/
if (googleMap == null || currentTrack == null || currentTrack.getNumberOfPoints() < 2
|| mapView == null || mapView.getWidth() == 0 || mapView.getHeight() == 0) {
return;
}
TripStatistics tripStatistics = currentTrack.getTripStatistics();
int latitudeSpanE6 = tripStatistics.getTop() - tripStatistics.getBottom();
int longitudeSpanE6 = tripStatistics.getRight() - tripStatistics.getLeft();
if (latitudeSpanE6 > 0 && latitudeSpanE6 < 180E6 && longitudeSpanE6 > 0
&& longitudeSpanE6 < 360E6) {
LatLng southWest = new LatLng(
tripStatistics.getBottomDegrees(), tripStatistics.getLeftDegrees());
LatLng northEast = new LatLng(
tripStatistics.getTopDegrees(), tripStatistics.getRightDegrees());
LatLngBounds bounds = LatLngBounds.builder().include(southWest).include(northEast).build();
/**
* Note cannot call CameraUpdateFactory.newLatLngBounds(LatLngBounds
* bounds, int padding) if the map view has not undergone layout. Thus
* calling CameraUpdateFactory.newLatLngBounds(LatLngBounds bounds, int
* width, int height, int padding) after making sure that mapView is valid
* in the above code.
*/
CameraUpdate cameraUpdate = CameraUpdateFactory.newLatLngBounds(
bounds, mapView.getWidth(), mapView.getHeight(), MAP_VIEW_PADDING);
googleMap.moveCamera(cameraUpdate);
}
}
/**
* Shows a marker by moving the camera over the marker.
*
* @param id the marker id
*/
private void showMarker(final long id) {
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
if (!isResumed() || googleMap == null) {
return;
}
MyTracksProviderUtils MyTracksProviderUtils = Factory.get(getActivity());
Waypoint waypoint = MyTracksProviderUtils.getWaypoint(id);
if (waypoint == null) {
return;
}
Location location = waypoint.getLocation();
if (location == null) {
return;
}
LatLng latLng = new LatLng(location.getLatitude(), location.getLongitude());
keepCurrentLocationVisible = false;
CameraUpdate cameraUpdate = CameraUpdateFactory.newLatLngZoom(latLng, DEFAULT_ZOOM_LEVEL);
googleMap.moveCamera(cameraUpdate);
}
});
}
/**
* Gets the default LatLng.
*/
private LatLng getDefaultLatLng() {
MyTracksProviderUtils myTracksProviderUtils = MyTracksProviderUtils.Factory.get(getActivity());
Location location = myTracksProviderUtils.getLastValidTrackPoint();
if (location != null) {
return new LatLng(location.getLatitude(), location.getLongitude());
}
return new LatLng(DEFAULT_LATITUDE, DEFAULT_LONGITUDE);
}
/**
* Returns true if the location is visible. Needs to run on the UI thread.
*
* @param location the location
*/
private boolean isLocationVisible(Location location) {
if (location == null || googleMap == null) {
return false;
}
LatLng latLng = new LatLng(location.getLatitude(), location.getLongitude());
return googleMap.getProjection().getVisibleRegion().latLngBounds.contains(latLng);
}
/**
* Sets the warning message.
*
* @param isGpsProviderEnabled true if gps provider is enabled
*/
private void setWarningMessage(boolean isGpsProviderEnabled) {
String message = getWarningMessage(isGpsProviderEnabled);
if (message == null) {
messageTextView.setVisibility(View.GONE);
return;
}
messageTextView.setText(message);
messageTextView.setVisibility(View.VISIBLE);
if (isGpsProviderEnabled) {
messageTextView.setOnClickListener(null);
} else {
Toast.makeText(getActivity(), R.string.gps_not_found, Toast.LENGTH_LONG).show();
// Click to show the location source settings
messageTextView.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (isResumed()) {
startActivity(GoogleLocationUtils.newLocationSettingsIntent(getActivity()));
}
}
});
}
}
/**
* Gets the warning message.
*
* @param isGpsProviderEnabled true if gps provider is enabled
*/
private String getWarningMessage(boolean isGpsProviderEnabled) {
if (!isSelectedTrackRecording()) {
return null;
}
if (!isGpsProviderEnabled) {
return GoogleLocationUtils.getGpsDisabledMessage(getActivity());
}
boolean hasFix;
boolean hasGoodFix;
if (currentLocation == null) {
hasFix = false;
hasGoodFix = false;
} else {
hasFix = !LocationUtils.isLocationOld(currentLocation);
hasGoodFix = currentLocation.hasAccuracy()
&& currentLocation.getAccuracy() < recordingGpsAccuracy;
}
if (!hasFix) {
return getString(R.string.gps_wait_for_signal);
} else if (!hasGoodFix) {
return getString(R.string.gps_wait_for_better_signal);
} else {
return null;
}
}
}
@@ -0,0 +1,136 @@
/*
* Copyright 2013 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.fragments;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.util.TrackUtils;
import com.google.android.maps.mytracks.R;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.DialogInterface;
import android.database.Cursor;
import android.os.Bundle;
import android.util.SparseBooleanArray;
import java.util.ArrayList;
/**
* A DialogFragment to play multiple tracks.
*
* @author Jimmy Shih
*/
public class PlayMultipleDialogFragment extends AbstractMyTracksDialogFragment {
public static final String PLAY_MULTIPLE_DIALOG_TAG = "playMultipleDialog";
/**
* Interface for caller of this dialog fragment.
*
* @author Jimmy Shih
*/
public interface PlayMultipleCaller {
/**
* Called when play multiple is done.
*/
public void onPlayMultipleDone(long[] trackIds);
}
private static final String KEY_TRACK_ID = "trackId";
public static PlayMultipleDialogFragment newInstance(long trackId) {
Bundle bundle = new Bundle();
bundle.putLong(KEY_TRACK_ID, trackId);
PlayMultipleDialogFragment fragment = new PlayMultipleDialogFragment();
fragment.setArguments(bundle);
return fragment;
}
private PlayMultipleCaller caller;
private String[] items;
private boolean[] checkedItems;
private long[] trackIds;
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
try {
caller = (PlayMultipleCaller) activity;
} catch (ClassCastException e) {
throw new ClassCastException(
activity.toString() + " must implement " + PlayMultipleCaller.class.getSimpleName());
}
}
@Override
protected Dialog createDialog() {
loadData();
return new AlertDialog.Builder(getActivity()).setMultiChoiceItems(items, checkedItems, null)
.setNegativeButton(R.string.generic_cancel, null)
.setPositiveButton(R.string.menu_play, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
SparseBooleanArray array = ((AlertDialog) dialog).getListView()
.getCheckedItemPositions();
caller.onPlayMultipleDone(getChecked(array));
}
}).setTitle(R.string.menu_play_multiple).create();
}
private long[] getChecked(SparseBooleanArray array) {
ArrayList<Long> checked = new ArrayList<Long>();
for (int i = 0; i < array.size(); i++) {
int key = array.keyAt(i);
if (array.get(key)) {
checked.add(trackIds[key]);
}
}
long[] result = new long[checked.size()];
for (int i = 0; i < checked.size(); i++) {
result[i] = checked.get(i).longValue();
}
return result;
}
private void loadData() {
long trackId = getArguments().getLong(KEY_TRACK_ID);
MyTracksProviderUtils myTracksProviderUtils = MyTracksProviderUtils.Factory.get(getActivity());
Cursor cursor = myTracksProviderUtils.getTrackCursor(null, null, TrackUtils.TRACK_SORT_ORDER);
if (cursor == null) {
items = new String[] {};
checkedItems = new boolean[] {};
trackIds = new long[] {};
} else {
int count = cursor.getCount();
items = new String[count];
checkedItems = new boolean[count];
trackIds = new long[count];
for (int i = 0; i < count; i++) {
cursor.moveToPosition(i);
Track track = myTracksProviderUtils.createTrack(cursor);
items[i] = track.getName();
checkedItems[i] = trackId != -1L && track.getId() == trackId;
trackIds[i] = track.getId();
}
}
}
}
@@ -0,0 +1,210 @@
/*
* Copyright 2013 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.fragments;
import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.util.AccountUtils;
import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.android.maps.mytracks.R;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.DialogInterface;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.provider.ContactsContract;
import android.support.v4.app.FragmentActivity;
import android.support.v4.widget.SimpleCursorAdapter;
import android.view.View;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.FilterQueryProvider;
import android.widget.MultiAutoCompleteTextView;
import android.widget.Spinner;
import android.widget.Toast;
/**
* A DialogFragment to share a track.
*
* @author Jimmy Shih
*/
public class ShareTrackDialogFragment extends AbstractMyTracksDialogFragment {
/**
* Interface for caller of this dialog fragment.
*
* @author Jimmy Shih
*/
public interface ShareTrackCaller {
/**
* Called when share track is done.
*
* @param trackId the track id
* @param makePublic true to make the track public
* @param emails the emails to share the track with
* @param account the google drive account
*/
public void onShareTrackDone(long trackId, boolean makePublic, String emails, Account account);
}
public static final String SHARE_TRACK_DIALOG_TAG = "shareTrackDialog";
private static final String KEY_TRACK_ID = "trackId";
public static ShareTrackDialogFragment newInstance(long trackId) {
Bundle bundle = new Bundle();
bundle.putLong(KEY_TRACK_ID, trackId);
ShareTrackDialogFragment shareleTrackDialogFragment = new ShareTrackDialogFragment();
shareleTrackDialogFragment.setArguments(bundle);
return shareleTrackDialogFragment;
}
private ShareTrackCaller caller;
private Account[] accounts;
// UI elements
private CheckBox publicCheckBox;
private CheckBox inviteCheckBox;
private MultiAutoCompleteTextView multiAutoCompleteTextView;
private Spinner accountSpinner;
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
try {
caller = (ShareTrackCaller) activity;
} catch (ClassCastException e) {
throw new ClassCastException(
activity.toString() + " must implement " + ShareTrackCaller.class.getSimpleName());
}
}
@Override
protected Dialog createDialog() {
FragmentActivity fragmentActivity = getActivity();
accounts = AccountManager.get(fragmentActivity).getAccountsByType(Constants.ACCOUNT_TYPE);
if (accounts.length == 0) {
return new AlertDialog.Builder(fragmentActivity).setMessage(
R.string.send_google_no_account_message).setTitle(R.string.send_google_no_account_title)
.setPositiveButton(R.string.generic_ok, null).create();
}
// Get all the views
View view = fragmentActivity.getLayoutInflater().inflate(R.layout.share_track, null);
publicCheckBox = (CheckBox) view.findViewById(R.id.share_track_public);
inviteCheckBox = (CheckBox) view.findViewById(R.id.share_track_invite);
multiAutoCompleteTextView = (MultiAutoCompleteTextView) view.findViewById(
R.id.share_track_emails);
accountSpinner = (Spinner) view.findViewById(R.id.share_track_account);
// Setup publicCheckBox
publicCheckBox.setChecked(PreferencesUtils.getBoolean(
fragmentActivity, R.string.share_track_public_key,
PreferencesUtils.SHARE_TRACK_PUBLIC_DEFAULT));
// Setup inviteCheckBox
inviteCheckBox.setOnCheckedChangeListener(new OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
multiAutoCompleteTextView.setVisibility(isChecked ? View.VISIBLE : View.GONE);
}
});
inviteCheckBox.setChecked(PreferencesUtils.getBoolean(
fragmentActivity, R.string.share_track_invite_key,
PreferencesUtils.SHARE_TRACK_INVITE_DEFAULT));
// Setup multiAutoCompleteTextView
multiAutoCompleteTextView.setTokenizer(new MultiAutoCompleteTextView.CommaTokenizer());
SimpleCursorAdapter adapter = new SimpleCursorAdapter(fragmentActivity,
R.layout.add_emails_item, getAutoCompleteCursor(fragmentActivity, null), new String[] {
ContactsContract.Contacts.DISPLAY_NAME, ContactsContract.CommonDataKinds.Email.DATA },
new int[] { android.R.id.text1, android.R.id.text2 }, 0);
adapter.setCursorToStringConverter(new SimpleCursorAdapter.CursorToStringConverter() {
@Override
public CharSequence convertToString(Cursor cursor) {
int index = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Email.DATA);
return cursor.getString(index).trim();
}
});
adapter.setFilterQueryProvider(new FilterQueryProvider() {
@Override
public Cursor runQuery(CharSequence constraint) {
return getAutoCompleteCursor(getActivity(), constraint);
}
});
multiAutoCompleteTextView.setAdapter(adapter);
// Setup accountSpinner
accountSpinner.setVisibility(accounts.length > 1 ? View.VISIBLE : View.GONE);
AccountUtils.setupAccountSpinner(fragmentActivity, accountSpinner, accounts);
return new AlertDialog.Builder(fragmentActivity).setNegativeButton(
R.string.generic_cancel, null)
.setPositiveButton(R.string.generic_ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
FragmentActivity context = getActivity();
if (!publicCheckBox.isChecked() && !inviteCheckBox.isChecked()) {
Toast.makeText(context, R.string.share_track_no_selection, Toast.LENGTH_LONG).show();
return;
}
String acl = multiAutoCompleteTextView.getText().toString().trim();
if (!publicCheckBox.isChecked() && acl.equals("")) {
Toast.makeText(context, R.string.share_track_no_emails, Toast.LENGTH_LONG).show();
return;
}
PreferencesUtils.setBoolean(
context, R.string.share_track_public_key, publicCheckBox.isChecked());
PreferencesUtils.setBoolean(
context, R.string.share_track_invite_key, inviteCheckBox.isChecked());
Account account = accounts.length > 1 ? accounts[accountSpinner
.getSelectedItemPosition()]
: accounts[0];
AccountUtils.updateShareTrackAccountPreference(context, account);
caller.onShareTrackDone(
getArguments().getLong(KEY_TRACK_ID), publicCheckBox.isChecked(), acl, account);
}
}).setTitle(R.string.share_track_title).setView(view).create();
}
/**
* Gets the auto complete cursor.
*
* @param constraint the constraint
*/
private Cursor getAutoCompleteCursor(FragmentActivity fragmentActivity, CharSequence constraint) {
String order = ContactsContract.Contacts.DISPLAY_NAME + " COLLATE LOCALIZED ASC";
String selection = ContactsContract.Contacts.IN_VISIBLE_GROUP + " = '1'";
if (constraint != null) {
selection += " AND (" + ContactsContract.Contacts.DISPLAY_NAME + " LIKE '%" + constraint
+ "%' OR " + ContactsContract.CommonDataKinds.Email.DATA + " LIKE '%" + constraint
+ "%' )";
}
String[] projection = new String[] { ContactsContract.Contacts._ID,
ContactsContract.Contacts.DISPLAY_NAME, ContactsContract.CommonDataKinds.Email.DATA };
Uri uri = ContactsContract.CommonDataKinds.Email.CONTENT_URI;
return fragmentActivity.getContentResolver().query(uri, projection, selection, null, order);
}
}
@@ -0,0 +1,307 @@
/*
* 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.fragments;
import com.google.android.apps.mytracks.TrackDetailActivity;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.TrackDataHub;
import com.google.android.apps.mytracks.content.TrackDataListener;
import com.google.android.apps.mytracks.content.TrackDataType;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.android.apps.mytracks.util.CalorieUtils;
import com.google.android.apps.mytracks.util.CalorieUtils.ActivityType;
import com.google.android.apps.mytracks.util.LocationUtils;
import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.android.apps.mytracks.util.StatsUtils;
import com.google.android.apps.mytracks.util.TrackIconUtils;
import com.google.android.apps.mytracks.util.UnitConversions;
import com.google.android.maps.mytracks.R;
import android.location.Location;
import android.os.Bundle;
import android.os.Handler;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Spinner;
import java.util.EnumSet;
/**
* A fragment to display track statistics to the user.
*
* @author Sandor Dornbush
* @author Rodrigo Damazio
*/
public class StatsFragment extends Fragment implements TrackDataListener {
public static final String STATS_FRAGMENT_TAG = "statsFragment";
// 1 second in milliseconds
private static final long ONE_SECOND = (long) UnitConversions.S_TO_MS;
private TrackDataHub trackDataHub;
private Handler handler;
private Location lastLocation = null;
private TripStatistics lastTripStatistics = null;
private String category = "";
private int recordingGpsAccuracy = PreferencesUtils.RECORDING_GPS_ACCURACY_DEFAULT;
// A runnable to update the total time field.
private final Runnable updateTotalTime = new Runnable() {
public void run() {
if (isResumed() && isSelectedTrackRecording()) {
if (!isSelectedTrackPaused() && lastTripStatistics != null) {
StatsUtils.setTotalTimeValue(getActivity(), System.currentTimeMillis()
- lastTripStatistics.getStopTime() + lastTripStatistics.getTotalTime());
}
handler.postDelayed(this, ONE_SECOND);
}
}
};
@Override
public View onCreateView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.stats, container, false);
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
handler = new Handler();
Spinner activityTypeIcon = (Spinner) getView().findViewById(R.id.stats_activity_type_icon);
activityTypeIcon.setAdapter(TrackIconUtils.getIconSpinnerAdapter(getActivity(), ""));
activityTypeIcon.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_UP) {
((TrackDetailActivity) getActivity()).chooseActivityType(category);
}
return true;
}
});
activityTypeIcon.setOnKeyListener(new View.OnKeyListener() {
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
((TrackDetailActivity) getActivity()).chooseActivityType(category);
}
return true;
}
});
}
@Override
public void onResume() {
super.onResume();
resumeTrackDataHub();
updateUi(getActivity());
if (isSelectedTrackRecording()) {
handler.post(updateTotalTime);
}
}
@Override
public void onPause() {
super.onPause();
pauseTrackDataHub();
handler.removeCallbacks(updateTotalTime);
}
@Override
public void onTrackUpdated(final Track track) {
if (isResumed()) {
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
if (isResumed()) {
lastTripStatistics = track != null ? track.getTripStatistics() : null;
category = track != null ? track.getCategory() : "";
updateUi(getActivity());
}
}
});
}
}
@Override
public void clearTrackPoints() {
lastLocation = null;
}
@Override
public void onSampledInTrackPoint(Location location) {
lastLocation = location;
}
@Override
public void onSampledOutTrackPoint(Location location) {
lastLocation = location;
}
@Override
public void onSegmentSplit(Location location) {
// We don't care.
}
@Override
public void onNewTrackPointsDone() {
if (isResumed()) {
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
if (isResumed()) {
if (!isSelectedTrackRecording() || isSelectedTrackPaused()) {
lastLocation = null;
}
if (lastLocation != null) {
boolean hasFix = !LocationUtils.isLocationOld(lastLocation);
boolean hasGoodFix = lastLocation.hasAccuracy()
&& lastLocation.getAccuracy() < recordingGpsAccuracy;
if (!hasFix || !hasGoodFix) {
lastLocation = null;
}
}
StatsUtils.setLocationValues(
getActivity(), getActivity(), null, lastLocation, isSelectedTrackRecording());
}
}
});
}
}
@Override
public void clearWaypoints() {
// We don't care.
}
@Override
public void onNewWaypoint(Waypoint wpt) {
// We don't care.
}
@Override
public void onNewWaypointsDone() {
// We don't care.
}
@Override
public boolean onMetricUnitsChanged(final boolean metric) {
if (isResumed()) {
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
if (isResumed()) {
updateUi(getActivity());
}
}
});
}
return true;
}
@Override
public boolean onReportSpeedChanged(final boolean speed) {
if (isResumed()) {
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
if (isResumed()) {
updateUi(getActivity());
}
}
});
}
return true;
}
@Override
public boolean onRecordingGpsAccuracy(int newValue) {
recordingGpsAccuracy = newValue;
return false;
}
@Override
public boolean onRecordingDistanceIntervalChanged(int minRecordingDistance) {
// We don't care.
return false;
}
@Override
public boolean onMapTypeChanged(int mapType) {
// We don't care
return false;
}
/**
* Resumes the trackDataHub. Needs to be synchronized because trackDataHub can
* be accessed by multiple threads.
*/
private synchronized void resumeTrackDataHub() {
trackDataHub = ((TrackDetailActivity) getActivity()).getTrackDataHub();
trackDataHub.registerTrackDataListener(this, EnumSet.of(TrackDataType.TRACKS_TABLE,
TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE, TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE,
TrackDataType.PREFERENCE));
}
/**
* Pauses the trackDataHub. Needs to be synchronized because trackDataHub can
* be accessed by multiple threads.
*/
private synchronized void pauseTrackDataHub() {
trackDataHub.unregisterTrackDataListener(this);
trackDataHub = null;
}
/**
* Returns true if the selected track is recording. Needs to be synchronized
* because trackDataHub can be accessed by multiple threads.
*/
private synchronized boolean isSelectedTrackRecording() {
return trackDataHub != null && trackDataHub.isSelectedTrackRecording();
}
/**
* Returns true if the selected track is paused. Needs to be synchronized
* because trackDataHub can be accessed by multiple threads.
*/
private synchronized boolean isSelectedTrackPaused() {
return trackDataHub != null && trackDataHub.isSelectedTrackPaused();
}
/**
* Updates the UI.
*/
private void updateUi(FragmentActivity activity) {
ActivityType activityType = CalorieUtils.getActivityType(activity, category);
String trackIconValue = TrackIconUtils.getIconValue(activity, category);
StatsUtils.setTripStatisticsValues(
activity, activity, null, lastTripStatistics, activityType, trackIconValue);
StatsUtils.setLocationValues(
activity, activity, null, lastLocation, isSelectedTrackRecording());
}
}
@@ -0,0 +1,40 @@
/*
* Copyright 2011 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.backup;
import android.app.backup.BackupManager;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
/**
* Backup preference listener.
*
* @author Jimmy Shih
*/
public class BackupPreferencesListener implements OnSharedPreferenceChangeListener {
private final BackupManager backupManager;
public BackupPreferencesListener(Context context) {
this.backupManager = new BackupManager(context);
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
backupManager.dataChanged();
}
}
@@ -0,0 +1,102 @@
/*
* 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.Constants;
import android.app.backup.BackupAgent;
import android.app.backup.BackupDataInput;
import android.app.backup.BackupDataOutput;
import android.content.Context;
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 TAG = MyTracksBackupAgent.class.getSimpleName();
private static final String PREFERENCES_ENTITY = "prefs";
@Override
public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
ParcelFileDescriptor newState) throws IOException {
Log.i(TAG, "Performing backup");
SharedPreferences preferences = this.getSharedPreferences(
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
backupPreferences(data, preferences);
Log.i(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(this);
}
@Override
public void onRestore(BackupDataInput data, int appVersionCode,
ParcelFileDescriptor newState) throws IOException {
Log.i(TAG, "Restoring from backup");
while (data.readNextHeader()) {
String key = data.getKey();
Log.d(TAG, "Restoring entity " + key);
if (key.equals(PREFERENCES_ENTITY)) {
restorePreferences(data);
} else {
Log.e(TAG, "Found unknown backup entity: " + key);
data.skipEntityData();
}
}
Log.i(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(
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
PreferenceBackupHelper importer = createPreferenceBackupHelper();
importer.importPreferences(dataBuffer, preferences);
}
}
@@ -0,0 +1,220 @@
/*
* 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 com.google.android.apps.mytracks.util.ApiAdapterFactory;
import com.google.android.maps.mytracks.R;
import android.annotation.SuppressLint;
import android.content.Context;
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.HashSet;
import java.util.Map;
/**
* Helper for backing up and restoring shared preferences.
*
* @author Rodrigo Damazio
*/
public class PreferenceBackupHelper {
private static final int BUFFER_SIZE = 2048;
private final HashSet<String> doNotBackup;
public PreferenceBackupHelper(Context context) {
doNotBackup = new HashSet<String>();
doNotBackup.add(context.getString(R.string.activity_recognition_type_key));
doNotBackup.add(context.getString(R.string.ant_bike_cadence_sensor_id_key));
doNotBackup.add(context.getString(R.string.ant_combined_bike_sensor_id_key));
doNotBackup.add(context.getString(R.string.ant_heart_rate_monitor_id_key));
doNotBackup.add(context.getString(R.string.ant_speed_distance_monitor_id_key));
doNotBackup.add(context.getString(R.string.auto_resume_track_current_retry_key));
doNotBackup.add(context.getString(R.string.bluetooth_sensor_key));
doNotBackup.add(context.getString(R.string.drive_deleted_list_key));
doNotBackup.add(context.getString(R.string.drive_edited_list_key));
doNotBackup.add(context.getString(R.string.drive_largest_change_id_key));
doNotBackup.add(context.getString(R.string.drive_sync_key));
doNotBackup.add(context.getString(R.string.google_account_key));
doNotBackup.add(context.getString(R.string.recording_track_id_key));
doNotBackup.add(context.getString(R.string.recording_track_paused_key));
doNotBackup.add(context.getString(R.string.share_track_account_key));
doNotBackup.add(context.getString(R.string.weight_key));
}
/**
* 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 outWriter 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<String, ?> values = preferences.getAll();
for (String name : doNotBackup) {
values.remove(name);
}
outWriter.writeInt(values.size());
for (Map.Entry<String, ?> 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
* @throws IOException if there are any errors while reading
*/
public void importPreferences(byte[] data, SharedPreferences preferences) throws IOException {
ByteArrayInputStream bufStream = new ByteArrayInputStream(data);
DataInputStream reader = new DataInputStream(bufStream);
importPreferences(reader, preferences);
}
/**
* Imports all preferences from the given stream.
*
* @param reader the stream to read from
* @param preferences the shared preferences to edit
* @throws IOException if there are any errors while reading
*/
@SuppressLint("CommitPrefEdits")
public void 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);
}
ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor);
}
/**
* 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 {
boolean save = true;
if (doNotBackup.contains(name)) {
save = false;
}
switch (typeId) {
case ContentTypeIds.BOOLEAN_TYPE_ID:
boolean booleanValue = reader.readBoolean();
if (save) {
editor.putBoolean(name, booleanValue);
}
return;
case ContentTypeIds.LONG_TYPE_ID:
long longValue = reader.readLong();
if (save) {
editor.putLong(name, longValue);
}
return;
case ContentTypeIds.FLOAT_TYPE_ID:
float floatValue = reader.readFloat();
if (save) {
editor.putFloat(name, floatValue);
}
return;
case ContentTypeIds.INT_TYPE_ID:
int intValue = reader.readInt();
if (save) {
editor.putInt(name, intValue);
}
return;
case ContentTypeIds.STRING_TYPE_ID:
String utfValue = reader.readUTF();
if (save) {
editor.putString(name, utfValue);
}
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");
}
}
}
@@ -0,0 +1,74 @@
/*
* Copyright 2013 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.drive;
import com.google.android.apps.mytracks.io.fusiontables.SendFusionTablesActivity;
import com.google.android.apps.mytracks.io.maps.SendMapsActivity;
import com.google.android.apps.mytracks.io.sendtogoogle.AbstractSendActivity;
import com.google.android.apps.mytracks.io.sendtogoogle.AbstractSendAsyncTask;
import com.google.android.apps.mytracks.io.sendtogoogle.SendRequest;
import com.google.android.apps.mytracks.io.sendtogoogle.UploadResultActivity;
import com.google.android.apps.mytracks.io.spreadsheets.SendSpreadsheetsActivity;
import com.google.android.apps.mytracks.util.IntentUtils;
import com.google.android.maps.mytracks.R;
import android.content.Intent;
/**
* An activity to send a track to Google Drive.
*
* @author Jimmy Shih
*/
public class SendDriveActivity extends AbstractSendActivity {
@Override
protected AbstractSendAsyncTask createAsyncTask() {
return new SendDriveAsyncTask(this, sendRequest.getTrackId(), sendRequest.getAccount(),
sendRequest.getDriveShareEmails(), sendRequest.isDriveSharePublic());
}
@Override
protected String getServiceName() {
return getString(R.string.export_google_drive);
}
@Override
protected void startNextActivity(boolean success, boolean isCancel) {
sendRequest.setDriveSuccess(success);
Class<?> next = getNextClass(isCancel);
Intent intent = IntentUtils.newIntent(this, next)
.putExtra(SendRequest.SEND_REQUEST_KEY, sendRequest);
startActivity(intent);
finish();
}
private Class<?> getNextClass(boolean isCancel) {
if (isCancel) {
return UploadResultActivity.class;
} else {
if (sendRequest.isSendMaps()) {
return SendMapsActivity.class;
} else if (sendRequest.isSendFusionTables()) {
return SendFusionTablesActivity.class;
} else if (sendRequest.isSendSpreadsheets()) {
return SendSpreadsheetsActivity.class;
} else {
return UploadResultActivity.class;
}
}
}
}
@@ -0,0 +1,198 @@
/*
* Copyright 2013 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.drive;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.io.sendtogoogle.AbstractSendAsyncTask;
import com.google.android.apps.mytracks.io.sendtogoogle.SendToGoogleUtils;
import com.google.android.apps.mytracks.io.sync.SyncUtils;
import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.android.gms.auth.GoogleAuthException;
import com.google.android.gms.auth.UserRecoverableAuthException;
import com.google.android.maps.mytracks.R;
import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential;
import com.google.api.client.googleapis.extensions.android.gms.auth.UserRecoverableAuthIOException;
import com.google.api.services.drive.Drive;
import com.google.api.services.drive.model.File;
import com.google.api.services.drive.model.Permission;
import com.google.api.services.drive.model.PermissionList;
import android.accounts.Account;
import android.content.Context;
import java.io.IOException;
/**
* AsyncTask to send a track to Google Drive.
*
* @author Jimmy Shih
*/
public class SendDriveAsyncTask extends AbstractSendAsyncTask {
private final long trackId;
private final Account account;
private final String[] acl;
private final boolean isPublic;
private final Context context;
private final MyTracksProviderUtils myTracksProviderUtils;
public SendDriveAsyncTask(
SendDriveActivity activity, long trackId, Account account, String acl, boolean isPublic) {
super(activity);
this.trackId = trackId;
this.account = account;
if (acl != null) {
this.acl = acl.split(",");
} else {
this.acl = null;
}
this.isPublic = isPublic;
context = activity.getApplicationContext();
myTracksProviderUtils = MyTracksProviderUtils.Factory.get(context);
}
@Override
protected void closeConnection() {}
@Override
protected boolean performTask() {
try {
GoogleAccountCredential credential = SendToGoogleUtils.getGoogleAccountCredential(
context, account.name, SendToGoogleUtils.DRIVE_SCOPE);
if (credential == null) {
return false;
}
Drive drive = SyncUtils.getDriveService(credential);
File folder = SyncUtils.getMyTracksFolder(context, drive);
if (folder == null) {
return false;
}
String folderId = folder.getId();
if (folderId == null) {
return false;
}
Track track = myTracksProviderUtils.getTrack(trackId);
String driveId = track.getDriveId();
String syncAccount = PreferencesUtils.getString(
context, R.string.google_account_key, PreferencesUtils.GOOGLE_ACCOUNT_DEFAULT);
boolean sameAccount = account.name.equals(syncAccount);
if (sameAccount && driveId != null && !driveId.equals("")) {
File driveFile = drive.files().get(driveId).execute();
if (driveFile != null && updateDriveFile(drive, driveFile, folderId, track)) {
addPermission(drive, driveFile);
return true;
}
// clear sync state
SyncUtils.updateTrack(myTracksProviderUtils, track, null);
}
// insert new drive file
File file = SyncUtils.insertDriveFile(
drive, folderId, context, myTracksProviderUtils, track, false, sameAccount);
if (file == null) {
return false;
}
addPermission(drive, file);
return true;
} catch (UserRecoverableAuthException e) {
SendToGoogleUtils.sendNotification(
context, account.name, e.getIntent(), SendToGoogleUtils.DRIVE_NOTIFICATION_ID);
return false;
} catch (GoogleAuthException e) {
return retryTask();
} catch (UserRecoverableAuthIOException e) {
SendToGoogleUtils.sendNotification(
context, account.name, e.getIntent(), SendToGoogleUtils.DRIVE_NOTIFICATION_ID);
return false;
} catch (IOException e) {
return retryTask();
}
}
/**
* Updates a drive file using info from a track. Returns true if successful.
*
* @param drive the drive
* @param driveFile the drive file
* @param folderId the folder id of the drive file
* @param track the track
*/
private boolean updateDriveFile(Drive drive, File driveFile, String folderId, Track track)
throws IOException {
if (!SyncUtils.isInMyTracksAndValid(driveFile, folderId)) {
return false;
}
long driveModifiedTime = driveFile.getModifiedDate().getValue();
long modifiedTime = track.getModifiedTime();
if (driveModifiedTime != modifiedTime) {
return SyncUtils.updateDriveFile(
drive, driveFile, context, myTracksProviderUtils, track, true);
}
return true;
}
@Override
protected void invalidateToken() {}
/**
* Adds permission.
*
* @param drive the drive
* @param file the drive file
*/
private void addPermission(Drive drive, File file) throws IOException {
if (isPublic) {
boolean hasPublic = false;
PermissionList permissionList = drive.permissions().list(file.getId()).execute();
for (Permission permission : permissionList.getItems()) {
String role = permission.getRole();
if (role.equals("reader") || role.equals("writer")) {
if (permission.getType().equals("anyone")) {
hasPublic = true;
break;
}
}
}
if (!hasPublic) {
Permission permission = new Permission();
permission.setRole("reader");
permission.setType("anyone");
permission.setValue("");
drive.permissions().insert(file.getId(), permission).execute();
}
shareUrl = file.getAlternateLink();
}
if (acl != null) {
for (String email : acl) {
email = email.trim();
if (!email.equals("")) {
Permission permission = new Permission();
permission.setRole("reader");
permission.setType("user");
permission.setValue(email);
drive.permissions().insert(file.getId(), permission).execute();
}
}
}
}
}
@@ -0,0 +1,88 @@
package com.google.android.apps.mytracks.io.file;
import com.google.android.apps.mytracks.io.file.exporter.CsvTrackWriter;
import com.google.android.apps.mytracks.io.file.exporter.GpxTrackWriter;
import com.google.android.apps.mytracks.io.file.exporter.KmlTrackWriter;
import com.google.android.apps.mytracks.io.file.exporter.TcxTrackWriter;
import com.google.android.apps.mytracks.io.file.exporter.TrackWriter;
import android.content.Context;
import android.os.Parcel;
import android.os.Parcelable;
import java.util.Locale;
/**
* Definition of all possible track formats.
*/
public enum TrackFileFormat implements Parcelable {
KML {
@Override
public TrackWriter newTrackWriter(Context context, boolean multiple, boolean playTrack) {
return new KmlTrackWriter(context, multiple, playTrack);
}
},
GPX {
@Override
public TrackWriter newTrackWriter(Context context, boolean multiple, boolean playTrack) {
return new GpxTrackWriter(context);
}
},
CSV {
@Override
public TrackWriter newTrackWriter(Context context, boolean multiple, boolean playTrack) {
return new CsvTrackWriter(context);
}
},
TCX {
@Override
public TrackWriter newTrackWriter(Context context, boolean multiple, boolean playTrack) {
return new TcxTrackWriter(context);
}
};
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(final Parcel dest, final int flags) {
dest.writeInt(ordinal());
}
public static final Creator<TrackFileFormat> CREATOR = new Creator<TrackFileFormat>() {
@Override
public TrackFileFormat createFromParcel(final Parcel source) {
return TrackFileFormat.values()[source.readInt()];
}
@Override
public TrackFileFormat[] newArray(final int size) {
return new TrackFileFormat[size];
}
};
/**
* Creates a new track writer for the format.
*
* @param context the context
* @param multiple true for writing multiple tracks
* @param playTrack true to play track
*/
public abstract TrackWriter newTrackWriter(Context context, boolean multiple, boolean playTrack);
/**
* Returns the mime type for each format.
*/
public String getMimeType() {
return "application/" + getExtension() + "+xml";
}
/**
* Returns the file extension for each format.
*/
public String getExtension() {
return this.name().toLowerCase(Locale.US);
}
}
@@ -0,0 +1,249 @@
/*
* 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.file.exporter;
import com.google.android.apps.mytracks.content.MyTracksLocation;
import com.google.android.apps.mytracks.content.Sensor;
import com.google.android.apps.mytracks.content.Sensor.SensorData;
import com.google.android.apps.mytracks.content.Sensor.SensorDataSet;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.io.file.TrackFileFormat;
import com.google.android.apps.mytracks.util.StringUtils;
import com.google.android.maps.mytracks.R;
import android.content.Context;
import android.location.Location;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.text.NumberFormat;
import java.util.Locale;
/**
* Write track as CSV to a file. See RFC 4180 for info on CSV. Output three
* tables.<br>
* The first table contains the track info. Its columns are:<br>
* "Track name","Activity type","Track description" <br>
* <br>
* The second table contains the markers. Its columns are:<br>
* "Marker name","Marker type","Marker description","Latitude (deg)","Longitude
* (deg)","Altitude (m)","Bearing (deg)","Accuracy (m)","Speed (m/s)","Time"<br>
* <br>
* The thrid table contains the points. Its columns are:<br>
* "Segment","Point","Latitude (deg)","Longitude (deg)","Altitude (m)","Bearing
* (deg)","Accuracy (m)","Speed (m/s)","Time","Power (W)","Cadence (rpm)","Heart
* rate (bpm)","Battery level (%)"<br>
*
* @author Rodrigo Damazio
*/
public class CsvTrackWriter implements TrackWriter {
private static final NumberFormat SHORT_FORMAT = NumberFormat.getInstance(Locale.US);
static {
SHORT_FORMAT.setMaximumFractionDigits(4);
}
private final Context context;
private PrintWriter printWriter;
private int segmentIndex;
private int pointIndex;
public CsvTrackWriter(Context context) {
this.context = context;
}
@Override
public String getExtension() {
return TrackFileFormat.CSV.getExtension();
}
@Override
public void prepare(OutputStream outputStream) {
printWriter = new PrintWriter(outputStream);
segmentIndex = 0;
pointIndex = 0;
}
@Override
public void close() {
if (printWriter != null) {
printWriter.flush();
printWriter = null;
}
}
@Override
public void writeHeader(Track[] tracks) {
writeCommaSeparatedLine(context.getString(R.string.generic_name),
context.getString(R.string.track_edit_activity_type_hint),
context.getString(R.string.generic_description));
Track track = tracks[0];
writeCommaSeparatedLine(track.getName(), track.getCategory(), track.getDescription());
writeCommaSeparatedLine();
}
@Override
public void writeFooter() {
// Do nothing
}
@Override
public void writeBeginWaypoints(Track track) {
writeCommaSeparatedLine(context.getString(R.string.generic_name),
context.getString(R.string.marker_edit_marker_type_hint),
context.getString(R.string.generic_description),
context.getString(R.string.description_location_latitude),
context.getString(R.string.description_location_longitude),
context.getString(R.string.description_location_altitude),
context.getString(R.string.description_location_bearing),
context.getString(R.string.description_location_accuracy),
context.getString(R.string.description_location_speed),
context.getString(R.string.description_time));
}
@Override
public void writeEndWaypoints() {
writeCommaSeparatedLine();
}
@Override
public void writeWaypoint(Waypoint waypoint) {
Location location = waypoint.getLocation();
writeCommaSeparatedLine(waypoint.getName(), waypoint.getCategory(), waypoint.getDescription(),
Double.toString(location.getLatitude()), Double.toString(location.getLongitude()),
getAltitude(location), getBearing(location), getAccuracy(location), getSpeed(location),
StringUtils.formatDateTimeIso8601(location.getTime()));
}
@Override
public void writeBeginTracks() {
// Do nothing
}
@Override
public void writeEndTracks() {
// Do nothing
}
@Override
public void writeBeginTrack(Track track, Location startLocation) {
writeCommaSeparatedLine(context.getString(R.string.description_track_segment),
context.getString(R.string.description_track_point),
context.getString(R.string.description_location_latitude),
context.getString(R.string.description_location_longitude),
context.getString(R.string.description_location_altitude),
context.getString(R.string.description_location_bearing),
context.getString(R.string.description_location_accuracy),
context.getString(R.string.description_location_speed),
context.getString(R.string.description_time),
context.getString(R.string.description_sensor_power),
context.getString(R.string.description_sensor_cadence),
context.getString(R.string.description_sensor_heart_rate));
}
@Override
public void writeEndTrack(Track track, Location endLocation) {
// Do nothing
}
@Override
public void writeOpenSegment() {
segmentIndex++;
pointIndex = 0;
}
@Override
public void writeCloseSegment() {
// Do nothing
}
@Override
public void writeLocation(Location location) {
String power = null;
String cadence = null;
String heartRate = null;
if (location instanceof MyTracksLocation) {
SensorDataSet sensorDataSet = ((MyTracksLocation) location).getSensorDataSet();
if (sensorDataSet != null) {
if (sensorDataSet.hasPower()) {
SensorData sensorData = sensorDataSet.getPower();
if (sensorData.hasValue() && sensorData.getState() == Sensor.SensorState.SENDING) {
power = Double.toString(sensorData.getValue());
}
}
if (sensorDataSet.hasCadence()) {
SensorData sensorData = sensorDataSet.getCadence();
if (sensorData.hasValue() && sensorData.getState() == Sensor.SensorState.SENDING) {
cadence = Double.toString(sensorData.getValue());
}
}
if (sensorDataSet.hasHeartRate()) {
SensorData sensorData = sensorDataSet.getHeartRate();
if (sensorData.hasValue() && sensorData.getState() == Sensor.SensorState.SENDING) {
heartRate = Double.toString(sensorData.getValue());
}
}
}
}
pointIndex++;
writeCommaSeparatedLine(Integer.toString(segmentIndex), Integer.toString(pointIndex),
Double.toString(location.getLatitude()), Double.toString(location.getLongitude()),
getAltitude(location), getBearing(location), getAccuracy(location), getSpeed(location),
StringUtils.formatDateTimeIso8601(location.getTime()), power, cadence, heartRate);
}
private String getAltitude(Location location) {
return location.hasAltitude() ? Double.toString(location.getAltitude()) : null;
}
private String getBearing(Location location) {
return location.hasBearing() ? Double.toString(location.getBearing()) : null;
}
private String getAccuracy(Location location) {
return location.hasAccuracy() ? SHORT_FORMAT.format(location.getAccuracy()) : null;
}
private String getSpeed(Location location) {
return location.hasSpeed() ? SHORT_FORMAT.format(location.getSpeed()) : null;
}
/**
* Writes a single line of a CSV file.
*
* @param values the values to be written as CSV
*/
private void writeCommaSeparatedLine(String... values) {
StringBuilder builder = new StringBuilder();
boolean isFirst = true;
for (String value : values) {
if (!isFirst) {
builder.append(',');
}
isFirst = false;
builder.append('"');
if (value != null) {
builder.append(value.replaceAll("\"", "\"\""));
}
builder.append('"');
}
printWriter.println(builder.toString());
}
}
@@ -0,0 +1,244 @@
/*
* Copyright 2011 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.file.exporter;
import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.content.MyTracksLocation;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils.LocationIterator;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.util.LocationUtils;
import android.database.Cursor;
import android.location.Location;
import android.util.Log;
import java.io.OutputStream;
/**
* Track Writer for writing tracks to an {@link OutputStream}.
*
* @author Sandor Dornbush
* @author Rodrigo Damazio
*/
public class FileTrackExporter implements TrackExporter {
private static final String TAG = FileTrackExporter.class.getSimpleName();
private final MyTracksProviderUtils myTracksProviderUtils;
private final Track[] tracks;
private final TrackWriter trackWriter;
private final TrackExporterListener trackExporterListener;
/**
* Constructor.
*
* @param myTracksProviderUtils the my tracks provider utils
* @param tracks the tracks
* @param trackWriter the track writer
* @param trackExporterListener the track export listener
*/
public FileTrackExporter(MyTracksProviderUtils myTracksProviderUtils, Track[] tracks,
TrackWriter trackWriter, TrackExporterListener trackExporterListener) {
this.myTracksProviderUtils = myTracksProviderUtils;
this.tracks = tracks;
this.trackWriter = trackWriter;
this.trackExporterListener = trackExporterListener;
}
@Override
public boolean writeTrack(OutputStream outputStream) {
try {
trackWriter.prepare(outputStream);
trackWriter.writeHeader(tracks);
for (int i = 0; i < tracks.length; i++) {
writeWaypoints(tracks[i]);
}
trackWriter.writeBeginTracks();
long startTime = tracks[0].getTripStatistics().getStartTime();
for (int i = 0; i < tracks.length; i++) {
long offset = tracks[i].getTripStatistics().getStartTime() - startTime;
writeLocations(tracks[i], offset);
}
trackWriter.writeEndTracks();
trackWriter.writeFooter();
trackWriter.close();
return true;
} catch (InterruptedException e) {
Log.e(TAG, "Thread interrupted", e);
return false;
}
}
/**
* Writes the waypoints.
*/
private void writeWaypoints(Track track) throws InterruptedException {
/*
* TODO: Stream through the waypoints in chunks. I am leaving the number of
* waypoints very high which should not be a problem because we don't try to
* load them into objects all at the same time.
*/
boolean hasWaypoints = false;
Cursor cursor = null;
try {
cursor = myTracksProviderUtils.getWaypointCursor(
track.getId(), -1L, Constants.MAX_LOADED_WAYPOINTS_POINTS);
if (cursor != null && cursor.moveToFirst()) {
/*
* Yes, this will skip the first waypoint and that is intentional as the
* first waypoint holds the stats for the track.
*/
while (cursor.moveToNext()) {
if (Thread.interrupted()) {
throw new InterruptedException();
}
if (!hasWaypoints) {
trackWriter.writeBeginWaypoints(track);
hasWaypoints = true;
}
Waypoint waypoint = myTracksProviderUtils.createWaypoint(cursor);
trackWriter.writeWaypoint(waypoint);
}
}
} finally {
if (cursor != null) {
cursor.close();
}
}
if (hasWaypoints) {
trackWriter.writeEndWaypoints();
}
}
/**
* Writes the locations.
*/
private void writeLocations(Track track, long offset) throws InterruptedException {
boolean wroteTrack = false;
boolean wroteSegment = false;
boolean isLastLocationValid = false;
TrackWriterLocationFactory locationFactory = new TrackWriterLocationFactory();
int locationNumber = 0;
LocationIterator locationIterator = null;
try {
locationIterator = myTracksProviderUtils.getTrackPointLocationIterator(
track.getId(), -1L, false, locationFactory);
while (locationIterator.hasNext()) {
if (Thread.interrupted()) {
throw new InterruptedException();
}
Location location = locationIterator.next();
setLocationTime(location, offset);
locationNumber++;
boolean isLocationValid = LocationUtils.isValidLocation(location);
boolean isSegmentValid = isLocationValid && isLastLocationValid;
if (!wroteTrack && isSegmentValid) {
// Found the first two consecutive locations that are valid
trackWriter.writeBeginTrack(track, locationFactory.lastLocation);
wroteTrack = true;
}
if (isSegmentValid) {
if (!wroteSegment) {
// Start a segment
trackWriter.writeOpenSegment();
wroteSegment = true;
// Write the previous location, which we had previously skipped
trackWriter.writeLocation(locationFactory.lastLocation);
}
// Write the current location
trackWriter.writeLocation(location);
if (trackExporterListener != null) {
trackExporterListener.onProgressUpdate(locationNumber, track.getNumberOfPoints());
}
} else {
if (wroteSegment) {
trackWriter.writeCloseSegment();
wroteSegment = false;
}
}
locationFactory.swapLocations();
isLastLocationValid = isLocationValid;
}
if (wroteSegment) {
trackWriter.writeCloseSegment();
wroteSegment = false;
}
if (wroteTrack) {
Location lastValidTrackPoint = myTracksProviderUtils.getLastValidTrackPoint(track.getId());
setLocationTime(lastValidTrackPoint, offset);
trackWriter.writeEndTrack(track, lastValidTrackPoint);
} else {
// Write an empty track
trackWriter.writeBeginTrack(track, null);
trackWriter.writeEndTrack(track, null);
}
} finally {
if (locationIterator != null) {
locationIterator.close();
}
}
}
/**
* Sets a location time.
*
* @param location the location
* @param offset the time offset
*/
private void setLocationTime(Location location, long offset) {
if (location != null) {
location.setTime(location.getTime() - offset);
}
}
/**
* Track writer location factory. Keeping the last two locations.
*
* @author Jimmy Shih
*/
private class TrackWriterLocationFactory implements MyTracksProviderUtils.LocationFactory {
Location currentLocation;
Location lastLocation;
@Override
public Location createLocation() {
if (currentLocation == null) {
currentLocation = new MyTracksLocation("");
}
return currentLocation;
}
public void swapLocations() {
Location tempLocation = lastLocation;
lastLocation = currentLocation;
currentLocation = tempLocation;
if (currentLocation != null) {
currentLocation.reset();
}
}
}
}
@@ -0,0 +1,202 @@
/*
* 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.file.exporter;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.io.file.TrackFileFormat;
import com.google.android.apps.mytracks.util.StringUtils;
import com.google.android.maps.mytracks.R;
import android.content.Context;
import android.location.Location;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.text.NumberFormat;
import java.util.Locale;
/**
* Write track as GPX to a file.
*
* @author Sandor Dornbush
*/
public class GpxTrackWriter implements TrackWriter {
private static final NumberFormat ELEVATION_FORMAT = NumberFormat.getInstance(Locale.US);
private static final NumberFormat COORDINATE_FORMAT = NumberFormat.getInstance(Locale.US);
static {
/*
* GPX readers expect to see fractional numbers with US-style punctuation.
* That is, they want periods for decimal points, rather than commas.
*/
ELEVATION_FORMAT.setMaximumFractionDigits(1);
ELEVATION_FORMAT.setGroupingUsed(false);
COORDINATE_FORMAT.setMaximumFractionDigits(6);
COORDINATE_FORMAT.setMaximumIntegerDigits(3);
COORDINATE_FORMAT.setGroupingUsed(false);
}
private final Context context;
private PrintWriter printWriter;
public GpxTrackWriter(Context context) {
this.context = context;
}
@Override
public String getExtension() {
return TrackFileFormat.GPX.getExtension();
}
@Override
public void prepare(OutputStream outputStream) {
this.printWriter = new PrintWriter(outputStream);
}
@Override
public void close() {
if (printWriter != null) {
printWriter.flush();
printWriter = null;
}
}
@Override
public void writeHeader(Track[] tracks) {
if (printWriter != null) {
printWriter.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
printWriter.println("<gpx");
printWriter.println("version=\"1.1\"");
printWriter.println(
"creator=\"" + context.getString(R.string.send_google_by_my_tracks, "", "") + "\"");
printWriter.println("xmlns=\"http://www.topografix.com/GPX/1/1\"");
printWriter.println(
"xmlns:topografix=\"http://www.topografix.com/GPX/Private/TopoGrafix/0/1\"");
printWriter.println("xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"");
printWriter.println("xsi:schemaLocation=\"http://www.topografix.com/GPX/1/1"
+ " http://www.topografix.com/GPX/1/1/gpx.xsd"
+ " http://www.topografix.com/GPX/Private/TopoGrafix/0/1"
+ " http://www.topografix.com/GPX/Private/TopoGrafix/0/1/topografix.xsd\">");
printWriter.println("<metadata>");
Track track = tracks[0];
printWriter.println("<name>" + StringUtils.formatCData(track.getName()) + "</name>");
printWriter.println("<desc>" + StringUtils.formatCData(track.getDescription()) + "</desc>");
printWriter.println("</metadata>");
}
}
@Override
public void writeFooter() {
if (printWriter != null) {
printWriter.println("</gpx>");
}
}
@Override
public void writeBeginWaypoints(Track track) {
// Do nothing
}
@Override
public void writeEndWaypoints() {
// Do nothing
}
@Override
public void writeWaypoint(Waypoint waypoint) {
if (printWriter != null) {
Location location = waypoint.getLocation();
if (location != null) {
printWriter.println("<wpt " + formatLocation(location) + ">");
if (location.hasAltitude()) {
printWriter.println("<ele>" + ELEVATION_FORMAT.format(location.getAltitude()) + "</ele>");
}
printWriter.println(
"<time>" + StringUtils.formatDateTimeIso8601(location.getTime()) + "</time>");
printWriter.println("<name>" + StringUtils.formatCData(waypoint.getName()) + "</name>");
printWriter.println("<cmt>" + StringUtils.formatCData(waypoint.getType().name()) + "</cmt>");
printWriter.println(
"<desc>" + StringUtils.formatCData(waypoint.getDescription()) + "</desc>");
printWriter.println("<type>" + StringUtils.formatCData(waypoint.getCategory()) + "</type>");
printWriter.println("</wpt>");
}
}
}
@Override
public void writeBeginTracks() {
// Do nothing
}
@Override
public void writeEndTracks() {
// Do nothing
}
@Override
public void writeBeginTrack(Track track, Location startLocation) {
if (printWriter != null) {
printWriter.println("<trk>");
printWriter.println("<name>" + StringUtils.formatCData(track.getName()) + "</name>");
printWriter.println("<desc>" + StringUtils.formatCData(track.getDescription()) + "</desc>");
printWriter.println("<type>" + StringUtils.formatCData(track.getCategory()) + "</type>");
printWriter.println("<extensions><topografix:color>c0c0c0</topografix:color></extensions>");
}
}
@Override
public void writeEndTrack(Track track, Location endLocation) {
if (printWriter != null) {
printWriter.println("</trk>");
}
}
@Override
public void writeOpenSegment() {
printWriter.println("<trkseg>");
}
@Override
public void writeCloseSegment() {
printWriter.println("</trkseg>");
}
@Override
public void writeLocation(Location location) {
if (printWriter != null) {
printWriter.println("<trkpt " + formatLocation(location) + ">");
if (location.hasAltitude()) {
printWriter.println("<ele>" + ELEVATION_FORMAT.format(location.getAltitude()) + "</ele>");
}
printWriter.println(
"<time>" + StringUtils.formatDateTimeIso8601(location.getTime()) + "</time>");
printWriter.println("</trkpt>");
}
}
/**
* Formats a location with latitude and longitude coordinates.
*
* @param location the location
*/
private String formatLocation(Location location) {
return "lat=\"" + COORDINATE_FORMAT.format(location.getLatitude()) + "\" lon=\""
+ COORDINATE_FORMAT.format(location.getLongitude()) + "\"";
}
}
@@ -0,0 +1,497 @@
/*
* 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.file.exporter;
import com.google.android.apps.mytracks.content.DescriptionGenerator;
import com.google.android.apps.mytracks.content.DescriptionGeneratorImpl;
import com.google.android.apps.mytracks.content.MyTracksLocation;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.Sensor;
import com.google.android.apps.mytracks.content.Sensor.SensorData;
import com.google.android.apps.mytracks.content.Sensor.SensorDataSet;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.content.Waypoint.WaypointType;
import com.google.android.apps.mytracks.io.file.TrackFileFormat;
import com.google.android.apps.mytracks.util.GoogleEarthUtils;
import com.google.android.apps.mytracks.util.StringUtils;
import com.google.android.maps.mytracks.R;
import com.google.common.annotations.VisibleForTesting;
import android.content.Context;
import android.database.Cursor;
import android.location.Location;
import android.net.Uri;
import java.io.File;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.util.ArrayList;
/**
* Write track as KML to a file.
*
* @author Leif Hendrik Wilden
*/
public class KmlTrackWriter implements TrackWriter {
private static final String WAYPOINT_STYLE = "waypoint";
private static final String STATISTICS_STYLE = "statistics";
private static final String START_STYLE = "start";
private static final String END_STYLE = "end";
private static final String TRACK_STYLE = "track";
private static final String SCHEMA_ID = "schema";
private static final String CADENCE = "cadence";
private static final String HEART_RATE = "heart_rate";
private static final String POWER = "power";
private static final String
WAYPOINT_ICON = "http://maps.google.com/mapfiles/kml/pushpin/blue-pushpin.png";
private static final String
STATISTICS_ICON = "http://maps.google.com/mapfiles/kml/pushpin/ylw-pushpin.png";
private static final String
START_ICON = "http://maps.google.com/mapfiles/kml/paddle/grn-circle.png";
private static final String
END_ICON = "http://maps.google.com/mapfiles/kml/paddle/red-circle.png";
private static final String
TRACK_ICON = "http://earth.google.com/images/kml-icons/track-directional/track-0.png";
private final Context context;
private final boolean multiple;
private final boolean playTrack;
private final DescriptionGenerator descriptionGenerator;
private final MyTracksProviderUtils myTracksProviderUtils;
private PrintWriter printWriter;
private ArrayList<Integer> powerList = new ArrayList<Integer>();
private ArrayList<Integer> cadenceList = new ArrayList<Integer>();
private ArrayList<Integer> heartRateList = new ArrayList<Integer>();
private boolean hasPower;
private boolean hasCadence;
private boolean hasHeartRate;
public KmlTrackWriter(Context context, boolean multiple, boolean playTrack) {
this(context, multiple, playTrack, new DescriptionGeneratorImpl(context));
}
@VisibleForTesting
KmlTrackWriter(Context context, boolean multiple, boolean playTrack,
DescriptionGenerator descriptionGenerator) {
this.context = context;
this.multiple = multiple;
this.playTrack = playTrack;
this.descriptionGenerator = descriptionGenerator;
this.myTracksProviderUtils = MyTracksProviderUtils.Factory.get(context);
}
@Override
public String getExtension() {
return TrackFileFormat.KML.getExtension();
}
@Override
public void prepare(OutputStream outputStream) {
this.printWriter = new PrintWriter(outputStream);
}
@Override
public void close() {
if (printWriter != null) {
printWriter.flush();
printWriter = null;
}
}
@Override
public void writeHeader(Track[] tracks) {
if (printWriter != null) {
printWriter.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
printWriter.println("<kml xmlns=\"http://www.opengis.net/kml/2.2\"");
printWriter.println("xmlns:gx=\"http://www.google.com/kml/ext/2.2\"");
printWriter.println("xmlns:atom=\"http://www.w3.org/2005/Atom\">");
printWriter.println("<Document>");
printWriter.println("<open>1</open>");
printWriter.println("<visibility>1</visibility>");
Track track = tracks[0];
printWriter.println("<name>" + StringUtils.formatCData(track.getName()) + "</name>");
printWriter.println("<atom:author><atom:name>"
+ StringUtils.formatCData(context.getString(R.string.send_google_by_my_tracks, "", ""))
+ "</atom:name></atom:author>");
writeTrackStyle();
writePlacemarkerStyle(START_STYLE, START_ICON, 32, 1);
writePlacemarkerStyle(END_STYLE, END_ICON, 32, 1);
writePlacemarkerStyle(STATISTICS_STYLE, STATISTICS_ICON, 20, 2);
writePlacemarkerStyle(WAYPOINT_STYLE, WAYPOINT_ICON, 20, 2);
printWriter.println("<Schema id=\"" + SCHEMA_ID + "\">");
writeSensorStyle(POWER, context.getString(R.string.description_sensor_power));
writeSensorStyle(CADENCE, context.getString(R.string.description_sensor_cadence));
writeSensorStyle(HEART_RATE, context.getString(R.string.description_sensor_heart_rate));
printWriter.println("</Schema>");
}
}
@Override
public void writeFooter() {
if (printWriter != null) {
printWriter.println("</Document>");
printWriter.println("</kml>");
}
}
@Override
public void writeBeginWaypoints(Track track) {
if (printWriter != null) {
printWriter.println("<Folder><name>"
+ StringUtils.formatCData(context.getString(R.string.track_markers, track.getName()))
+ "</name>");
printWriter.println("<open>1</open>");
}
}
@Override
public void writeEndWaypoints() {
if (printWriter != null) {
printWriter.println("</Folder>");
}
}
@Override
public void writeWaypoint(Waypoint waypoint) {
if (printWriter != null) {
String styleName = waypoint.getType() == WaypointType.STATISTICS ? STATISTICS_STYLE
: WAYPOINT_STYLE;
String photoUrl = waypoint.getPhotoUrl();
if (photoUrl != null && !photoUrl.equals("")) {
float heading = getHeading(waypoint.getTrackId(), waypoint.getLocation());
writePhotoOverlay(waypoint.getName(), waypoint.getCategory(), waypoint.getDescription(),
styleName, waypoint.getLocation(), photoUrl, heading);
} else {
writePlacemark(waypoint.getName(), waypoint.getCategory(), waypoint.getDescription(),
styleName, waypoint.getLocation());
}
}
}
@Override
public void writeBeginTracks() {
if (printWriter != null && multiple) {
printWriter.println("<Folder id=\"" + GoogleEarthUtils.TOUR_FEATURE_ID_VALUE + "\">");
printWriter.println("<name>" + context.getString(R.string.generic_tracks) + "</name>");
printWriter.println("<open>1</open>");
}
}
@Override
public void writeEndTracks() {
if (printWriter != null && multiple) {
printWriter.println("</Folder>");
}
}
@Override
public void writeBeginTrack(Track track, Location startLocation) {
if (printWriter != null) {
String name = context.getString(R.string.marker_label_start, track.getName());
writePlacemark(name, "", "", START_STYLE, startLocation);
if (multiple) {
// No need to add TOUR_FEATURE_ID_VALUE
printWriter.println("<Placemark>");
} else {
printWriter.println("<Placemark id=\"" + GoogleEarthUtils.TOUR_FEATURE_ID_VALUE + "\">");
}
printWriter.println("<name>" + StringUtils.formatCData(track.getName()) + "</name>");
printWriter.println(
"<description>" + StringUtils.formatCData(track.getDescription()) + "</description>");
printWriter.println("<styleUrl>#" + TRACK_STYLE + "</styleUrl>");
writeCategory(track.getCategory());
printWriter.println("<gx:MultiTrack>");
printWriter.println("<altitudeMode>absolute</altitudeMode>");
printWriter.println("<gx:interpolate>1</gx:interpolate>");
}
}
@Override
public void writeEndTrack(Track track, Location endLocation) {
if (printWriter != null) {
printWriter.println("</gx:MultiTrack>");
printWriter.println("</Placemark>");
String name = context.getString(R.string.marker_label_end, track.getName());
String description = descriptionGenerator.generateTrackDescription(track, null, null, false);
writePlacemark(name, "", description, END_STYLE, endLocation);
}
}
@Override
public void writeOpenSegment() {
if (printWriter != null) {
printWriter.println("<gx:Track>");
hasPower = false;
hasCadence = false;
hasHeartRate = false;
powerList.clear();
cadenceList.clear();
heartRateList.clear();
}
}
@Override
public void writeCloseSegment() {
if (printWriter != null) {
printWriter.println("<ExtendedData>");
printWriter.println("<SchemaData schemaUrl=\"#" + SCHEMA_ID + "\">");
if (hasPower) {
writeSensorData(powerList, POWER);
}
if (hasCadence) {
writeSensorData(cadenceList, CADENCE);
}
if (hasHeartRate) {
writeSensorData(heartRateList, HEART_RATE);
}
printWriter.println("</SchemaData>");
printWriter.println("</ExtendedData>");
printWriter.println("</gx:Track>");
}
}
@Override
public void writeLocation(Location location) {
if (printWriter != null) {
printWriter.println(
"<when>" + StringUtils.formatDateTimeIso8601(location.getTime()) + "</when>");
printWriter.println("<gx:coord>" + getCoordinates(location, " ") + "</gx:coord>");
if (location instanceof MyTracksLocation) {
SensorDataSet sensorDataSet = ((MyTracksLocation) location).getSensorDataSet();
int power = -1;
int cadence = -1;
int heartRate = -1;
if (sensorDataSet != null) {
if (sensorDataSet.hasPower()) {
SensorData sensorData = sensorDataSet.getPower();
if (sensorData.hasValue() && sensorData.getState() == Sensor.SensorState.SENDING) {
hasPower = true;
power = sensorData.getValue();
}
}
if (sensorDataSet.hasCadence()) {
SensorData sensorData = sensorDataSet.getCadence();
if (sensorData.hasValue() && sensorData.getState() == Sensor.SensorState.SENDING) {
hasCadence = true;
cadence = sensorData.getValue();
}
}
if (sensorDataSet.hasHeartRate()) {
SensorData sensorData = sensorDataSet.getHeartRate();
if (sensorData.hasValue() && sensorData.getState() == Sensor.SensorState.SENDING) {
hasHeartRate = true;
heartRate = sensorData.getValue();
}
}
}
powerList.add(power);
cadenceList.add(cadence);
heartRateList.add(heartRate);
}
}
}
/**
* Writes the sensor data.
*
* @param list a list of sensor data
* @param name the name of the sensor data
*/
private void writeSensorData(ArrayList<Integer> list, String name) {
printWriter.println("<gx:SimpleArrayData name=\"" + name + "\">");
for (int i = 0; i < list.size(); i++) {
printWriter.println("<gx:value>" + list.get(i) + "</gx:value>");
}
printWriter.println("</gx:SimpleArrayData>");
}
/**
* Writes a placemark.
*
* @param name the name
* @param category the category
* @param description the description
* @param styleName the style name
* @param location the location
*/
private void writePlacemark(
String name, String category, String description, String styleName, Location location) {
if (location != null) {
printWriter.println("<Placemark>");
printWriter.println("<name>" + StringUtils.formatCData(name) + "</name>");
printWriter.println(
"<description>" + StringUtils.formatCData(description) + "</description>");
printWriter.println("<TimeStamp><when>"
+ StringUtils.formatDateTimeIso8601(location.getTime()) + "</when></TimeStamp>");
printWriter.println("<styleUrl>#" + styleName + "</styleUrl>");
writeCategory(category);
printWriter.println("<Point>");
printWriter.println("<coordinates>" + getCoordinates(location, ",") + "</coordinates>");
printWriter.println("</Point>");
printWriter.println("</Placemark>");
}
}
/**
* Writes a photo overlay.
*
* @param name the name
* @param category the category
* @param description the description
* @param styleName the style name
* @param location the location
* @param photoUrl the photo url
* @param heading the heading
*/
private void writePhotoOverlay(String name, String category, String description, String styleName,
Location location, String photoUrl, float heading) {
if (location != null) {
printWriter.println("<PhotoOverlay>");
printWriter.println("<name>" + StringUtils.formatCData(name) + "</name>");
printWriter.println(
"<description>" + StringUtils.formatCData(description) + "</description>");
printWriter.print("<Camera>");
printWriter.print("<longitude>" + location.getLongitude() + "</longitude>");
printWriter.print("<latitude>" + location.getLatitude() + "</latitude>");
printWriter.print("<altitude>20</altitude>");
printWriter.print("<heading>" + heading + "</heading>");
printWriter.print("<tilt>90</tilt>");
printWriter.println("</Camera>");
printWriter.println("<TimeStamp><when>"
+ StringUtils.formatDateTimeIso8601(location.getTime()) + "</when></TimeStamp>");
printWriter.println("<styleUrl>#" + styleName + "</styleUrl>");
writeCategory(category);
if (playTrack) {
printWriter.println("<Icon><href>" + Uri.decode(photoUrl) + "</href></Icon>");
} else {
Uri uri = Uri.parse(photoUrl);
printWriter.println("<Icon><href>" + KmzTrackExporter.KMZ_IMAGES_DIR + File.separatorChar
+ uri.getLastPathSegment() + "</href></Icon>");
}
printWriter.print("<ViewVolume>");
printWriter.print("<near>10</near>");
printWriter.print("<leftFov>-60</leftFov>");
printWriter.print("<rightFov>60</rightFov>");
printWriter.print("<bottomFov>-45</bottomFov>");
printWriter.print("<topFov>45</topFov>");
printWriter.println("</ViewVolume>");
printWriter.println("<Point>");
printWriter.println("<coordinates>" + getCoordinates(location, ",") + "</coordinates>");
printWriter.println("</Point>");
printWriter.println("</PhotoOverlay>");
}
}
/**
* Gets the heading to a location.
*
* @param trackId the track id containing the location
* @param location the location
*/
private float getHeading(long trackId, Location location) {
long trackPointId = myTracksProviderUtils.getTrackPointId(trackId, location);
if (trackPointId == -1L) {
return location.getBearing();
}
Cursor cursor = null;
Location viewLocation;
try {
cursor = myTracksProviderUtils.getTrackPointCursor(trackId, trackPointId, 10, true);
if (cursor == null || cursor.getCount() == 0) {
return location.getBearing();
}
cursor.moveToPosition(cursor.getCount() - 1);
viewLocation = myTracksProviderUtils.createTrackPoint(cursor);
} finally {
if (cursor != null) {
cursor.close();
}
}
return viewLocation.bearingTo(location);
}
private String getCoordinates(Location location, String separator) {
StringBuffer buffer = new StringBuffer();
buffer.append(location.getLongitude() + separator + location.getLatitude());
if (location.hasAltitude()) {
buffer.append(separator + location.getAltitude());
}
return buffer.toString();
}
/**
* Writes the category.
*
* @param category the category
*/
private void writeCategory(String category) {
if (category == null || category.equals("")) {
return;
}
printWriter.println("<ExtendedData>");
printWriter.println(
"<Data name=\"type\"><value>" + StringUtils.formatCData(category) + "</value></Data>");
printWriter.println("</ExtendedData>");
}
/**
* Writes the track style.
*/
private void writeTrackStyle() {
printWriter.println("<Style id=\"" + TRACK_STYLE + "\">");
printWriter.println("<LineStyle><color>7f0000ff</color><width>4</width></LineStyle>");
printWriter.println("<IconStyle>");
printWriter.println("<scale>1.3</scale>");
printWriter.println("<Icon><href>" + TRACK_ICON + "</href></Icon>");
printWriter.println("</IconStyle>");
printWriter.println("</Style>");
}
/**
* Writes a placemarker style.
*
* @param name the name of the style
* @param url the url of the style icon
* @param x the x position of the hotspot
* @param y the y position of the hotspot
*/
private void writePlacemarkerStyle(String name, String url, int x, int y) {
printWriter.println("<Style id=\"" + name + "\"><IconStyle>");
printWriter.println("<scale>1.3</scale>");
printWriter.println("<Icon><href>" + url + "</href></Icon>");
printWriter.println(
"<hotSpot x=\"" + x + "\" y=\"" + y + "\" xunits=\"pixels\" yunits=\"pixels\"/>");
printWriter.println("</IconStyle></Style>");
}
/**
* Writes a sensor style.
*
* @param name the name of the sesnor
* @param displayName the sensor display name
*/
private void writeSensorStyle(String name, String displayName) {
printWriter.println("<gx:SimpleArrayField name=\"" + name + "\" type=\"int\">");
printWriter.println("<displayName>" + StringUtils.formatCData(displayName) + "</displayName>");
printWriter.println("</gx:SimpleArrayField>");
}
}
@@ -0,0 +1,198 @@
/*
* Copyright 2013 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.file.exporter;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.android.maps.mytracks.R;
import android.content.Context;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.util.Log;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
/**
* KMZ track exporter.
*
* @author Jimmy Shih
*/
public class KmzTrackExporter implements TrackExporter {
public static final String KMZ_EXTENSION = "kmz";
public static final String KMZ_IMAGES_DIR = "images";
public static final String KMZ_KML_FILE = "doc.kml";
private static final String TAG = KmzTrackExporter.class.getSimpleName();
private static final int BUFFER_SIZE = 4096;
private final MyTracksProviderUtils myTracksProviderUtils;
private final FileTrackExporter fileTrackExporter;
private final Track[] tracks;
private final long photoSize;
/**
* Constructor.
*
* @param myTracksProviderUtils the my tracks provider utils
* @param fileTrackExporter the file track exporter
* @param tracks the tracks to export
* @param context the context
*/
public KmzTrackExporter(MyTracksProviderUtils myTracksProviderUtils,
FileTrackExporter fileTrackExporter, Track[] tracks, Context context) {
this.myTracksProviderUtils = myTracksProviderUtils;
this.fileTrackExporter = fileTrackExporter;
this.tracks = tracks;
this.photoSize = PreferencesUtils.getInt(
context, R.string.photo_size_key, PreferencesUtils.PHOTO_SIZE_DEFAULT);
}
@Override
public boolean writeTrack(OutputStream outputStream) {
ZipOutputStream zipOutputStream = null;
try {
zipOutputStream = new ZipOutputStream(outputStream);
// Add kml file
ZipEntry zipEntry = new ZipEntry(KMZ_KML_FILE);
zipOutputStream.putNextEntry(zipEntry);
boolean success = fileTrackExporter.writeTrack(zipOutputStream);
zipOutputStream.closeEntry();
if (!success) {
Log.e(TAG, "Unable to write kml in kmz");
return false;
}
// Add photos
addImages(zipOutputStream);
return true;
} catch (InterruptedException e) {
Log.e(TAG, "Unable to write track", e);
return false;
} catch (IOException e) {
Log.e(TAG, "Unable to write track", e);
return false;
} finally {
if (zipOutputStream != null) {
try {
zipOutputStream.close();
} catch (IOException e) {
Log.e(TAG, "Unable to close zip input stream", e);
}
}
}
}
private void addImages(ZipOutputStream zipOutputStream) throws InterruptedException, IOException {
for (Track track : tracks) {
Cursor cursor = null;
try {
cursor = myTracksProviderUtils.getWaypointCursor(track.getId(), -1L, -1);
if (cursor != null && cursor.moveToFirst()) {
/*
* Yes, this will skip the first waypoint and that is intentional as
* the first waypoint holds the stats for the track.
*/
while (cursor.moveToNext()) {
if (Thread.interrupted()) {
throw new InterruptedException();
}
Waypoint waypoint = myTracksProviderUtils.createWaypoint(cursor);
String photoUrl = waypoint.getPhotoUrl();
if (photoUrl != null && !photoUrl.equals("")) {
addImage(zipOutputStream, photoUrl);
}
}
}
} finally {
if (cursor != null) {
cursor.close();
}
}
}
}
private void addImage(ZipOutputStream zipOutputStream, String photoUrl) throws IOException {
Uri uri = Uri.parse(photoUrl);
File file = new File(uri.getPath());
if (!file.exists()) {
Log.e(TAG, "file not found " + photoUrl);
return;
}
ZipEntry zipEntry = new ZipEntry(
KMZ_IMAGES_DIR + File.separatorChar + uri.getLastPathSegment());
zipOutputStream.putNextEntry(zipEntry);
int sampleSize;
if (photoSize == -1) {
sampleSize = 1;
} else {
long size = file.length();
// Convert from kilobytes to bytes.
long limit = photoSize * 1024;
sampleSize = size > limit ? (int) Math.ceil(size / limit) : 1;
}
if (sampleSize == 1) {
readFromFile(zipOutputStream, uri);
} else {
readFromScaledBitmap(zipOutputStream, uri, sampleSize);
}
zipOutputStream.closeEntry();
}
private void readFromScaledBitmap(ZipOutputStream zipOutputStream, Uri uri, int sampleSize) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = false;
options.inSampleSize = sampleSize;
Bitmap bitmap = BitmapFactory.decodeFile(uri.getPath(), options);
if (bitmap == null) {
return;
}
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, zipOutputStream);
bitmap.recycle();
}
private void readFromFile(ZipOutputStream zipOutputStream, Uri uri) throws IOException {
FileInputStream fileInputStream = null;
try {
fileInputStream = new FileInputStream(new File(uri.getPath()));
byte[] buffer = new byte[BUFFER_SIZE];
int byteCount = 0;
while ((byteCount = fileInputStream.read(buffer)) != -1) {
zipOutputStream.write(buffer, 0, byteCount);
}
} finally {
if (fileInputStream != null) {
fileInputStream.close();
}
}
}
}
@@ -0,0 +1,231 @@
/*
* Copyright 2011 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.file.exporter;
import com.google.android.apps.mytracks.io.file.TrackFileFormat;
import com.google.android.apps.mytracks.util.DialogUtils;
import com.google.android.apps.mytracks.util.FileUtils;
import com.google.android.apps.mytracks.util.GoogleEarthUtils;
import com.google.android.apps.mytracks.util.IntentUtils;
import com.google.android.maps.mytracks.R;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.widget.Toast;
import java.io.File;
/**
* An activity for saving tracks to the external storage. If saving a specific
* track, option to save it to a temp directory and play the track afterward.
*
* @author Rodrigo Damazio
*/
public class SaveActivity extends Activity {
public static final String EXTRA_TRACK_FILE_FORMAT = "track_file_format";
public static final String EXTRA_TRACK_IDS = "track_ids";
public static final String EXTRA_PLAY_TRACK = "play_track";
private static final int DIALOG_PROGRESS_ID = 0;
private static final int DIALOG_RESULT_ID = 1;
private TrackFileFormat trackFileFormat;
private long[] trackIds;
private boolean playTrack;
private String directoryDisplayName;
private SaveAsyncTask saveAsyncTask;
private ProgressDialog progressDialog;
// the number of tracks successfully saved
private int successCount;
// the number of tracks to save
private int totalCount;
// the last successfully saved path
private String savedPath;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent intent = getIntent();
trackFileFormat = intent.getParcelableExtra(EXTRA_TRACK_FILE_FORMAT);
trackIds = intent.getLongArrayExtra(EXTRA_TRACK_IDS);
playTrack = intent.getBooleanExtra(EXTRA_PLAY_TRACK, false);
if (!FileUtils.isExternalStorageWriteable()) {
Toast.makeText(this, R.string.external_storage_not_writable, Toast.LENGTH_LONG).show();
finish();
return;
}
File directory = playTrack ? new File(getCacheDir(), FileUtils.PLAY_TRACKS_DIR)
: new File(FileUtils.getPath(trackFileFormat.getExtension()));
if (!FileUtils.ensureDirectoryExists(directory)) {
Toast.makeText(this, R.string.external_storage_not_writable, Toast.LENGTH_LONG).show();
finish();
return;
}
if (playTrack) {
for (File file : directory.listFiles()) {
file.delete();
}
}
directoryDisplayName = playTrack ? directory.getName()
: FileUtils.getPathDisplayName(trackFileFormat.getExtension());
Object retained = getLastNonConfigurationInstance();
if (retained instanceof SaveAsyncTask) {
saveAsyncTask = (SaveAsyncTask) retained;
saveAsyncTask.setActivity(this);
} else {
saveAsyncTask = new SaveAsyncTask(this, trackIds, trackFileFormat, playTrack, directory);
saveAsyncTask.execute();
}
}
@Override
public Object onRetainNonConfigurationInstance() {
saveAsyncTask.setActivity(null);
return saveAsyncTask;
}
@Override
protected Dialog onCreateDialog(int id) {
switch (id) {
case DIALOG_PROGRESS_ID:
progressDialog = DialogUtils.createHorizontalProgressDialog(this,
R.string.export_external_storage_progress_message,
new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
saveAsyncTask.cancel(true);
dialog.dismiss();
}
}, directoryDisplayName);
return progressDialog;
case DIALOG_RESULT_ID:
int iconId;
int titleId;
String message;
String totalTracks = getResources()
.getQuantityString(R.plurals.tracks, totalCount, totalCount);
if (successCount == totalCount) {
iconId = R.drawable.ic_dialog_success;
titleId = R.string.generic_success_title;
message = getString(
R.string.export_external_storage_success, totalTracks, directoryDisplayName);
} else {
iconId = android.R.drawable.ic_dialog_alert;
titleId = R.string.generic_error_title;
message = getString(R.string.export_external_storage_error, successCount, totalTracks,
directoryDisplayName);
}
AlertDialog.Builder builder = new AlertDialog.Builder(this).setCancelable(true)
.setIcon(iconId).setMessage(message)
.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
dialog.dismiss();
finish();
}
}).setPositiveButton(R.string.generic_ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int arg1) {
dialog.dismiss();
finish();
}
}).setTitle(titleId);
if (!playTrack && trackIds.length == 1 && trackIds[0] != -1L && successCount == totalCount
&& savedPath != null) {
builder.setNegativeButton(
R.string.share_track_share_file, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Intent intent = IntentUtils.newShareFileIntent(
SaveActivity.this, trackIds[0], savedPath, trackFileFormat);
startActivity(
Intent.createChooser(intent, getString(R.string.share_track_picker_title)));
finish();
}
});
}
final Dialog dialog = builder.create();
dialog.setOnShowListener(new DialogInterface.OnShowListener() {
@Override
public void onShow(DialogInterface dialogInterface) {
DialogUtils.setDialogTitleDivider(SaveActivity.this, dialog);
}
});
return dialog;
default:
return null;
}
}
/**
* Invokes when the associated AsyncTask completes.
*
* @param aSuccessCount the number of tracks successfully saved
* @param aTotalCount the number of tracks to save
* @param aSavedPath the last successfully saved path
*/
public void onAsyncTaskCompleted(int aSuccessCount, int aTotalCount, String aSavedPath) {
successCount = aSuccessCount;
totalCount = aTotalCount;
savedPath = aSavedPath;
removeDialog(DIALOG_PROGRESS_ID);
if (playTrack && successCount == 1 && totalCount == 1 && savedPath != null) {
startActivity(GoogleEarthUtils.getPlayInEarthIntent(this, savedPath));
finish();
} else {
showDialog(DIALOG_RESULT_ID);
}
}
/**
* Shows the progress dialog.
*/
public void showProgressDialog() {
showDialog(DIALOG_PROGRESS_ID);
}
/**
* Sets the progress dialog value.
*
* @param number the number of points saved
* @param max the maximum number of points
*/
public void setProgressDialogValue(int number, int max) {
if (progressDialog != null) {
progressDialog.setIndeterminate(false);
progressDialog.setMax(max);
progressDialog.setProgress(Math.min(number, max));
}
}
}
@@ -0,0 +1,268 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.file.exporter;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.TracksColumns;
import com.google.android.apps.mytracks.io.file.TrackFileFormat;
import com.google.android.apps.mytracks.util.FileUtils;
import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.android.apps.mytracks.util.SystemUtils;
import com.google.android.maps.mytracks.R;
import android.content.Context;
import android.database.Cursor;
import android.os.AsyncTask;
import android.os.PowerManager.WakeLock;
import android.util.Log;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
/**
* Async Task to save tracks to the external storage.
*
* @author Jimmy Shih
*/
public class SaveAsyncTask extends AsyncTask<Void, Integer, Boolean> {
private static final String TAG = SaveAsyncTask.class.getSimpleName();
private SaveActivity saveActivity;
private final long[] trackIds;
private final TrackFileFormat trackFileFormat;
private final boolean playTrack;
private final File directory;
private final Context context;
private final MyTracksProviderUtils myTracksProviderUtils;
private WakeLock wakeLock;
private TrackExporter trackExporter;
// true if the AsyncTask has completed
private boolean completed;
// the number of tracks successfully saved
private int successCount;
// the number of tracks to save
private int totalCount;
// the last successfully saved path
private String savedPath;
/**
* Creates an AsyncTask.
*
* @param saveActivity the activity currently associated with this task
* @param trackIds the track ids to save. To save all, set to size 1 with
* trackIds[0] == -1L
* @param trackFileFormat the track file format
* @param playTrack true to play track
* @param directory the directory to write the file
*/
public SaveAsyncTask(SaveActivity saveActivity, long[] trackIds, TrackFileFormat trackFileFormat,
boolean playTrack, File directory) {
this.saveActivity = saveActivity;
this.trackIds = trackIds;
this.trackFileFormat = trackFileFormat;
this.playTrack = playTrack;
this.directory = directory;
context = saveActivity.getApplicationContext();
myTracksProviderUtils = MyTracksProviderUtils.Factory.get(context);
completed = false;
successCount = 0;
totalCount = 0;
savedPath = null;
}
/**
* Sets the current activity associated with this AyncTask.
*
* @param saveActivity the current activity, can be null
*/
public void setActivity(SaveActivity saveActivity) {
this.saveActivity = saveActivity;
if (completed && saveActivity != null) {
saveActivity.onAsyncTaskCompleted(successCount, totalCount, savedPath);
}
}
@Override
protected void onPreExecute() {
if (saveActivity != null) {
saveActivity.showProgressDialog();
}
}
@Override
protected Boolean doInBackground(Void... params) {
try {
Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
boolean isRecording = PreferencesUtils.getLong(saveActivity, R.string.recording_track_id_key)
!= PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
boolean isPaused = PreferencesUtils.getBoolean(saveActivity,
R.string.recording_track_paused_key, PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT);
// Get the wake lock if not recording or paused
if (!isRecording || isPaused) {
wakeLock = SystemUtils.acquireWakeLock(saveActivity, wakeLock);
}
if (trackIds.length == 1 && trackIds[0] == -1L) {
return saveAllTracks();
} else {
totalCount = 1;
Track[] tracks = new Track[trackIds.length];
for (int i = 0; i < trackIds.length; i++) {
tracks[i] = myTracksProviderUtils.getTrack(trackIds[i]);
if (tracks[i] == null) {
Log.d(TAG, "No track for " + trackIds[i]);
return false;
}
}
if (saveTracks(tracks)) {
successCount = 1;
return true;
} else {
return false;
}
}
} finally {
if (wakeLock != null && wakeLock.isHeld()) {
wakeLock.release();
}
}
}
@Override
protected void onProgressUpdate(Integer... values) {
if (saveActivity != null) {
saveActivity.setProgressDialogValue(values[0], values[1]);
}
}
@Override
protected void onPostExecute(Boolean result) {
completed = true;
if (saveActivity != null) {
saveActivity.onAsyncTaskCompleted(successCount, totalCount, savedPath);
}
}
@Override
protected void onCancelled() {
completed = true;
if (saveActivity != null) {
saveActivity.onAsyncTaskCompleted(successCount, totalCount, null);
}
}
/**
* Saves tracks to one file.
*
* @param tracks the tracks
*/
private Boolean saveTracks(Track[] tracks) {
if (tracks.length == 0) {
return false;
}
Track track = tracks[0];
boolean useKmz = trackFileFormat == TrackFileFormat.KML && !playTrack;
String extension = useKmz ? KmzTrackExporter.KMZ_EXTENSION : trackFileFormat.getExtension();
FileTrackExporter fileTrackExporter = new FileTrackExporter(myTracksProviderUtils, tracks,
trackFileFormat.newTrackWriter(context, tracks.length > 1, playTrack),
new TrackExporterListener() {
@Override
public void onProgressUpdate(int number, int max) {
/*
* If only saving one track, update the progress dialog once every
* 500 points
*/
if (trackIds.length == 1 && trackIds[0] != -1L && number % 500 == 0) {
publishProgress(number, max);
}
}
});
trackExporter = useKmz ? new KmzTrackExporter(
myTracksProviderUtils, fileTrackExporter, tracks, context)
: fileTrackExporter;
String fileName = FileUtils.buildUniqueFileName(directory, track.getName(), extension);
File file = new File(directory, fileName);
FileOutputStream fileOutputStream = null;
try {
fileOutputStream = new FileOutputStream(file);
if (trackExporter.writeTrack(fileOutputStream)) {
savedPath = file.getAbsolutePath();
return true;
} else {
if (!file.delete()) {
Log.d(TAG, "Unable to delete file");
}
Log.e(TAG, "Unable to export track");
return false;
}
} catch (FileNotFoundException e) {
Log.e(TAG, "Unable to open file " + file.getName(), e);
return false;
} finally {
if (fileOutputStream != null) {
try {
fileOutputStream.close();
} catch (IOException e) {
Log.e(TAG, "Unable to close file output stream", e);
}
}
}
}
/**
* Saves all the tracks.
*/
private Boolean saveAllTracks() {
Cursor cursor = null;
try {
cursor = myTracksProviderUtils.getTrackCursor(null, null, TracksColumns._ID);
if (cursor == null) {
return false;
}
totalCount = cursor.getCount();
for (int i = 0; i < totalCount; i++) {
if (isCancelled()) {
return false;
}
cursor.moveToPosition(i);
Track track = myTracksProviderUtils.createTrack(cursor);
if (track != null && saveTracks(new Track[] { track })) {
successCount++;
}
publishProgress(i + 1, totalCount);
}
return true;
} finally {
if (cursor != null) {
cursor.close();
}
}
}
}
@@ -0,0 +1,342 @@
/*
* 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.file.exporter;
import com.google.android.apps.mytracks.content.MyTracksLocation;
import com.google.android.apps.mytracks.content.Sensor;
import com.google.android.apps.mytracks.content.Sensor.SensorDataSet;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.io.file.TrackFileFormat;
import com.google.android.apps.mytracks.util.StringUtils;
import com.google.android.apps.mytracks.util.SystemUtils;
import com.google.android.apps.mytracks.util.UnitConversions;
import com.google.android.maps.mytracks.R;
import android.content.Context;
import android.location.Location;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.util.Locale;
/**
* Write track as TCX to a file. See http://developer.garmin.com/schemas/tcx/v2/
* for info on TCX. <br>
* The TCX file output is verified by uploading the file to
* http://connect.garmin.com/.
*
* @author Sandor Dornbush
* @author Dominik Ršttsches
*/
public class TcxTrackWriter implements TrackWriter {
/**
* TCX sport type. See the TCX spec.
*
* @author Jimmy Shih
*/
private enum SportType {
RUNNING("Running"),
BIKING("Biking"),
OTHER("Other");
private final String name;
private SportType(String name) {
this.name = name;
}
/**
* Gets the name of the sport type
*/
public String getName() {
return name;
}
}
// My Tracks categories that are considered as TCX biking sport type.
private static final int TCX_SPORT_BIKING_IDS[] = {
R.string.activity_type_cycling,
R.string.activity_type_dirt_bike,
R.string.activity_type_mountain_biking,
R.string.activity_type_road_biking,
R.string.activity_type_track_cycling };
// My Tracks categories that are considered as TCX running sport type.
private static final int TCX_SPORT_RUNNING_IDS[] = {
R.string.activity_type_running,
R.string.activity_type_speed_walking,
R.string.activity_type_street_running,
R.string.activity_type_track_running,
R.string.activity_type_trail_running,
R.string.activity_type_walking };
private final Context context;
private PrintWriter printWriter;
private SportType sportType;
public TcxTrackWriter(Context context) {
this.context = context;
}
@Override
public void prepare(OutputStream outputStream) {
this.printWriter = new PrintWriter(outputStream);
}
@Override
public void close() {
if (printWriter != null) {
printWriter.flush();
printWriter = null;
}
}
@Override
public String getExtension() {
return TrackFileFormat.TCX.getExtension();
}
@Override
public void writeHeader(Track[] tracks) {
if (printWriter != null) {
printWriter.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
printWriter.println("<TrainingCenterDatabase"
+ " xmlns=\"http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2\"");
printWriter.println("xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"");
printWriter.println("xsi:schemaLocation="
+ "\"http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2"
+ " http://www.garmin.com/xmlschemas/TrainingCenterDatabasev2.xsd\">");
}
}
@Override
public void writeFooter() {
if (printWriter != null) {
printWriter.println("<Author xsi:type=\"Application_t\">");
printWriter.println("<Name>"
+ StringUtils.formatCData(context.getString(R.string.send_google_by_my_tracks, "", ""))
+ "</Name>");
// <Build>, <LangID>, and <PartNumber> are required by type=Application_t.
printWriter.println("<Build>");
writeVersion();
printWriter.println("</Build>");
printWriter.println("<LangID>" + Locale.getDefault().getLanguage() + "</LangID>");
printWriter.println("<PartNumber>000-00000-00</PartNumber>");
printWriter.println("</Author>");
printWriter.println("</TrainingCenterDatabase>");
}
}
@Override
public void writeBeginWaypoints(Track track) {
// Do nothing
}
@Override
public void writeEndWaypoints() {
// Do nothing
}
@Override
public void writeWaypoint(Waypoint waypoint) {
// Do nothing
}
@Override
public void writeBeginTracks() {
// Do nothing
}
@Override
public void writeEndTracks() {
// Do nothing
}
@Override
public void writeBeginTrack(Track track, Location startLocation) {
sportType = getSportType(track.getCategory());
if (printWriter != null) {
String startTime = StringUtils.formatDateTimeIso8601(
track.getTripStatistics().getStartTime());
long totalTimeInSeconds = (long) (track.getTripStatistics().getTotalTime()
* UnitConversions.MS_TO_S);
printWriter.println("<Activities>");
printWriter.println("<Activity Sport=\"" + sportType.getName() + "\">");
printWriter.println("<Id>" + startTime + "</Id>");
printWriter.println("<Lap StartTime=\"" + startTime + "\">");
printWriter.println("<TotalTimeSeconds>" + totalTimeInSeconds + "</TotalTimeSeconds>");
printWriter.println("<DistanceMeters>" + track.getTripStatistics().getTotalDistance()
+ "</DistanceMeters>");
// <Calories> is required, just put in 0.
printWriter.println("<Calories>0</Calories>");
printWriter.println("<Intensity>Active</Intensity>");
printWriter.println("<TriggerMethod>Manual</TriggerMethod>");
}
}
@Override
public void writeEndTrack(Track track, Location endLocation) {
if (printWriter != null) {
printWriter.println("</Lap>");
printWriter.println("<Notes>" + StringUtils.formatCData(track.getDescription()) + "</Notes>");
printWriter.println("<Creator xsi:type=\"Device_t\">");
printWriter.println("<Name>"
+ StringUtils.formatCData(context.getString(R.string.send_google_by_my_tracks, "", ""))
+ "</Name>");
// <UnitId>, <ProductID>, and <Version> are required for type=Device_t.
printWriter.println("<UnitId>0</UnitId>");
printWriter.println("<ProductID>0</ProductID>");
writeVersion();
printWriter.println("</Creator>");
printWriter.println("</Activity>");
printWriter.println("</Activities>");
}
}
@Override
public void writeOpenSegment() {
if (printWriter != null) {
printWriter.println("<Track>");
}
}
@Override
public void writeCloseSegment() {
if (printWriter != null) {
printWriter.println("</Track>");
}
}
@Override
public void writeLocation(Location location) {
if (printWriter != null) {
printWriter.println("<Trackpoint>");
printWriter.println("<Time>" + StringUtils.formatDateTimeIso8601(location.getTime()) + "</Time>");
printWriter.println("<Position>");
printWriter.println("<LatitudeDegrees>" + location.getLatitude() + "</LatitudeDegrees>");
printWriter.println("<LongitudeDegrees>" + location.getLongitude() + "</LongitudeDegrees>");
printWriter.println("</Position>");
if (location.hasAltitude()) {
printWriter.println("<AltitudeMeters>" + location.getAltitude() + "</AltitudeMeters>");
}
if (location instanceof MyTracksLocation) {
SensorDataSet sensorDataSet = ((MyTracksLocation) location).getSensorDataSet();
if (sensorDataSet != null) {
boolean heartRateAvailable = sensorDataSet.hasHeartRate()
&& sensorDataSet.getHeartRate().hasValue()
&& sensorDataSet.getHeartRate().getState() == Sensor.SensorState.SENDING;
boolean cadenceAvailable = sensorDataSet.hasCadence()
&& sensorDataSet.getCadence().hasValue()
&& sensorDataSet.getCadence().getState() == Sensor.SensorState.SENDING;
boolean powerAvailable = sensorDataSet.hasPower()
&& sensorDataSet.getPower().hasValue()
&& sensorDataSet.getPower().getState() == Sensor.SensorState.SENDING;
if (heartRateAvailable) {
printWriter.println("<HeartRateBpm>");
printWriter.println("<Value>" + sensorDataSet.getHeartRate().getValue() + "</Value>");
printWriter.println("</HeartRateBpm>");
}
// <Cadence> needs to be put before <Extensions>.
// According to the TCX spec, <Cadence> is only for the biking sport
// type. For others, use <RunCadence> in <Extensions>.
if (cadenceAvailable && sportType == SportType.BIKING) {
// The spec requires the max value be 254.
printWriter.println(
"<Cadence>" + Math.min(254, sensorDataSet.getCadence().getValue()) + "</Cadence>");
}
if ((cadenceAvailable && sportType != SportType.BIKING) || powerAvailable) {
printWriter.println("<Extensions>");
printWriter.println(
"<TPX xmlns=\"http://www.garmin.com/xmlschemas/ActivityExtension/v2\">");
// <RunCadence> needs to be put before <Watts>.
if (cadenceAvailable && sportType != SportType.BIKING) {
// The spec requires the max value to be 254.
printWriter.println("<RunCadence>"
+ Math.min(254, sensorDataSet.getCadence().getValue()) + "</RunCadence>");
}
if (powerAvailable) {
printWriter.println("<Watts>" + sensorDataSet.getPower().getValue() + "</Watts>");
}
printWriter.println("</TPX>");
printWriter.println("</Extensions>");
}
}
}
printWriter.println("</Trackpoint>");
}
}
/**
* Writes the TCX Version.
*/
private void writeVersion() {
// Split the My Tracks version code into VersionMajor, VersionMinor, and,
// BuildMajor to fit the integer type requirement for these fields in the
// TCX spec.
String[] versionComponents = SystemUtils.getMyTracksVersion(context).split("\\.");
int versionMajor = versionComponents.length > 0 ? Integer.valueOf(versionComponents[0]) : 0;
int versionMinor = versionComponents.length > 1 ? Integer.valueOf(versionComponents[1]) : 0;
int buildMajor = versionComponents.length > 2 ? Integer.valueOf(versionComponents[2]) : 0;
printWriter.println("<Version>");
printWriter.println("<VersionMajor>" + versionMajor + "</VersionMajor>");
printWriter.println("<VersionMinor>" + versionMinor + "</VersionMinor>");
// According to TCX spec, these are optional. But http://connect.garmin.com
// requires them.
printWriter.println("<BuildMajor>" + buildMajor + "</BuildMajor>");
printWriter.println("<BuildMinor>0</BuildMinor>");
printWriter.println("</Version>");
}
/**
* Gets the sport type from the category.
*
* @param category the category
*/
private SportType getSportType(String category) {
category = category.trim();
// For tracks with localized category.
for (int i : TCX_SPORT_RUNNING_IDS) {
if (category.equalsIgnoreCase(context.getString(i))) {
return SportType.RUNNING;
}
}
for (int i : TCX_SPORT_BIKING_IDS) {
if (category.equalsIgnoreCase(context.getString(i))) {
return SportType.BIKING;
}
}
// For tracks without localized category.
if (category.equalsIgnoreCase(SportType.RUNNING.getName())) {
return SportType.RUNNING;
} else if (category.equalsIgnoreCase(SportType.BIKING.getName())) {
return SportType.BIKING;
} else {
return SportType.OTHER;
}
}
}
@@ -0,0 +1,34 @@
/*
* Copyright 2011 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.file.exporter;
import java.io.OutputStream;
/**
* Track exporting for exporting track to an {@link OutputStream}.
*
* @author Jimmy Shih
*/
public interface TrackExporter {
/**
* Write track to an output stream
*
* @param outputStream the output stream
*/
boolean writeTrack(OutputStream outputStream);
}
@@ -0,0 +1,33 @@
/*
* 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.file.exporter;
/**
* Listener for {@link TrackExporter} progress.
*
* @author Jimmy Shih
*/
public interface TrackExporterListener {
/**
* Called to update progress.
*
* @param number the number of locations written
* @param max the maximum number of locations in a track, for calculation of
* completion percentage
*/
public void onProgressUpdate(int number, int max);
}
@@ -0,0 +1,144 @@
/*
* 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.file.exporter;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.Waypoint;
import android.location.Location;
import java.io.OutputStream;
/**
* Interface for writing tracks to a file. The expected sequence of calls is:
*
* <pre>
* {@link #prepare(OutputStream)}
* {@link #writeHeader(Track[])}
* For each track:
* {@link #writeBeginWaypoints(Track)}
* For each waypoint:
* {@link #writeWaypoint(Waypoint)}
* {@link #writeEndWaypoints()}
* {@link #writeBeginTracks()}
* For each track:
* {@link #writeBeginTrack(Track, Location)}
* For each segment:
* {@link #writeOpenSegment()}
* For each location in the segment:
* {@link #writeLocation(Location)}
* {@link #writeCloseSegment()}
* {@link #writeEndTrack(Track, Location)}
* {@link #writeEndTracks()}
* {@link #writeFooter()}
* {@link #close()}
* </pre>
*
* @author Rodrigo Damazio
*/
public interface TrackWriter {
/**
* Gets the file extension (e.g, gpx, kml, ...).
*/
public String getExtension();
/**
* Prepares the output stream.
*
* @param outputStream the output stream
*/
public void prepare(OutputStream outputStream);
/**
* Closes the output stream.
*/
public void close();
/**
* Writes the header
*
* @param tracks the tracks
*/
public void writeHeader(Track[] tracks);
/**
* Writes the footer.
*/
public void writeFooter();
/**
* Writes the beginning of the waypoints.
*
* @param track the track
*/
public void writeBeginWaypoints(Track track);
/**
* Writes the end of the waypoints.
*/
public void writeEndWaypoints();
/**
* Writes a waypoint.
*
* @param waypoint the waypoint
*/
public void writeWaypoint(Waypoint waypoint);
/**
* Writes the beginning of the tracks.
*/
public void writeBeginTracks();
/**
* Writes the end of the tracks,
*/
public void writeEndTracks();
/**
* Writes the beginning of a track.
*
* @param track the track
* @param startLocation the start location
*/
public void writeBeginTrack(Track track, Location startLocation);
/**
* Writes the end of a track.
*
* @param track the track
* @param endLocation the end location
*/
public void writeEndTrack(Track track, Location endLocation);
/**
* Writes open segment.
*/
public void writeOpenSegment();
/**
* Writes close segment.
*/
public void writeCloseSegment();
/**
* Writes a location.
*
* @param location the location
*/
public void writeLocation(Location location);
}
@@ -0,0 +1,630 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.file.importer;
import com.google.android.apps.mytracks.content.DescriptionGeneratorImpl;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils.LocationIterator;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.content.Waypoint.WaypointType;
import com.google.android.apps.mytracks.services.TrackRecordingService;
import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.android.apps.mytracks.stats.TripStatisticsUpdater;
import com.google.android.apps.mytracks.util.CalorieUtils;
import com.google.android.apps.mytracks.util.CalorieUtils.ActivityType;
import com.google.android.apps.mytracks.util.FileUtils;
import com.google.android.apps.mytracks.util.LocationUtils;
import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.android.apps.mytracks.util.StringUtils;
import com.google.android.apps.mytracks.util.TrackIconUtils;
import com.google.android.apps.mytracks.util.UnitConversions;
import com.google.android.maps.mytracks.R;
import android.content.Context;
import android.location.Location;
import android.location.LocationManager;
import android.net.Uri;
import android.util.Log;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
/**
* Abstract class for various file track importers like {@link GpxFileTrackImporter} and
* {@link KmlFileTrackImporter}.
*
* @author Jimmy Shih
*/
abstract class AbstractFileTrackImporter extends DefaultHandler implements TrackImporter {
/**
* Data for the current track.
*
* @author Jimmy Shih
*/
private class TrackData {
// The current track
Track track = new Track();
// The number of segments processed for the current track
int numberOfSegments = 0;
/*
* The last location in the current segment. Null if the current segment
* doesn't have a last location.
*/
Location lastLocationInCurrentSegment;
// The number of locations processed for the current track
int numberOfLocations = 0;
// The trip statistics updater for the current track
TripStatisticsUpdater tripStatisticsUpdater;
// The import time of the track.
long importTime = System.currentTimeMillis();
// The buffered locations
Location[] bufferedLocations = new Location[MAX_BUFFERED_LOCATIONS];
// The number of buffered locations
int numBufferedLocations = 0;
}
private static final String TAG = AbstractFileTrackImporter.class.getSimpleName();
// The maximum number of buffered locations for bulk-insertion
private static final int MAX_BUFFERED_LOCATIONS = 512;
private final Context context;
private final long importTrackId;
private final MyTracksProviderUtils myTracksProviderUtils;
private final int recordingDistanceInterval;
private final double weight;
private final List<Long> trackIds;
private final List<Waypoint> waypoints;
// The current track data
private TrackData trackData;
// The SAX locator to get the current line information
private Locator locator;
// The current element content
protected String content;
protected String name;
protected String description;
protected String category;
protected String latitude;
protected String longitude;
protected String altitude;
protected String time;
protected String waypointType;
protected String photoUrl;
/**
* Constructor.
*
* @param context the context
* @param importTrackId the track id to import to. -1L to import to a new
* track.
*/
AbstractFileTrackImporter(
Context context, long importTrackId, MyTracksProviderUtils myTracksProviderUtils) {
this.context = context;
this.importTrackId = importTrackId;
this.myTracksProviderUtils = myTracksProviderUtils;
this.recordingDistanceInterval = PreferencesUtils.getInt(context,
R.string.recording_distance_interval_key,
PreferencesUtils.RECORDING_DISTANCE_INTERVAL_DEFAULT);
this.weight = PreferencesUtils.getFloat(
context, R.string.weight_key, PreferencesUtils.getDefaultWeight(context));
trackIds = new ArrayList<Long>();
waypoints = new ArrayList<Waypoint>();
}
@Override
public void setDocumentLocator(Locator locator) {
this.locator = locator;
}
@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 long importFile(InputStream inputStream) {
try {
SAXParser saxParser = SAXParserFactory.newInstance().newSAXParser();
long start = System.currentTimeMillis();
saxParser.parse(inputStream, this);
Log.d(TAG, "Total import time: " + (System.currentTimeMillis() - start) + "ms");
if (trackIds.size() != 1) {
Log.d(TAG, trackIds.size() + " tracks imported");
cleanImport();
return -1L;
}
return trackIds.get(0);
} catch (IOException e) {
Log.e(TAG, "Unable to import file", e);
cleanImport();
return -1L;
} catch (ParserConfigurationException e) {
Log.e(TAG, "Unable to import file", e);
cleanImport();
return -1L;
} catch (SAXException e) {
Log.e(TAG, "Unable to import file", e);
cleanImport();
return -1L;
}
}
/**
* On file end.
*/
protected void onFileEnd() {
// Add waypoints to the last imported track
int size = trackIds.size();
if (size == 0) {
return;
}
long trackId = trackIds.get(size - 1);
Track track = myTracksProviderUtils.getTrack(trackId);
if (track == null) {
return;
}
int waypointPosition = -1;
Waypoint waypoint = null;
Location location = null;
TripStatisticsUpdater trackTripStatisticstrackUpdater = new TripStatisticsUpdater(
track.getTripStatistics().getStartTime());
TripStatisticsUpdater markerTripStatisticsUpdater = new TripStatisticsUpdater(
track.getTripStatistics().getStartTime());
LocationIterator locationIterator = null;
ActivityType activityType = CalorieUtils.getActivityType(context, track.getCategory());
try {
locationIterator = myTracksProviderUtils.getTrackPointLocationIterator(
track.getId(), -1L, false, MyTracksProviderUtils.DEFAULT_LOCATION_FACTORY);
while (true) {
if (waypoint == null) {
waypointPosition++;
waypoint = waypointPosition < waypoints.size() ? waypoints.get(waypointPosition) : null;
if (waypoint == null) {
// No more waypoints
return;
}
}
if (location == null) {
if (!locationIterator.hasNext()) {
// No more track points. Ignore the rest of the waypoints.
return;
}
location = locationIterator.next();
trackTripStatisticstrackUpdater.addLocation(
location, recordingDistanceInterval, false, ActivityType.INVALID, 0.0);
markerTripStatisticsUpdater.addLocation(
location, recordingDistanceInterval, true, activityType, weight);
}
if (waypoint.getLocation().getTime() > location.getTime()) {
location = null;
} else if (waypoint.getLocation().getTime() < location.getTime()) {
waypoint = null;
} else {
// The waypoint location time matches the track point time
if (!LocationUtils.isValidLocation(location)) {
// Invalid location, load the next location
location = null;
continue;
}
// Valid location
if (location.getLatitude() == waypoint.getLocation().getLatitude()
&& location.getLongitude() == waypoint.getLocation().getLongitude()) {
// Get tripStatistics, description, and icon
TripStatistics tripStatistics;
String waypointDescription;
String icon;
if (waypoint.getType() == WaypointType.STATISTICS) {
tripStatistics = markerTripStatisticsUpdater.getTripStatistics();
markerTripStatisticsUpdater = new TripStatisticsUpdater(location.getTime());
waypointDescription = new DescriptionGeneratorImpl(context)
.generateWaypointDescription(tripStatistics);
icon = context.getString(R.string.marker_statistics_icon_url);
} else {
tripStatistics = null;
waypointDescription = waypoint.getDescription();
icon = context.getString(R.string.marker_waypoint_icon_url);
}
// Get length and duration
double length = trackTripStatisticstrackUpdater.getTripStatistics().getTotalDistance();
long duration = trackTripStatisticstrackUpdater.getTripStatistics().getTotalTime();
// Insert waypoint
Waypoint newWaypoint = new Waypoint(waypoint.getName(), waypointDescription,
waypoint.getCategory(), icon, track.getId(), waypoint.getType(), length, duration,
-1L, -1L, location, tripStatistics, waypoint.getPhotoUrl());
myTracksProviderUtils.insertWaypoint(newWaypoint);
}
// Load the next waypoint
waypoint = null;
}
}
} finally {
if (locationIterator != null) {
locationIterator.close();
}
}
}
/**
* On track start.
*/
protected void onTrackStart() throws SAXException {
trackData = new TrackData();
long trackId;
if (importTrackId == -1L) {
Uri uri = myTracksProviderUtils.insertTrack(trackData.track);
trackId = Long.parseLong(uri.getLastPathSegment());
} else {
if (trackIds.size() > 0) {
throw new SAXException(createErrorMessage(
"Cannot import more than one track to an existing track " + importTrackId));
}
trackId = importTrackId;
myTracksProviderUtils.clearTrack(context, trackId);
}
trackIds.add(trackId);
trackData.track.setId(trackId);
}
/**
* On track end.
*/
protected void onTrackEnd() {
flushLocations(trackData);
if (name != null) {
trackData.track.setName(name);
}
if (description != null) {
trackData.track.setDescription(description);
}
if (category != null) {
trackData.track.setCategory(category);
trackData.track.setIcon(TrackIconUtils.getIconValue(context, category));
}
if (trackData.tripStatisticsUpdater == null) {
trackData.tripStatisticsUpdater = new TripStatisticsUpdater(trackData.importTime);
trackData.tripStatisticsUpdater.updateTime(trackData.importTime);
}
trackData.track.setTripStatistics(trackData.tripStatisticsUpdater.getTripStatistics());
trackData.track.setNumberOfPoints(trackData.numberOfLocations);
myTracksProviderUtils.updateTrack(trackData.track);
insertFirstWaypoint(trackData.track);
}
/**
* On track segment start.
*/
protected void onTrackSegmentStart() {
trackData.numberOfSegments++;
/*
* If not the first segment, add a pause separator if there is at least one
* location in the last segment.
*/
if (trackData.numberOfSegments > 1 && trackData.lastLocationInCurrentSegment != null) {
insertLocation(createLocation(TrackRecordingService.PAUSE_LATITUDE, 0.0, 0.0,
trackData.lastLocationInCurrentSegment.getTime()));
}
trackData.lastLocationInCurrentSegment = null;
}
/**
* Adds a waypoint.
*
* @param type the waypoint type
*/
protected void addWaypoint(WaypointType type) throws SAXException {
// Waypoint must have a time, else cannot match to the track points
if (time == null) {
return;
}
Waypoint waypoint = new Waypoint();
Location location = createLocation();
if (!LocationUtils.isValidLocation(location)) {
throw new SAXException(createErrorMessage("Invalid location detected: " + location));
}
waypoint.setLocation(location);
if (name != null) {
waypoint.setName(name);
}
if (description != null) {
waypoint.setDescription(description);
}
if (category != null) {
waypoint.setCategory(category);
}
waypoint.setType(type);
if (photoUrl != null) {
waypoint.setPhotoUrl(photoUrl);
}
waypoints.add(waypoint);
}
/**
* Gets a track point.
*/
protected Location getTrackPoint() throws SAXException {
Location location = createLocation();
// Calculate derived attributes from the previous point
if (trackData.lastLocationInCurrentSegment != null
&& trackData.lastLocationInCurrentSegment.getTime() != 0) {
long timeDifference = location.getTime() - trackData.lastLocationInCurrentSegment.getTime();
// Check for negative time change
if (timeDifference <= 0) {
Log.w(TAG, "Time difference not postive.");
} else {
/*
* We don't have a speed and bearing in GPX, make something up from the
* last two points. 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.
*/
double duration = timeDifference * UnitConversions.MS_TO_S;
double speed = trackData.lastLocationInCurrentSegment.distanceTo(location) / duration;
location.setSpeed((float) speed);
}
location.setBearing(trackData.lastLocationInCurrentSegment.bearingTo(location));
}
if (!LocationUtils.isValidLocation(location)) {
throw new SAXException(createErrorMessage("Invalid location detected: " + location));
}
if (trackData.numberOfSegments > 1 && trackData.lastLocationInCurrentSegment == null) {
/*
* If not the first segment, add a resume separator before adding the
* first location.
*/
insertLocation(
createLocation(TrackRecordingService.RESUME_LATITUDE, 0.0, 0.0, location.getTime()));
}
trackData.lastLocationInCurrentSegment = location;
return location;
}
/**
* Inserts a track point.
*
* @param location the location
*/
protected void insertTrackPoint(Location location) {
insertLocation(location);
if (trackData.track.getStartId() == -1L) {
// Flush the location to set the track start id and the track end id
flushLocations(trackData);
}
}
/**
* Creates an error message.
*
* @param message the message
*/
protected String createErrorMessage(String message) {
return String.format(Locale.US, "Parsing error at line: %d column: %d. %s",
locator.getLineNumber(), locator.getColumnNumber(), message);
}
/**
* Gets the photo url for a file.
*
* @param fileName the file name
*/
protected String getPhotoUrl(String fileName) {
if (importTrackId == -1L) {
return null;
}
File dir = FileUtils.getPhotoDir(importTrackId);
File file = new File(dir, fileName);
return Uri.fromFile(file).toString();
}
/**
* Creates a location.
*/
private Location createLocation() throws SAXException {
if (latitude == null || longitude == null) {
return null;
}
double latitudeValue;
double longitudeValue;
try {
latitudeValue = Double.parseDouble(latitude);
longitudeValue = Double.parseDouble(longitude);
} catch (NumberFormatException e) {
throw new SAXException(createErrorMessage(String.format(
Locale.US, "Unable to parse latitude longitude: %s %s", latitude, longitude)), e);
}
Double altitudeValue = null;
if (altitude != null) {
try {
altitudeValue = Double.parseDouble(altitude);
} catch (NumberFormatException e) {
throw new SAXException(
createErrorMessage(String.format(Locale.US, "Unable to parse altitude: %s", altitude)),
e);
}
}
long timeValue;
if (time == null) {
timeValue = trackData.importTime;
} else {
try {
timeValue = StringUtils.getTime(time);
} catch (IllegalArgumentException e) {
throw new SAXException(
createErrorMessage(String.format(Locale.US, "Unable to parse time: %s", time)), e);
}
}
return createLocation(latitudeValue, longitudeValue, altitudeValue, timeValue);
}
/**
* Creates a location.
*
* @param latitudeValue the latitude value
* @param longitudeValue the longitude value
* @param altitudeValue the altitude value
* @param timeValue the time value
*/
private Location createLocation(
double latitudeValue, double longitudeValue, Double altitudeValue, long timeValue) {
Location location = new Location(LocationManager.GPS_PROVIDER);
location.setLatitude(latitudeValue);
location.setLongitude(longitudeValue);
if (altitudeValue != null) {
location.setAltitude(altitudeValue);
} else {
location.removeAltitude();
}
location.setTime(timeValue);
location.removeAccuracy();
location.removeBearing();
location.removeSpeed();
return location;
}
/**
* Inserts a location.
*
* @param location the location
*/
private void insertLocation(Location location) {
if (trackData.tripStatisticsUpdater == null) {
trackData.tripStatisticsUpdater = new TripStatisticsUpdater(
location.getTime() != -1L ? location.getTime() : trackData.importTime);
}
ActivityType activityType = CalorieUtils.getActivityType(context, category);
trackData.tripStatisticsUpdater.addLocation(
location, recordingDistanceInterval, true, activityType, weight);
trackData.bufferedLocations[trackData.numBufferedLocations] = location;
trackData.numBufferedLocations++;
trackData.numberOfLocations++;
if (trackData.numBufferedLocations >= MAX_BUFFERED_LOCATIONS) {
flushLocations(trackData);
}
}
/**
* Flushes the locations to the database.
*
* @param data the track data
*/
private void flushLocations(TrackData data) {
if (data.numBufferedLocations <= 0) {
return;
}
myTracksProviderUtils.bulkInsertTrackPoint(
data.bufferedLocations, data.numBufferedLocations, data.track.getId());
data.numBufferedLocations = 0;
if (data.track.getStartId() == -1L) {
data.track.setStartId(myTracksProviderUtils.getFirstTrackPointId(data.track.getId()));
}
data.track.setStopId(myTracksProviderUtils.getLastTrackPointId(data.track.getId()));
}
/**
* Inserts the first waypoint, the track statistics waypoint.
*
* @param track the track
*/
private void insertFirstWaypoint(Track track) {
String waypointName = context.getString(R.string.marker_split_name_format, 0);
String waypointCategory = "";
TripStatisticsUpdater updater = new TripStatisticsUpdater(
track.getTripStatistics().getStartTime());
TripStatistics tripStatistics = updater.getTripStatistics();
String waypointDescription = new DescriptionGeneratorImpl(context).generateWaypointDescription(
tripStatistics);
String icon = context.getString(R.string.marker_statistics_icon_url);
double length = 0.0;
long duration = 0L;
Location waypointLocation = new Location("");
waypointLocation.setLatitude(100);
waypointLocation.setLongitude(180);
Waypoint waypoint = new Waypoint(waypointName, waypointDescription, waypointCategory, icon,
track.getId(), WaypointType.STATISTICS, length, duration, -1L, -1L, waypointLocation,
tripStatistics, "");
myTracksProviderUtils.insertWaypoint(waypoint);
}
/**
* Cleans up import.
*/
private void cleanImport() {
for (long trackId : trackIds) {
myTracksProviderUtils.deleteTrack(context, trackId);
}
}
}
@@ -0,0 +1,174 @@
/*
* 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.file.importer;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.Waypoint.WaypointType;
import com.google.common.annotations.VisibleForTesting;
import android.content.Context;
import android.location.Location;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
/**
* Imports a GPX file.
*
* @author Jimmy Shih
*/
public class GpxFileTrackImporter extends AbstractFileTrackImporter {
private static final String TAG_DESCRIPTION = "desc";
private static final String TAG_COMMENT = "cmt";
private static final String TAG_ELEVATION = "ele";
private static final String TAG_GPX = "gpx";
private static final String TAG_NAME = "name";
private static final String TAG_TIME = "time";
private static final String TAG_TRACK = "trk";
private static final String TAG_TRACK_POINT = "trkpt";
private static final String TAG_TRACK_SEGMENT = "trkseg";
private static final String TAG_TYPE = "type";
private static final String TAG_WAYPOINT = "wpt";
private static final String ATTRIBUTE_LAT = "lat";
private static final String ATTRIBUTE_LON = "lon";
/**
* Constructor.
*
* @param context the context
*/
public GpxFileTrackImporter(Context context) {
this(context, MyTracksProviderUtils.Factory.get(context));
}
@VisibleForTesting
GpxFileTrackImporter(Context context, MyTracksProviderUtils myTracksProviderUtils) {
super(context, -1L, myTracksProviderUtils);
}
@Override
public void startElement(String uri, String localName, String tag, Attributes attributes)
throws SAXException {
if (tag.equals(TAG_WAYPOINT)) {
onWaypointStart(attributes);
} else if (tag.equals(TAG_TRACK)) {
onTrackStart();
} else if (tag.equals(TAG_TRACK_SEGMENT)) {
onTrackSegmentStart();
} else if (tag.equals(TAG_TRACK_POINT)) {
onTrackPointStart(attributes);
}
}
@Override
public void endElement(String uri, String localName, String tag) throws SAXException {
if (tag.equals(TAG_GPX)) {
onFileEnd();
} else if (tag.equals(TAG_WAYPOINT)) {
onWaypointEnd();
} else if (tag.equals(TAG_TRACK)) {
onTrackEnd();
} else if (tag.equals(TAG_TRACK_POINT)) {
onTrackPointEnd();
} else if (tag.equals(TAG_NAME)) {
if (content != null) {
name = content.trim();
}
} else if (tag.equals(TAG_DESCRIPTION)) {
if (content != null) {
description = content.trim();
}
} else if (tag.equals(TAG_TYPE)) {
if (content != null) {
category = content.trim();
}
} else if (tag.equals(TAG_TIME)) {
if (content != null) {
time = content.trim();
}
} else if (tag.equals(TAG_ELEVATION)) {
if (content != null) {
altitude = content.trim();
}
} else if (tag.equals(TAG_COMMENT)) {
if (content != null) {
waypointType = content.trim();
}
}
// Reset element content
content = null;
}
@Override
protected void onTrackStart() throws SAXException {
super.onTrackStart();
name = null;
description = null;
category = null;
}
/**
* On track point start.
*
* @param attributes the attributes
*/
private void onTrackPointStart(Attributes attributes) {
latitude = attributes.getValue(ATTRIBUTE_LAT);
longitude = attributes.getValue(ATTRIBUTE_LON);
altitude = null;
time = null;
}
/**
* On track point end.
*/
private void onTrackPointEnd() throws SAXException {
Location location = getTrackPoint();
if (location == null) {
return;
}
insertTrackPoint(location);
}
/**
* On waypoint start.
*
* @param attributes the attributes
*/
private void onWaypointStart(Attributes attributes) {
name = null;
description = null;
category = null;
photoUrl = null;
latitude = attributes.getValue(ATTRIBUTE_LAT);
longitude = attributes.getValue(ATTRIBUTE_LON);
altitude = null;
time = null;
waypointType = null;
}
/**
* On waypoint end.
*/
private void onWaypointEnd() throws SAXException {
addWaypoint(WaypointType.STATISTICS.name().equals(waypointType) ? WaypointType.STATISTICS
: WaypointType.WAYPOINT);
}
}
@@ -0,0 +1,241 @@
/*
* Copyright 2011 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.file.importer;
import com.google.android.apps.mytracks.TrackDetailActivity;
import com.google.android.apps.mytracks.io.file.TrackFileFormat;
import com.google.android.apps.mytracks.util.DialogUtils;
import com.google.android.apps.mytracks.util.FileUtils;
import com.google.android.apps.mytracks.util.IntentUtils;
import com.google.android.apps.mytracks.util.UriUtils;
import com.google.android.maps.mytracks.R;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.support.v4.app.TaskStackBuilder;
import android.util.Log;
import android.widget.Toast;
import java.io.File;
/**
* An activity to import files from the external storage. Optionally to import
* one specific file.
*
* @author Rodrigo Damazio
*/
public class ImportActivity extends Activity {
public static final String EXTRA_IMPORT_ALL = "import_all";
public static final String EXTRA_TRACK_FILE_FORMAT = "track_file_format";
private static final String TAG = ImportActivity.class.getSimpleName();
private static final int DIALOG_PROGRESS_ID = 0;
private static final int DIALOG_RESULT_ID = 1;
private ImportAsyncTask importAsyncTask;
private ProgressDialog progressDialog;
private boolean importAll;
private TrackFileFormat trackFileFormat;
// the path on the external storage to import
private String directoryDisplayName;
// the number of files successfully imported
private int successCount;
// the number of files to import
private int totalCount;
// the last successfully imported track id
private long trackId;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent intent = getIntent();
importAll = intent.getBooleanExtra(EXTRA_IMPORT_ALL, false);
trackFileFormat = intent.getParcelableExtra(EXTRA_TRACK_FILE_FORMAT);
if (trackFileFormat == null) {
trackFileFormat = TrackFileFormat.GPX;
}
if (!FileUtils.isExternalStorageAvailable()) {
Toast.makeText(this, R.string.external_storage_not_available, Toast.LENGTH_LONG).show();
finish();
return;
}
String directoryPath;
if (importAll) {
directoryDisplayName = FileUtils.getPathDisplayName(
trackFileFormat.getExtension());
directoryPath = FileUtils.getPath(trackFileFormat.getExtension());
if (!FileUtils.isDirectory(new File(directoryPath))) {
Toast.makeText(
this, getString(R.string.import_no_directory, directoryDisplayName), Toast.LENGTH_LONG)
.show();
finish();
return;
}
} else {
String action = intent.getAction();
if (!(Intent.ACTION_ATTACH_DATA.equals(action) || Intent.ACTION_VIEW.equals(action))) {
Log.d(TAG, "Invalid action: " + intent);
finish();
return;
}
Uri data = intent.getData();
if (!UriUtils.isFileUri(data)) {
Log.d(TAG, "Invalid data: " + intent);
finish();
return;
}
directoryDisplayName = data.getPath();
directoryPath = data.getPath();
}
Object retained = getLastNonConfigurationInstance();
if (retained instanceof ImportAsyncTask) {
importAsyncTask = (ImportAsyncTask) retained;
importAsyncTask.setActivity(this);
} else {
importAsyncTask = new ImportAsyncTask(this, importAll, trackFileFormat, directoryPath);
importAsyncTask.execute();
}
}
@Override
public Object onRetainNonConfigurationInstance() {
importAsyncTask.setActivity(null);
return importAsyncTask;
}
@Override
protected Dialog onCreateDialog(int id) {
switch (id) {
case DIALOG_PROGRESS_ID:
progressDialog = DialogUtils.createHorizontalProgressDialog(
this, R.string.import_progress_message, new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
importAsyncTask.cancel(true);
dialog.dismiss();
}
}, directoryDisplayName);
return progressDialog;
case DIALOG_RESULT_ID:
int iconId;
int titleId;
String message;
String totalFiles = getResources()
.getQuantityString(R.plurals.files, totalCount, totalCount);
if (successCount == totalCount) {
if (totalCount == 0) {
iconId = android.R.drawable.ic_dialog_info;
titleId = R.string.import_no_file_title;
message = getString(R.string.import_no_file, directoryDisplayName);
} else {
iconId = R.drawable.ic_dialog_success;
titleId = R.string.generic_success_title;
message = getString(R.string.import_success, totalFiles, directoryDisplayName);
}
} else {
iconId = android.R.drawable.ic_dialog_alert;
titleId = R.string.generic_error_title;
message = getString(
R.string.import_error, successCount, totalFiles, directoryDisplayName);
}
final Dialog dialog = new AlertDialog.Builder(this).setCancelable(true).setIcon(iconId)
.setMessage(message).setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialogInterface) {
dialogInterface.dismiss();
finish();
}
}).setPositiveButton(R.string.generic_ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int which) {
if (successCount == totalCount && !importAll && trackId != -1L) {
Intent intent = IntentUtils.newIntent(
ImportActivity.this, TrackDetailActivity.class)
.putExtra(TrackDetailActivity.EXTRA_TRACK_ID, trackId);
TaskStackBuilder.create(ImportActivity.this)
.addParentStack(TrackDetailActivity.class).addNextIntent(intent)
.startActivities();
}
dialogInterface.dismiss();
finish();
}
}).setTitle(titleId).create();
dialog.setOnShowListener(new DialogInterface.OnShowListener() {
@Override
public void onShow(DialogInterface dialogInterface) {
DialogUtils.setDialogTitleDivider(ImportActivity.this, dialog);
}
});
return dialog;
default:
return null;
}
}
/**
* Invokes when the associated AsyncTask completes.
*
* @param aSuccessCount the number of files successfully imported
* @param aTotalCount the number of files to import
* @param aTrackId the last successfully imported track id
*/
public void onAsyncTaskCompleted(int aSuccessCount, int aTotalCount, long aTrackId) {
successCount = aSuccessCount;
totalCount = aTotalCount;
trackId = aTrackId;
removeDialog(DIALOG_PROGRESS_ID);
showDialog(DIALOG_RESULT_ID);
}
/**
* Shows the progress dialog.
*/
public void showProgressDialog() {
showDialog(DIALOG_PROGRESS_ID);
}
/**
* Sets the progress dialog value.
*
* @param number the number of files imported
* @param max the maximum number of files
*/
public void setProgressDialogValue(int number, int max) {
if (progressDialog != null) {
progressDialog.setIndeterminate(false);
progressDialog.setMax(max);
progressDialog.setProgress(Math.min(number, max));
}
}
}
@@ -0,0 +1,241 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.file.importer;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.io.file.TrackFileFormat;
import com.google.android.apps.mytracks.io.file.exporter.KmzTrackExporter;
import com.google.android.apps.mytracks.util.FileUtils;
import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.android.apps.mytracks.util.SystemUtils;
import com.google.android.maps.mytracks.R;
import android.content.Context;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.PowerManager.WakeLock;
import android.util.Log;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
/**
* AsyncTask to import files from the external storage.
*
* @author Jimmy Shih
*/
public class ImportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
private static final String TAG = ImportAsyncTask.class.getSimpleName();
private ImportActivity importActivity;
private final boolean importAll;
private final TrackFileFormat trackFileFormat;
private final String path;
private final Context context;
private WakeLock wakeLock;
// true if the AsyncTask has completed
private boolean completed;
// the number of files successfully imported
private int successCount;
// the number of files to import
private int totalCount;
// the last successfully imported track id
private long trackId;
/**
* Creates an AsyncTask.
*
* @param importActivity the activity currently associated with this AsyncTask
* @param importAll true to import all GPX files
* @param trackFileFormat the track file format
* @param path path to import GPX files
*/
public ImportAsyncTask(ImportActivity importActivity, boolean importAll,
TrackFileFormat trackFileFormat, String path) {
this.importActivity = importActivity;
this.importAll = importAll;
this.trackFileFormat = trackFileFormat;
this.path = path;
context = importActivity.getApplicationContext();
completed = false;
successCount = 0;
totalCount = 0;
trackId = -1L;
}
/**
* Sets the current {@link ImportActivity} associated with this AyncTask.
*
* @param importActivity the current {@link ImportActivity}, can be null
*/
public void setActivity(ImportActivity importActivity) {
this.importActivity = importActivity;
if (completed && importActivity != null) {
importActivity.onAsyncTaskCompleted(successCount, totalCount, trackId);
}
}
@Override
protected void onPreExecute() {
if (importActivity != null) {
importActivity.showProgressDialog();
}
}
@Override
protected Boolean doInBackground(Void... params) {
try {
Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
// Get the wake lock if not recording or paused
boolean isRecording =
PreferencesUtils.getLong(importActivity, R.string.recording_track_id_key)
!= PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
boolean isPaused = PreferencesUtils.getBoolean(importActivity,
R.string.recording_track_paused_key, PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT);
if (!isRecording || isPaused) {
wakeLock = SystemUtils.acquireWakeLock(importActivity, wakeLock);
}
List<File> files = getFiles();
totalCount = files.size();
if (totalCount == 0) {
return true;
}
for (int i = 0; i < totalCount; i++) {
if (isCancelled()) {
// If cancelled, return true to show the number of files imported
return true;
}
if (importFile(files.get(i))) {
successCount++;
}
publishProgress(i + 1, totalCount);
}
return true;
} finally {
if (wakeLock != null && wakeLock.isHeld()) {
wakeLock.release();
}
}
}
@Override
protected void onProgressUpdate(Integer... values) {
if (importActivity != null) {
importActivity.setProgressDialogValue(values[0], values[1]);
}
}
@Override
protected void onPostExecute(Boolean result) {
completed = true;
if (importActivity != null) {
importActivity.onAsyncTaskCompleted(successCount, totalCount, trackId);
}
}
@Override
protected void onCancelled() {
completed = true;
if (importActivity != null) {
importActivity.onAsyncTaskCompleted(successCount, totalCount, trackId);
}
}
/**
* Imports a file.
*
* @param file the file
*/
private boolean importFile(final File file) {
FileInputStream fileInputStream = null;
try {
TrackImporter trackImporter;
if (trackFileFormat == TrackFileFormat.KML) {
String extension = FileUtils.getExtension(file.getName());
if (TrackFileFormat.KML.getExtension().equals(extension)) {
trackImporter = new KmlFileTrackImporter(context, -1L);
} else {
MyTracksProviderUtils myTracksProviderUtils = MyTracksProviderUtils.Factory.get(context);
Uri uri = myTracksProviderUtils.insertTrack(new Track());
long newId = Long.parseLong(uri.getLastPathSegment());
trackImporter = new KmzTrackImporter(context, newId);
}
} else {
trackImporter = new GpxFileTrackImporter(context);
}
fileInputStream = new FileInputStream(file);
trackId = trackImporter.importFile(fileInputStream);
return trackId != -1L;
} catch (FileNotFoundException e) {
Log.e(TAG, "Unable to import file", e);
return false;
} finally {
if (fileInputStream != null) {
try {
fileInputStream.close();
} catch (IOException e) {
Log.e(TAG, "Unable to close file input stream", e);
}
}
}
}
/**
* Gets a list of files. If importAll is true, returns a list of the files
* under the path directory. If importAll is false, returns a list containing
* just the path file.
*/
private List<File> getFiles() {
List<File> files = new ArrayList<File>();
File file = new File(path);
if (importAll) {
File[] candidates = file.listFiles();
if (candidates != null) {
for (File candidate : candidates) {
if (!FileUtils.isDirectory(candidate)) {
String extension = FileUtils.getExtension(candidate.getName());
if (trackFileFormat == TrackFileFormat.KML && (
TrackFileFormat.KML.getExtension().equals(extension)
|| KmzTrackExporter.KMZ_EXTENSION.equals(extension))) {
files.add(candidate);
} else if (trackFileFormat == TrackFileFormat.GPX
&& TrackFileFormat.GPX.getExtension().equals(extension)) {
files.add(candidate);
}
}
}
}
} else {
files.add(file);
}
return files;
}
}
@@ -0,0 +1,317 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.file.importer;
import com.google.android.apps.mytracks.content.MyTracksLocation;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.Sensor;
import com.google.android.apps.mytracks.content.Sensor.SensorDataSet;
import com.google.android.apps.mytracks.content.Waypoint.WaypointType;
import com.google.common.annotations.VisibleForTesting;
import android.content.Context;
import android.location.Location;
import android.net.Uri;
import java.util.ArrayList;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
/**
* Imports a KML file.
*
* @author Jimmy Shih
*/
public class KmlFileTrackImporter extends AbstractFileTrackImporter {
private static final String CADENCE = "cadence";
private static final String HEART_RATE = "heart_rate";
private static final String POWER = "power";
private static final String STATISTICS_STYLE = "#statistics";
private static final String WAYPOINT_STYLE = "#waypoint";
private static final String TAG_COORDINATES = "coordinates";
private static final String TAG_DESCRIPTION = "description";
private static final String TAG_GX_COORD = "gx:coord";
private static final String TAG_GX_MULTI_TRACK = "gx:MultiTrack";
private static final String TAG_GX_SIMPLE_ARRAY_DATA = "gx:SimpleArrayData";
private static final String TAG_GX_TRACK = "gx:Track";
private static final String TAG_GX_VALUE = "gx:value";
private static final String TAG_HREF = "href";
private static final String TAG_KML = "kml";
private static final String TAG_NAME = "name";
private static final String TAG_PHOTO_OVERLAY = "PhotoOverlay";
private static final String TAG_PLACEMARK = "Placemark";
private static final String TAG_STYLE_URL = "styleUrl";
private static final String TAG_VALUE = "value";
private static final String TAG_WHEN = "when";
private static final String ATTRIBUTE_NAME = "name";
private boolean trackStarted = false;
private String sensorName;
private ArrayList<Location> locationList;
private ArrayList<Integer> cadenceList;
private ArrayList<Integer> heartRateList;
private ArrayList<Integer> powerList;
/**
* Constructor.
*
* @param context the context
* @param importTrackId track id to import to. -1L to import to a new track.
*/
public KmlFileTrackImporter(Context context, long importTrackId) {
this(context, importTrackId, MyTracksProviderUtils.Factory.get(context));
}
@VisibleForTesting
KmlFileTrackImporter(
Context context, long importTrackId, MyTracksProviderUtils myTracksProviderUtils) {
super(context, importTrackId, myTracksProviderUtils);
}
@Override
public void startElement(String uri, String localName, String tag, Attributes attributes)
throws SAXException {
if (tag.equals(TAG_PLACEMARK) || tag.equals(TAG_PHOTO_OVERLAY)) {
/*
* Note that a track is contained in a Placemark, calling onWaypointStart
* will clear various track variables like name, category, and
* description.
*/
onWaypointStart();
} else if (tag.equals(TAG_GX_MULTI_TRACK)) {
trackStarted = true;
onTrackStart();
} else if (tag.equals(TAG_GX_TRACK)) {
if (!trackStarted) {
throw new SAXException("No " + TAG_GX_MULTI_TRACK);
}
onTrackSegmentStart();
} else if (tag.equals(TAG_GX_SIMPLE_ARRAY_DATA)) {
onSensorDataStart(attributes);
}
}
@Override
public void endElement(String uri, String localName, String tag) throws SAXException {
if (tag.equals(TAG_KML)) {
onFileEnd();
} else if (tag.equals(TAG_PLACEMARK) || tag.equals(TAG_PHOTO_OVERLAY)) {
/*
* Note that a track is contained in a Placemark, calling onWaypointend is
* save since waypointType is not set for a track.
*/
onWaypointEnd();
} else if (localName.equals(TAG_COORDINATES)) {
onWaypointLocationEnd();
} else if (tag.equals(TAG_GX_MULTI_TRACK)) {
onTrackEnd();
} else if (tag.equals(TAG_GX_TRACK)) {
onTrackSegmentEnd();
} else if (tag.equals(TAG_GX_COORD)) {
onTrackPointEnd();
} else if (tag.equals(TAG_GX_VALUE)) {
onSensorValueEnd();
} else if (tag.equals(TAG_NAME)) {
if (content != null) {
name = content.trim();
}
} else if (localName.equals(TAG_DESCRIPTION)) {
if (content != null) {
description = content.trim();
}
} else if (localName.equals(TAG_VALUE)) {
if (content != null) {
category = content.trim();
}
} else if (localName.equals(TAG_WHEN)) {
if (content != null) {
time = content.trim();
}
} else if (localName.equals(TAG_STYLE_URL)) {
if (content != null) {
waypointType = content.trim();
}
} else if (localName.equals(TAG_HREF)) {
if (content != null) {
photoUrl = content.trim();
}
}
// Reset element content
content = null;
}
/**
* On waypoint start.
*/
private void onWaypointStart() {
// Reset all Placemark variables
name = null;
description = null;
category = null;
photoUrl = null;
latitude = null;
longitude = null;
altitude = null;
time = null;
waypointType = null;
}
/**
* On waypoint end.
*/
private void onWaypointEnd() throws SAXException {
// Add a waypoint if the waypointType matches
WaypointType type = null;
if (WAYPOINT_STYLE.equals(waypointType)) {
type = WaypointType.WAYPOINT;
} else if (STATISTICS_STYLE.equals(waypointType)) {
type = WaypointType.STATISTICS;
}
if (type == null) {
return;
}
if (photoUrl != null) {
Uri uri = Uri.parse(photoUrl);
photoUrl = getPhotoUrl(uri.getLastPathSegment());
}
addWaypoint(type);
}
/**
* On waypoint location end.
*/
private void onWaypointLocationEnd() {
if (content != null) {
String parts[] = content.trim().split(",");
if (parts.length != 2 && parts.length != 3) {
return;
}
longitude = parts[0];
latitude = parts[1];
altitude = parts.length == 3 ? parts[2] : null;
}
}
@Override
protected void onTrackSegmentStart() {
super.onTrackSegmentStart();
locationList = new ArrayList<Location>();
powerList = new ArrayList<Integer>();
cadenceList = new ArrayList<Integer>();
heartRateList = new ArrayList<Integer>();
}
/**
* On track segment end.
*/
private void onTrackSegmentEnd() {
// Close a track segment by inserting the segment locations
boolean hasPower = powerList.size() == locationList.size();
boolean hasCadence = cadenceList.size() == locationList.size();
boolean hasHeartRate = heartRateList.size() == locationList.size();
for (int i = 0; i < locationList.size(); i++) {
Location location = locationList.get(i);
if (!hasPower && !hasCadence && !hasHeartRate) {
insertTrackPoint(location);
} else {
SensorDataSet.Builder builder = Sensor.SensorDataSet.newBuilder();
if (hasPower) {
builder.setPower(Sensor.SensorData.newBuilder()
.setValue(powerList.get(i)).setState(Sensor.SensorState.SENDING));
}
if (hasCadence) {
builder.setCadence(Sensor.SensorData.newBuilder()
.setValue(cadenceList.get(i)).setState(Sensor.SensorState.SENDING));
}
if (hasHeartRate) {
builder.setHeartRate(Sensor.SensorData.newBuilder()
.setValue(heartRateList.get(i)).setState(Sensor.SensorState.SENDING));
}
SensorDataSet sensorDataSet = builder.setCreationTime(location.getTime()).build();
MyTracksLocation myTracksLocation = new MyTracksLocation(location, sensorDataSet);
insertTrackPoint(myTracksLocation);
}
}
}
/**
* On track point end. gx:coord end tag.
*/
private void onTrackPointEnd() throws SAXException {
// Add location to locationList
if (content == null) {
return;
}
String parts[] = content.trim().split(" ");
if (parts.length != 2 && parts.length != 3) {
return;
}
longitude = parts[0];
latitude = parts[1];
altitude = parts.length == 3 ? parts[2] : null;
Location location = getTrackPoint();
if (location == null) {
return;
}
locationList.add(location);
time = null;
}
/**
* On sensor data start. gx:SimpleArrayData start tag.
*
* @param attributes
*/
private void onSensorDataStart(Attributes attributes) {
sensorName = attributes.getValue(ATTRIBUTE_NAME);
}
/**
* On sensor value end. gx:value end tag.
*/
private void onSensorValueEnd() throws SAXException {
if (content == null) {
return;
}
content = content.trim();
if (content.equals("")) {
return;
}
int value;
try {
value = Integer.parseInt(content);
} catch (NumberFormatException e) {
throw new SAXException(createErrorMessage("Unable to parse gx:value:" + content), e);
}
if (POWER.equals(sensorName)) {
powerList.add(value);
} else if (HEART_RATE.equals(sensorName)) {
heartRateList.add(value);
} else if (CADENCE.equals(sensorName)) {
cadenceList.add(value);
}
}
}
@@ -0,0 +1,204 @@
/*
* Copyright 2013 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.file.importer;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.io.file.exporter.KmzTrackExporter;
import com.google.android.apps.mytracks.util.FileUtils;
import android.content.Context;
import android.net.Uri;
import android.util.Log;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
/**
* Imports a KMZ file.
*
* @author Jimmy Shih
*/
public class KmzTrackImporter implements TrackImporter {
private static final String TAG = KmzTrackImporter.class.getSimpleName();
private static final int BUFFER_SIZE = 4096;
private final Context context;
private final long importTrackId;
/**
* Constructor.
*
* @param context the context
* @param importTrackId track id to import to. This should not be -1L so that
* images in the kmz file can be imported.
*/
public KmzTrackImporter(Context context, long importTrackId) {
this.context = context;
this.importTrackId = importTrackId;
}
@Override
public long importFile(InputStream inputStream) {
ZipInputStream zipInputStream = null;
long trackId = importTrackId;
try {
ZipEntry zipEntry;
zipInputStream = new ZipInputStream(inputStream);
while ((zipEntry = zipInputStream.getNextEntry()) != null) {
if (Thread.interrupted()) {
Log.d(TAG, "Thread interrupted");
cleanImport(trackId);
return -1L;
}
String fileName = zipEntry.getName();
if (fileName.equals(KmzTrackExporter.KMZ_KML_FILE)) {
trackId = parseKml(zipInputStream);
if (trackId == -1L) {
Log.d(TAG, "Unable to parse kml in kmz");
cleanImport(trackId);
return -1L;
}
} else {
String prefix = KmzTrackExporter.KMZ_IMAGES_DIR + File.separatorChar;
if (fileName.startsWith(prefix)) {
readImageFile(zipInputStream, fileName.substring(prefix.length()));
}
}
zipInputStream.closeEntry();
}
return trackId;
} catch (IOException e) {
Log.e(TAG, "Unable to import file", e);
cleanImport(trackId);
return -1L;
} finally {
if (zipInputStream != null) {
try {
zipInputStream.close();
} catch (IOException e) {
Log.e(TAG, "Unable to close zip input stream", e);
}
}
}
}
/**
* Cleans up import.
*
* @param trackId the trackId
*/
private void cleanImport(long trackId) {
if (trackId != -1L) {
MyTracksProviderUtils myTracksProviderUtils = MyTracksProviderUtils.Factory.get(context);
myTracksProviderUtils.deleteTrack(context, trackId);
}
if (importTrackId != -1L) {
File dir = FileUtils.getPhotoDir(importTrackId);
if (FileUtils.isDirectory(dir)) {
for (File file : dir.listFiles()) {
file.delete();
}
dir.delete();
FileUtils.updateMediaScanner(context, Uri.fromFile(dir));
}
}
}
/**
* Parses kml
*
* @param zipInputStream the zip input stream
* @return the imported track id or -1L
*/
private long parseKml(ZipInputStream zipInputStream) throws IOException {
ByteArrayInputStream byteArrayInputStream = null;
try {
KmlFileTrackImporter kmlFileTrackImporter = new KmlFileTrackImporter(context, importTrackId);
byteArrayInputStream = new ByteArrayInputStream(getKml(zipInputStream));
return kmlFileTrackImporter.importFile(byteArrayInputStream);
} finally {
if (byteArrayInputStream != null) {
byteArrayInputStream.close();
}
}
}
/**
* Gets the kml as byte array.
*
* @param zipInputStream the zip input stream
*/
private byte[] getKml(ZipInputStream zipInputStream) throws IOException {
ByteArrayOutputStream byteArrayOutputStream = null;
try {
byteArrayOutputStream = new ByteArrayOutputStream();
byte[] buffer = new byte[BUFFER_SIZE];
int count;
while ((count = zipInputStream.read(buffer)) != -1) {
byteArrayOutputStream.write(buffer, 0, count);
}
return byteArrayOutputStream.toByteArray();
} finally {
if (byteArrayOutputStream != null) {
byteArrayOutputStream.close();
}
}
}
/**
* Reads an image file.
*
* @param zipInputStream the zip input stream
* @param fileName the file name
*/
private void readImageFile(ZipInputStream zipInputStream, String fileName) throws IOException {
FileOutputStream fileOutputStream = null;
try {
if (importTrackId == -1L) {
return;
}
if (fileName.equals("")) {
return;
}
File dir = FileUtils.getPhotoDir(importTrackId);
FileUtils.ensureDirectoryExists(dir);
File file = new File(dir, fileName);
fileOutputStream = new FileOutputStream(file);
byte[] buffer = new byte[BUFFER_SIZE];
int count;
while ((count = zipInputStream.read(buffer)) != -1) {
fileOutputStream.write(buffer, 0, count);
}
FileUtils.updateMediaScanner(context, Uri.fromFile(file));
} finally {
if (fileOutputStream != null) {
fileOutputStream.close();
}
}
}
}
@@ -0,0 +1,34 @@
/*
* Copyright 2013 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.file.importer;
import java.io.InputStream;
/**
* Interface for a track importer.
*
* @author Jimmy Shih
*/
public interface TrackImporter {
/**
* Import a file.
*
* @param inputStream the file's input stream
* @return the imported track id or -1L.
*/
public long importFile(InputStream inputStream);
}
@@ -0,0 +1,69 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.fusiontables;
import com.google.android.apps.mytracks.io.sendtogoogle.AbstractSendActivity;
import com.google.android.apps.mytracks.io.sendtogoogle.AbstractSendAsyncTask;
import com.google.android.apps.mytracks.io.sendtogoogle.SendRequest;
import com.google.android.apps.mytracks.io.sendtogoogle.UploadResultActivity;
import com.google.android.apps.mytracks.io.spreadsheets.SendSpreadsheetsActivity;
import com.google.android.apps.mytracks.util.IntentUtils;
import com.google.android.maps.mytracks.R;
import com.google.common.annotations.VisibleForTesting;
import android.content.Intent;
/**
* An activity to send a track to Google Fusion Tables.
*
* @author Jimmy Shih
*/
public class SendFusionTablesActivity extends AbstractSendActivity {
@Override
protected AbstractSendAsyncTask createAsyncTask() {
return new SendFusionTablesAsyncTask(this, sendRequest.getTrackId(), sendRequest.getAccount());
}
@Override
protected String getServiceName() {
return getString(R.string.export_google_fusion_tables);
}
@Override
protected void startNextActivity(boolean success, boolean isCancel) {
sendRequest.setFusionTablesSuccess(success);
Class<?> next = getNextClass(sendRequest, isCancel);
Intent intent = IntentUtils.newIntent(this, next)
.putExtra(SendRequest.SEND_REQUEST_KEY, sendRequest);
startActivity(intent);
finish();
}
@VisibleForTesting
Class<?> getNextClass(SendRequest request, boolean isCancel) {
if (isCancel) {
return UploadResultActivity.class;
} else {
if (request.isSendSpreadsheets()) {
return SendSpreadsheetsActivity.class;
} else {
return UploadResultActivity.class;
}
}
}
}
@@ -0,0 +1,402 @@
// Copyright 2012 Google Inc. All Rights Reserved.
package com.google.android.apps.mytracks.io.fusiontables;
import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.content.DescriptionGenerator;
import com.google.android.apps.mytracks.content.DescriptionGeneratorImpl;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils.LocationIterator;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.content.Waypoint.WaypointType;
import com.google.android.apps.mytracks.io.sendtogoogle.AbstractSendAsyncTask;
import com.google.android.apps.mytracks.io.sendtogoogle.SendToGoogleUtils;
import com.google.android.apps.mytracks.io.sync.SyncUtils;
import com.google.android.apps.mytracks.util.LocationUtils;
import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.android.gms.auth.GoogleAuthException;
import com.google.android.gms.auth.UserRecoverableAuthException;
import com.google.android.maps.mytracks.R;
import com.google.api.client.extensions.android.http.AndroidHttp;
import com.google.api.client.googleapis.GoogleUrl;
import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential;
import com.google.api.client.googleapis.extensions.android.gms.auth.UserRecoverableAuthIOException;
import com.google.api.client.http.ByteArrayContent;
import com.google.api.client.http.HttpContent;
import com.google.api.client.json.gson.GsonFactory;
import com.google.api.services.drive.Drive;
import com.google.api.services.drive.model.Permission;
import com.google.api.services.fusiontables.Fusiontables;
import com.google.api.services.fusiontables.Fusiontables.Query.Sql;
import com.google.api.services.fusiontables.model.Column;
import com.google.api.services.fusiontables.model.PointStyle;
import com.google.api.services.fusiontables.model.StyleFunction;
import com.google.api.services.fusiontables.model.StyleSetting;
import com.google.api.services.fusiontables.model.Table;
import com.google.api.services.fusiontables.model.Template;
import android.accounts.Account;
import android.content.Context;
import android.database.Cursor;
import android.location.Location;
import android.util.Log;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* AsyncTask to send a track to Google Fusion Tables.
*
* @author Jimmy Shih
*/
public class SendFusionTablesAsyncTask extends AbstractSendAsyncTask {
private static final String TAG = SendFusionTablesAsyncTask.class.getSimpleName();
private static final int MAX_POINTS_PER_UPLOAD = 2048;
private static final int PROGRESS_CREATE_TABLE = 0;
private static final int PROGRESS_SET_STYLE = 5;
private static final int PROGRESS_UPLOAD_DATA_MIN = 10;
private static final int PROGRESS_UPLOAD_DATA_MAX = 90;
private static final int PROGRESS_UPLOAD_WAYPOINTS = 95;
private static final int PROGRESS_COMPLETE = 100;
// See
// http://support.google.com/fusiontables/bin/answer.py?hl=en&answer=185991
private static final String MARKER_TYPE_START = "large_green";
private static final String MARKER_TYPE_END = "large_red";
private static final String MARKER_TYPE_WAYPOINT = "large_blue";
private static final String MARKER_TYPE_STATISTICS = "large_yellow";
private final long trackId;
private final Account account;
private final Context context;
private final MyTracksProviderUtils myTracksProviderUtils;
int currentSegment;
public SendFusionTablesAsyncTask(
SendFusionTablesActivity activity, long trackId, Account account) {
super(activity);
this.trackId = trackId;
this.account = account;
context = activity.getApplicationContext();
myTracksProviderUtils = MyTracksProviderUtils.Factory.get(context);
}
@Override
protected void closeConnection() {}
@Override
protected boolean performTask() {
try {
// Reset the per upload states
currentSegment = 1;
GoogleAccountCredential credential = SendToGoogleUtils.getGoogleAccountCredential(
context, account.name, SendToGoogleUtils.FUSION_TABLES_SCOPE);
if (credential == null) {
return false;
}
Fusiontables fusiontables = new Fusiontables.Builder(
AndroidHttp.newCompatibleTransport(), new GsonFactory(), credential).build();
Track track = myTracksProviderUtils.getTrack(trackId);
if (track == null) {
Log.d(TAG, "No track for " + trackId);
return false;
}
// Create a new table
publishProgress(PROGRESS_CREATE_TABLE);
String tableId = createNewTable(fusiontables, track);
if (tableId == null) {
return retryTask();
}
publishProgress(PROGRESS_SET_STYLE);
setStyle(fusiontables, tableId);
setTemplate(fusiontables, tableId);
if (!setPermission(track, tableId)) {
Log.d(TAG, "Cannot set permission for table " + tableId);
return false;
}
// Upload all the track points plus the start and end markers
publishProgress(PROGRESS_UPLOAD_DATA_MIN);
if (!uploadAllTrackPoints(fusiontables, tableId, track)) {
return false;
}
// Upload all the waypoints
publishProgress(PROGRESS_UPLOAD_WAYPOINTS);
if (!uploadWaypoints(fusiontables, tableId)) {
return false;
}
publishProgress(PROGRESS_COMPLETE);
return true;
} catch (UserRecoverableAuthException e) {
SendToGoogleUtils.sendNotification(
context, account.name, e.getIntent(), SendToGoogleUtils.FUSION_TABLES_NOTIFICATION_ID);
return false;
} catch (GoogleAuthException e) {
return retryTask();
} catch (UserRecoverableAuthIOException e) {
SendToGoogleUtils.sendNotification(
context, account.name, e.getIntent(), SendToGoogleUtils.FUSION_TABLES_NOTIFICATION_ID);
return false;
} catch (IOException e) {
return retryTask();
}
}
@Override
protected void invalidateToken() {}
/**
* Creates a new table.
*
* @param fusiontables fusion tables
* @param track the track
* @return the table id if success.
*/
private String createNewTable(Fusiontables fusiontables, Track track) throws IOException {
Table table = new Table();
table.setName(track.getName());
table.setDescription(track.getDescription());
table.setIsExportable(true);
table.setColumns(Arrays.asList(new Column().setName("name").setType("STRING"),
new Column().setName("description").setType("STRING"),
new Column().setName("geometry").setType("LOCATION"),
new Column().setName("icon").setType("STRING")));
return fusiontables.table().insert(table).execute().getTableId();
}
private void setStyle(Fusiontables fusiontables, String tableId) throws IOException {
StyleFunction styleFunction = new StyleFunction();
styleFunction.setColumnName("icon");
PointStyle pointStyle = new PointStyle();
pointStyle.setIconStyler(styleFunction);
StyleSetting styleSetting = new StyleSetting();
styleSetting.setTableId(tableId);
styleSetting.setMarkerOptions(pointStyle);
fusiontables.style().insert(tableId, styleSetting).execute();
}
private void setTemplate(Fusiontables fusiontables, String tableId) throws IOException {
Template template = new Template();
template.setTableId(tableId);
template.setAutomaticColumnNames(Arrays.asList("name", "description"));
fusiontables.template().insert(tableId, template).execute();
}
private boolean setPermission(Track track, String tableId) throws IOException, GoogleAuthException {
boolean defaultTablePublic = PreferencesUtils.getBoolean(context,
R.string.export_google_fusion_tables_public_key,
PreferencesUtils.EXPORT_GOOGLE_FUSION_TABLES_PUBLIC_DEFAULT);
if (!defaultTablePublic) {
return true;
}
GoogleAccountCredential driveCredential = SendToGoogleUtils.getGoogleAccountCredential(
context, account.name, SendToGoogleUtils.DRIVE_SCOPE);
if (driveCredential == null) {
return false;
}
Drive drive = SyncUtils.getDriveService(driveCredential);
Permission permission = new Permission();
permission.setRole("reader");
permission.setType("anyone");
permission.setValue("");
drive.permissions().insert(tableId, permission).execute();
shareUrl = SendFusionTablesUtils.getMapUrl(track, tableId);
return true;
}
/**
* Uploads all the points in a track. *
*
* @param fusiontables fusion tables
* @param tableId the table id
* @param track the track
* @return true if success.
*/
private boolean uploadAllTrackPoints(Fusiontables fusiontables, String tableId, Track track)
throws IOException {
int numberOfPoints = track.getNumberOfPoints();
List<Location> locations = new ArrayList<Location>(MAX_POINTS_PER_UPLOAD);
Location lastValidLocation = null;
boolean sentStartMarker = false;
int readCount = 0;
LocationIterator locationIterator = null;
try {
locationIterator = myTracksProviderUtils.getTrackPointLocationIterator(
trackId, -1L, false, MyTracksProviderUtils.DEFAULT_LOCATION_FACTORY);
while (locationIterator.hasNext()) {
Location location = locationIterator.next();
locations.add(location);
if (LocationUtils.isValidLocation(location)) {
lastValidLocation = location;
}
if (!sentStartMarker && lastValidLocation != null) {
// Create a start marker
String name = context.getString(R.string.marker_label_start, track.getName());
createNewPoint(fusiontables, tableId, name, "", lastValidLocation, MARKER_TYPE_START);
sentStartMarker = true;
}
// Upload periodically
readCount++;
if (readCount % MAX_POINTS_PER_UPLOAD == 0) {
if (!prepareAndUploadPoints(fusiontables, tableId, track, locations, false)) {
Log.d(TAG, "Unable to upload points");
return false;
}
updateProgress(readCount, numberOfPoints);
locations.clear();
}
}
// Do a final upload with the remaining locations
if (!prepareAndUploadPoints(fusiontables, tableId, track, locations, true)) {
Log.d(TAG, "Unable to upload points");
return false;
}
// Create an end marker
if (lastValidLocation != null) {
String name = context.getString(R.string.marker_label_end, track.getName());
DescriptionGenerator descriptionGenerator = new DescriptionGeneratorImpl(context);
String description = descriptionGenerator.generateTrackDescription(track, null, null, true);
createNewPoint(
fusiontables, tableId, name, description, lastValidLocation, MARKER_TYPE_END);
}
return true;
} finally {
if (locationIterator != null) {
locationIterator.close();
}
}
}
/**
* Prepares and uploads a list of locations from a track.
*
* @param fusiontables fusion tables
* @param tableId the table id
* @param track the track
* @param locations the locations from the track
* @param lastBatch true if it is the last batch of locations
*/
private boolean prepareAndUploadPoints(Fusiontables fusiontables, String tableId, Track track,
List<Location> locations, boolean lastBatch) throws IOException {
// Prepare locations
ArrayList<Track> splitTracks = SendToGoogleUtils.prepareLocations(track, locations);
// Upload segments
boolean onlyOneSegment = lastBatch && currentSegment == 1 && splitTracks.size() == 1;
for (Track splitTrack : splitTracks) {
if (!onlyOneSegment) {
splitTrack.setName(context.getString(
R.string.send_google_track_part_label, splitTrack.getName(), currentSegment));
}
createNewLineString(fusiontables, tableId, splitTrack);
currentSegment++;
}
return true;
}
/**
* Uploads all the waypoints.
*
* @param fusiontables fusion tables
* @param tableId the table id
* @return true if success.
* @throws IOException
*/
private boolean uploadWaypoints(Fusiontables fusiontables, String tableId) throws IOException {
Cursor cursor = null;
try {
cursor = myTracksProviderUtils.getWaypointCursor(
trackId, -1L, Constants.MAX_LOADED_WAYPOINTS_POINTS);
if (cursor != null && cursor.moveToFirst()) {
/*
* This will skip the first waypoint (it carries the stats for the
* track).
*/
while (cursor.moveToNext()) {
Waypoint wpt = myTracksProviderUtils.createWaypoint(cursor);
String type = wpt.getType() == WaypointType.STATISTICS ? MARKER_TYPE_STATISTICS
: MARKER_TYPE_WAYPOINT;
String description = wpt.getDescription().replaceAll("\n", "<br>");
createNewPoint(
fusiontables, tableId, wpt.getName(), description, wpt.getLocation(), type);
}
}
return true;
} finally {
if (cursor != null) {
cursor.close();
}
}
}
/**
* Creates a new row in Google Fusion Tables representing a marker as a point.
*
* @param fusiontables fusion tables
* @param tableId the table id
* @param name the marker name
* @param description the marker description
* @param location the marker location
* @param type the marker type
*/
private void createNewPoint(Fusiontables fusiontables, String tableId, String name,
String description, Location location, String type) throws IOException {
String values = SendFusionTablesUtils.formatSqlValues(
name, description, SendFusionTablesUtils.getKmlPoint(location), type);
Sql sql = fusiontables.query()
.sql("INSERT INTO " + tableId + " (name,description,geometry,icon) VALUES " + values);
sql.execute();
}
/**
* Creates a new row in Google Fusion Tables representing the track as a line
* segment.
*
* @param fusiontables fusion tables
* @param tableId the table id
* @param track the track
*/
private void createNewLineString(Fusiontables fusiontables, String tableId, Track track)
throws IOException {
String values = SendFusionTablesUtils.formatSqlValues(track.getName(), track.getDescription(),
SendFusionTablesUtils.getKmlLineString(track.getLocations()));
String sql = "INSERT INTO " + tableId + " (name,description,geometry) VALUES " + values;
HttpContent content = ByteArrayContent.fromString(null, "sql=" + sql);
GoogleUrl url = new GoogleUrl("https://www.googleapis.com/fusiontables/v1/query");
fusiontables.getRequestFactory().buildPostRequest(url, content).execute();
}
/**
* Updates the progress based on the number of locations uploaded.
*
* @param uploaded the number of uploaded locations
* @param total the number of total locations
*/
private void updateProgress(int uploaded, int total) {
double totalPercentage = (double) uploaded / total;
double scaledPercentage = totalPercentage
* (PROGRESS_UPLOAD_DATA_MAX - PROGRESS_UPLOAD_DATA_MIN) + PROGRESS_UPLOAD_DATA_MIN;
publishProgress((int) scaledPercentage);
}
}
@@ -0,0 +1,155 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.fusiontables;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.common.annotations.VisibleForTesting;
import android.location.Location;
import android.util.Log;
import java.util.ArrayList;
import java.util.Locale;
/**
* Utilities for sending a track to Google Fusion Tables.
*
* @author Jimmy Shih
*/
public class SendFusionTablesUtils {
private static final String MAP_URL = "https://www.google.com/fusiontables/embedviz?"
+ "viz=MAP&q=select+col0,+col1,+col2,+col3+from+%s+&h=false&lat=%f&lng=%f&z=%d&t=1&l=col2";
private static final String TAG = SendFusionTablesUtils.class.getSimpleName();
private SendFusionTablesUtils() {}
/**
* Gets the url to visualize a fusion table on a map.
*
* @param track the track
* @param tableId the table id
* @return the url.
*/
public static String getMapUrl(Track track, String tableId) {
if (track == null || track.getTripStatistics() == null) {
Log.e(TAG, "Invalid track");
return null;
}
double latE6;
double lonE6;
int z;
if (track.getNumberOfPoints() < 2) {
// Use Google's latitude and longitude
latE6 = 37.423 * 1.E6;
lonE6 = -122.084 * 1.E6;
z = 2;
} else {
TripStatistics stats = track.getTripStatistics();
latE6 = stats.getBottom() + (stats.getTop() - stats.getBottom()) / 2;
lonE6 = stats.getLeft() + (stats.getRight() - stats.getLeft()) / 2;
z = 15;
}
// We explicitly format with Locale.US because we need the latitude and
// longitude to be formatted in a locale-independent manner. Specifically,
// we need the decimal separator to be a period rather than a comma.
return String.format(Locale.US, MAP_URL, tableId, latE6 / 1.E6, lonE6 / 1.E6, z);
}
/**
* Formats an array of values as a SQL VALUES like
* ('value1','value2',...,'value_n'). Escapes single quotes with two single
* quotes.
*
* @param values an array of values to format
* @return the formated SQL VALUES.
*/
public static String formatSqlValues(String... values) {
StringBuilder builder = new StringBuilder("(");
for (int i = 0; i < values.length; i++) {
if (i > 0) {
builder.append(',');
}
builder.append('\'');
builder.append(escapeSqlString(values[i]));
builder.append('\'');
}
builder.append(")");
return builder.toString();
}
/**
* Escapes a SQL string. Escapes single quotes with two single quotes.
*
* @param string the string
* @return the escaped string.
*/
@VisibleForTesting
static String escapeSqlString(String string) {
return string.replaceAll("'", "''");
}
/**
* Gets a KML Point value representing a location.
*
* @param location the location
* @return the KML Point value.
*/
public static String getKmlPoint(Location location) {
StringBuilder builder = new StringBuilder("<Point><coordinates>");
if (location != null) {
appendLocation(location, builder);
}
builder.append("</coordinates></Point>");
return builder.toString();
}
/**
* Gets a KML LineString value representing an array of locations.
*
* @param locations the locations.
* @return the KML LineString value.
*/
public static String getKmlLineString(ArrayList<Location> locations) {
StringBuilder builder = new StringBuilder("<LineString><coordinates>");
if (locations != null) {
for (int i = 0; i < locations.size(); i++) {
if (i != 0) {
builder.append(' ');
}
appendLocation(locations.get(i), builder);
}
}
builder.append("</coordinates></LineString>");
return builder.toString();
}
/**
* Appends a location to a string builder using "longitude,latitude[,altitude]" format.
*
* @param location the location
* @param builder the string builder
*/
@VisibleForTesting
static void appendLocation(Location location, StringBuilder builder) {
builder.append(location.getLongitude()).append(",").append(location.getLatitude());
if (location.hasAltitude()) {
builder.append(",");
builder.append(location.getAltitude());
}
}
}
@@ -0,0 +1,457 @@
/*
* 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.gdata;
import com.google.wireless.gdata.client.GDataClient;
import com.google.wireless.gdata.client.HttpException;
import com.google.wireless.gdata.client.QueryParams;
import com.google.wireless.gdata.data.StringUtils;
import com.google.wireless.gdata.parser.ParseException;
import com.google.wireless.gdata.serializer.GDataSerializer;
import android.text.TextUtils;
import android.util.Log;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.util.zip.GZIPInputStream;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.StatusLine;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.entity.AbstractHttpEntity;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.entity.InputStreamEntity;
import org.apache.http.impl.client.DefaultHttpClient;
/**
* Implementation of a GDataClient using GoogleHttpClient to make HTTP requests.
* Always issues GETs and POSTs, using the X-HTTP-Method-Override header when a
* PUT or DELETE is desired, to avoid issues with firewalls, etc., that do not
* allow methods other than GET or POST.
*/
public class AndroidGDataClient implements GDataClient {
private static final String TAG = "GDataClient";
private static final boolean DEBUG = false;
private static final String X_HTTP_METHOD_OVERRIDE = "X-HTTP-Method-Override";
private static final int MAX_REDIRECTS = 10;
private final HttpClient httpClient;
/**
* Interface for creating HTTP requests. Used by
* {@link AndroidGDataClient#createAndExecuteMethod}, since HttpUriRequest
* does not allow for changing the URI after creation, e.g., when you want to
* follow a redirect.
*/
private interface HttpRequestCreator {
HttpUriRequest createRequest(URI uri);
}
private static class GetRequestCreator implements HttpRequestCreator {
public HttpUriRequest createRequest(URI uri) {
return new HttpGet(uri);
}
}
private static class PostRequestCreator implements HttpRequestCreator {
private final String mMethodOverride;
private final HttpEntity mEntity;
public PostRequestCreator(String methodOverride, HttpEntity entity) {
mMethodOverride = methodOverride;
mEntity = entity;
}
public HttpUriRequest createRequest(URI uri) {
HttpPost post = new HttpPost(uri);
if (mMethodOverride != null) {
post.addHeader(X_HTTP_METHOD_OVERRIDE, mMethodOverride);
}
post.setEntity(mEntity);
return post;
}
}
// MAJOR TODO: make this work across redirects (if we can reset the
// InputStream).
// OR, read the bits into a local buffer (yuck, the media could be large).
private static class MediaPutRequestCreator implements HttpRequestCreator {
private final InputStream mMediaInputStream;
private final String mContentType;
public MediaPutRequestCreator(InputStream mediaInputStream,
String contentType) {
mMediaInputStream = mediaInputStream;
mContentType = contentType;
}
public HttpUriRequest createRequest(URI uri) {
HttpPost post = new HttpPost(uri);
post.addHeader(X_HTTP_METHOD_OVERRIDE, "PUT");
InputStreamEntity entity =
new InputStreamEntity(mMediaInputStream, -1 /* read until EOF */);
entity.setContentType(mContentType);
post.setEntity(entity);
return post;
}
}
/**
* Creates a new AndroidGDataClient.
*/
public AndroidGDataClient() {
httpClient = new DefaultHttpClient();
}
public void close() {
}
/*
* (non-Javadoc)
*
* @see GDataClient#encodeUri(java.lang.String)
*/
public String encodeUri(String uri) {
String encodedUri = null;
try {
encodedUri = URLEncoder.encode(uri, "UTF-8");
} catch (UnsupportedEncodingException uee) {
// Should not happen
throw new IllegalStateException("Cannot encode " + uri, uee);
}
return encodedUri;
}
/*
* (non-Javadoc)
*
* @see com.google.wireless.gdata.client.GDataClient#createQueryParams()
*/
public QueryParams createQueryParams() {
return new QueryParamsImpl();
}
// follows redirects
private InputStream createAndExecuteMethod(HttpRequestCreator creator,
String uriString, String authToken) throws HttpException, IOException {
HttpResponse response = null;
int status = 500;
int redirectsLeft = MAX_REDIRECTS;
URI uri;
try {
uri = new URI(uriString);
} catch (URISyntaxException use) {
Log.w(TAG, "Unable to parse " + uriString + " as URI.", use);
throw new IOException("Unable to parse " + uriString + " as URI: "
+ use.getMessage());
}
// we follow redirects ourselves, since we want to follow redirects even on
// POSTs, which
// the HTTP library does not do. following redirects ourselves also allows
// us to log
// the redirects using our own logging.
while (redirectsLeft > 0) {
HttpUriRequest request = creator.createRequest(uri);
request.addHeader("User-Agent", "Android-GData");
request.addHeader("Accept-Encoding", "gzip");
// only add the auth token if not null (to allow for GData feeds that do
// not require
// authentication.)
if (!TextUtils.isEmpty(authToken)) {
request.addHeader("Authorization", "GoogleLogin auth=" + authToken);
}
if (DEBUG) {
for (Header h : request.getAllHeaders()) {
Log.v(TAG, h.getName() + ": " + h.getValue());
}
Log.d(TAG, "Executing " + request.getRequestLine().toString());
}
response = null;
try {
response = httpClient.execute(request);
} catch (IOException ioe) {
Log.w(TAG, "Unable to execute HTTP request." + ioe);
throw ioe;
}
StatusLine statusLine = response.getStatusLine();
if (statusLine == null) {
Log.w(TAG, "StatusLine is null.");
throw new NullPointerException(
"StatusLine is null -- should not happen.");
}
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, response.getStatusLine().toString());
for (Header h : response.getAllHeaders()) {
Log.d(TAG, h.getName() + ": " + h.getValue());
}
}
status = statusLine.getStatusCode();
HttpEntity entity = response.getEntity();
if ((status >= 200) && (status < 300) && entity != null) {
return getUngzippedContent(entity);
}
// TODO: handle 301, 307?
// TODO: let the http client handle the redirects, if we can be sure we'll
// never get a
// redirect on POST.
if (status == 302) {
// consume the content, so the connection can be closed.
entity.consumeContent();
Header location = response.getFirstHeader("Location");
if (location == null) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Redirect requested but no Location " + "specified.");
}
break;
}
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Following redirect to " + location.getValue());
}
try {
uri = new URI(location.getValue());
} catch (URISyntaxException use) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Unable to parse " + location.getValue() + " as URI.",
use);
throw new IOException("Unable to parse " + location.getValue()
+ " as URI.");
}
break;
}
--redirectsLeft;
} else {
break;
}
}
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "Received " + status + " status code.");
}
String errorMessage = null;
HttpEntity entity = response.getEntity();
try {
if (entity != null) {
InputStream in = entity.getContent();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buf = new byte[8192];
int bytesRead = -1;
while ((bytesRead = in.read(buf)) != -1) {
baos.write(buf, 0, bytesRead);
}
// TODO: use appropriate encoding, picked up from Content-Type.
errorMessage = new String(baos.toByteArray());
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, errorMessage);
}
}
} finally {
if (entity != null) {
entity.consumeContent();
}
}
String exceptionMessage = "Received " + status + " status code";
if (errorMessage != null) {
exceptionMessage += (": " + errorMessage);
}
throw new HttpException(exceptionMessage, status, null /* InputStream */);
}
/**
* Gets the input stream from a response entity. If the entity is gzipped
* then this will get a stream over the uncompressed data.
*
* @param entity the entity whose content should be read
* @return the input stream to read from
* @throws IOException
*/
private static InputStream getUngzippedContent(HttpEntity entity)
throws IOException {
InputStream responseStream = entity.getContent();
if (responseStream == null) {
return responseStream;
}
Header header = entity.getContentEncoding();
if (header == null) {
return responseStream;
}
String contentEncoding = header.getValue();
if (contentEncoding == null) {
return responseStream;
}
if (contentEncoding.contains("gzip")){
responseStream = new GZIPInputStream(responseStream);
}
return responseStream;
}
/*
* (non-Javadoc)
*
* @see GDataClient#getFeedAsStream(java.lang.String, java.lang.String)
*/
public InputStream getFeedAsStream(String feedUrl, String authToken)
throws HttpException, IOException {
InputStream in =
createAndExecuteMethod(new GetRequestCreator(), feedUrl, authToken);
if (in != null) {
return in;
}
throw new IOException("Unable to access feed.");
}
public InputStream getMediaEntryAsStream(String mediaEntryUrl,
String authToken) throws HttpException, IOException {
InputStream in =
createAndExecuteMethod(new GetRequestCreator(), mediaEntryUrl,
authToken);
if (in != null) {
return in;
}
throw new IOException("Unable to access media entry.");
}
/*
* (non-Javadoc)
*
* @see GDataClient#createEntry
*/
public InputStream createEntry(String feedUrl, String authToken,
GDataSerializer entry) throws HttpException, IOException {
HttpEntity entity =
createEntityForEntry(entry, GDataSerializer.FORMAT_CREATE);
InputStream in =
createAndExecuteMethod(new PostRequestCreator(null /* override */,
entity), feedUrl, authToken);
if (in != null) {
return in;
}
throw new IOException("Unable to create entry.");
}
/*
* (non-Javadoc)
*
* @see GDataClient#updateEntry
*/
public InputStream updateEntry(String editUri, String authToken,
GDataSerializer entry) throws HttpException, IOException {
HttpEntity entity =
createEntityForEntry(entry, GDataSerializer.FORMAT_UPDATE);
InputStream in =
createAndExecuteMethod(new PostRequestCreator("PUT", entity), editUri,
authToken);
if (in != null) {
return in;
}
throw new IOException("Unable to update entry.");
}
/*
* (non-Javadoc)
*
* @see GDataClient#deleteEntry
*/
public void deleteEntry(String editUri, String authToken)
throws HttpException, IOException {
if (StringUtils.isEmpty(editUri)) {
throw new IllegalArgumentException(
"you must specify an non-empty edit url");
}
InputStream in =
createAndExecuteMethod(
new PostRequestCreator("DELETE", null /* entity */), editUri,
authToken);
if (in == null) {
throw new IOException("Unable to delete entry.");
}
try {
in.close();
} catch (IOException ioe) {
// ignore
}
}
public InputStream updateMediaEntry(String editUri, String authToken,
InputStream mediaEntryInputStream, String contentType)
throws HttpException, IOException {
InputStream in =
createAndExecuteMethod(new MediaPutRequestCreator(
mediaEntryInputStream, contentType), editUri, authToken);
if (in != null) {
return in;
}
throw new IOException("Unable to write media entry.");
}
private HttpEntity createEntityForEntry(GDataSerializer entry, int format)
throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
entry.serialize(baos, format);
} catch (IOException ioe) {
Log.e(TAG, "Unable to serialize entry.", ioe);
throw ioe;
} catch (ParseException pe) {
Log.e(TAG, "Unable to serialize entry.", pe);
throw new IOException("Unable to serialize entry: " + pe.getMessage());
}
byte[] entryBytes = baos.toByteArray();
if (entryBytes != null && Log.isLoggable(TAG, Log.DEBUG)) {
try {
Log.d(TAG, "Serialized entry: " + new String(entryBytes, "UTF-8"));
} catch (UnsupportedEncodingException uee) {
// should not happen
throw new IllegalStateException("UTF-8 should be supported!", uee);
}
}
AbstractHttpEntity entity = new ByteArrayEntity(entryBytes);
entity.setContentType(entry.getContentType());
return entity;
}
}
@@ -0,0 +1,52 @@
/*
* 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.gdata;
import com.google.wireless.gdata.client.GDataClient;
import android.content.Context;
import android.util.Log;
/**
* This factory will fetch the right class for the platform.
*
* @author Sandor Dornbush
*/
public class GDataClientFactory {
private static final String TAG = GDataClientFactory.class.getSimpleName();
private GDataClientFactory() { }
/**
* Creates a new GData client.
* This factory will fetch the right class for the platform.
* @return A GDataClient appropriate for this platform
*/
public static GDataClient getGDataClient(Context context) {
// TODO This should be moved into ApiAdapter
try {
// Try to use the official unbundled gdata client implementation.
// This should work on Froyo and beyond.
return new com.google.android.common.gdata.AndroidGDataClient(context);
} catch (LinkageError e) {
// On all other platforms use the client implementation packaged in the
// apk.
Log.i(TAG, "Using mytracks AndroidGDataClient.", e);
return new com.google.android.apps.mytracks.io.gdata.AndroidGDataClient();
}
}
}
@@ -0,0 +1,103 @@
/*
* 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.gdata;
import com.google.wireless.gdata.client.QueryParams;
import android.text.TextUtils;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/**
* Simple implementation of the QueryParams interface.
*/
// TODO: deal with categories
public class QueryParamsImpl extends QueryParams {
private final Map<String, String> mParams = new HashMap<String, String>();
@Override
public void clear() {
setEntryId(null);
mParams.clear();
}
@Override
public String generateQueryUrl(String feedUrl) {
if (TextUtils.isEmpty(getEntryId()) && mParams.isEmpty()) {
// nothing to do
return feedUrl;
}
// handle entry IDs
if (!TextUtils.isEmpty(getEntryId())) {
if (!mParams.isEmpty()) {
throw new IllegalStateException("Cannot set both an entry ID "
+ "and other query paramters.");
}
return feedUrl + '/' + getEntryId();
}
// otherwise, append the querystring params.
StringBuilder sb = new StringBuilder();
sb.append(feedUrl);
Set<String> params = mParams.keySet();
boolean first = true;
if (feedUrl.contains("?")) {
first = false;
} else {
sb.append('?');
}
for (String param : params) {
if (first) {
first = false;
} else {
sb.append('&');
}
sb.append(param);
sb.append('=');
String value = mParams.get(param);
String encodedValue = null;
try {
encodedValue = URLEncoder.encode(value, "UTF-8");
} catch (UnsupportedEncodingException uee) {
// Should not happen
throw new IllegalStateException("Cannot encode " + value, uee);
}
sb.append(encodedValue);
}
return sb.toString();
}
@Override
public String getParamValue(String param) {
if (!(mParams.containsKey(param))) {
return null;
}
return mParams.get(param);
}
@Override
public void setParamValue(String param, String value) {
mParams.put(param, value);
}
}
@@ -0,0 +1,36 @@
// Copyright 2010 Google Inc. All Rights Reserved.
package com.google.android.apps.mytracks.io.gdata.maps;
import com.google.wireless.gdata.data.Entry;
import java.util.HashMap;
import java.util.Map;
/**
* GData entry for a map feature.
*/
public class MapFeatureEntry extends Entry {
private String mPrivacy = null;
private Map<String, String> mAttributes = new HashMap<String, String>();
public void setPrivacy(String privacy) {
mPrivacy = privacy;
}
public String getPrivacy() {
return mPrivacy;
}
public void setAttribute(String name, String value) {
mAttributes.put(name, value);
}
public void removeAttribute(String name) {
mAttributes.remove(name);
}
public Map<String, String> getAllAttributes() {
return mAttributes;
}
}
@@ -0,0 +1,102 @@
// Copyright 2010 Google Inc. All Rights Reserved.
package com.google.android.apps.mytracks.io.gdata.maps;
import com.google.wireless.gdata.client.GDataClient;
import com.google.wireless.gdata.client.GDataParserFactory;
import com.google.wireless.gdata.client.GDataServiceClient;
import android.util.Log;
import java.util.Locale;
/**
* Client to talk to Google Maps via GData.
*/
public class MapsClient extends GDataServiceClient {
private static final boolean DEBUG = false;
public static final boolean LOG_COMMUNICATION = false;
private static final String MAPS_BASE_FEED_URL =
"http://maps.google.com/maps/feeds/";
private static final String MAPS_MAP_FEED_PATH = "maps/default/full";
private static final String MAPS_FEATURE_FEED_PATH_BEFORE_MAPID = "features/";
private static final String MAPS_FEATURE_FEED_PATH_AFTER_MAPID = "/full";
private static final String MAPS_VERSION_FEED_PATH_FORMAT =
"%smaps/%s/versions/%s/full/%s";
private static final String MAP_ENTRY_ID_BEFORE_USER_ID = "maps/feeds/maps/";
private static final String MAP_ENTRY_ID_BETWEEN_USER_ID_AND_MAP_ID = "/";
private static final String V2_ONLY_PARAM = "?v=2.0";
public MapsClient(GDataClient dataClient,
GDataParserFactory dataParserFactory) {
super(dataClient, dataParserFactory);
}
@Override
public String getServiceName() {
return MapsConstants.SERVICE_NAME;
}
public static String buildMapUrl(String mapId) {
return MapsConstants.MAPSHOP_BASE_URL + "?msa=0&msid=" + mapId;
}
public static String getMapsFeed() {
if (DEBUG) {
Log.d("Maps Client", "Requesting map feed:");
}
return MAPS_BASE_FEED_URL + MAPS_MAP_FEED_PATH + V2_ONLY_PARAM;
}
public static String getFeaturesFeed(String mapid) {
StringBuilder feed = new StringBuilder();
feed.append(MAPS_BASE_FEED_URL);
feed.append(MAPS_FEATURE_FEED_PATH_BEFORE_MAPID);
feed.append(mapid);
feed.append(MAPS_FEATURE_FEED_PATH_AFTER_MAPID);
feed.append(V2_ONLY_PARAM);
return feed.toString();
}
public static String getMapIdFromMapEntryId(String entryId) {
String userId = null;
String mapId = null;
if (DEBUG) {
Log.d("Maps GData Client", "Getting mapid from entry id: " + entryId);
}
int userIdStart =
entryId.indexOf(MAP_ENTRY_ID_BEFORE_USER_ID)
+ MAP_ENTRY_ID_BEFORE_USER_ID.length();
int userIdEnd =
entryId.indexOf(MAP_ENTRY_ID_BETWEEN_USER_ID_AND_MAP_ID, userIdStart);
if (userIdStart >= 0 && userIdEnd < entryId.length()
&& userIdStart <= userIdEnd) {
userId = entryId.substring(userIdStart, userIdEnd);
}
int mapIdStart =
entryId.indexOf(MAP_ENTRY_ID_BETWEEN_USER_ID_AND_MAP_ID, userIdEnd)
+ MAP_ENTRY_ID_BETWEEN_USER_ID_AND_MAP_ID.length();
if (mapIdStart >= 0 && mapIdStart < entryId.length()) {
mapId = entryId.substring(mapIdStart);
}
if (userId == null) {
userId = "";
}
if (mapId == null) {
mapId = "";
}
if (DEBUG) {
Log.d("Maps GData Client", "Got user id: " + userId);
Log.d("Maps GData Client", "Got map id: " + mapId);
}
return userId + "." + mapId;
}
public static String getVersionFeed(String versionUserId,
String versionClient, String currentVersion) {
return String.format(Locale.US, MAPS_VERSION_FEED_PATH_FORMAT, MAPS_BASE_FEED_URL,
versionUserId, versionClient, currentVersion);
}
}
@@ -0,0 +1,16 @@
// Copyright 2009 Google Inc. All Rights Reserved.
package com.google.android.apps.mytracks.io.gdata.maps;
/**
* Constants for Google Maps.
*/
public class MapsConstants {
static final String MAPSHOP_BASE_URL =
"https://maps.google.com/maps/ms";
public static final String SERVICE_NAME = "local";
/**
* Private constructor to prevent instantiation.
*/
private MapsConstants() { }
}
@@ -0,0 +1,219 @@
// Copyright 2009 Google Inc. All Rights Reserved.
package com.google.android.apps.mytracks.io.gdata.maps;
import android.location.Location;
import java.util.Random;
import java.util.Vector;
/**
* MapsFeature contains all of the data associated with a feature in Google
* Maps, where a feature is a marker, line, or shape. Some of the data is stored
* in a {@link MapsFeatureMetadata} object so that it can be more efficiently
* transmitted to other activities.
*/
public class MapsFeature {
/** A marker feature displays an icon at a single point on the map. */
public static final int MARKER = 0;
/**
* A line feature displays a line connecting a set of points on the map.
*/
public static final int LINE = 1;
/**
* A shape feature displays a border defined by connecting a set of points,
* including connecting the last to the first, and displays the area
* confined by this border.
*/
public static final int SHAPE = 2;
/** The local feature id for this feature, if needed. */
private String androidId;
// Points of this feature in order.
private final Vector<Location> points = new Vector<Location>();
/** The metadata of this feature in a format efficient for transmission. */
private MapsFeatureMetadata featureInfo = new MapsFeatureMetadata();
private final Random random = new Random();
/**
* Initializes a valid but empty feature. It will default to a
* {@link #MARKER} with a blue placemark with a dot as an icon at the
* location (0, 0).
*/
public MapsFeature() {
}
/**
* Adds a new point to the end of this feature.
*
* @param location the new point to add
*/
public void addPoint(Location location) {
points.add(location);
}
/**
* Generates a new local id for this feature based on the current time and
* a random number.
*/
public void generateAndroidId() {
long time = System.currentTimeMillis();
int rand = random.nextInt(10000);
androidId = time + "." + rand;
}
/**
* Retrieves the current local id for this feature if one is available.
*
* @return The local id for this feature
*/
public String getAndroidId() {
return androidId;
}
/**
* Retrieves the current (html) description of this feature. The description
* is stored in the feature metadata.
*
* @return The description of this feature
*/
public String getDescription() {
return featureInfo.getDescription();
}
/**
* Sets the description of this feature. That description is stored in the
* feature metadata.
*
* @param description The new description of this feature
*/
public void setDescription(String description) {
featureInfo.setDescription(description);
}
/**
* Gets the point at a given index.
*
* @param index the index
*/
public Location getPoint(int index) {
if (index >= points.size()) {
return null;
}
return points.get(index);
}
/**
* Gets the number of points.
*/
public int getPointCount() {
return points.size();
}
/**
* Retrieves the title of this feature. That title is stored in the feature
* metadata.
*
* @return the current title of this feature
*/
public String getTitle() {
return featureInfo.getTitle();
}
/**
* Retrieves the type of this feature. That type is stored in the feature
* metadata.
*
* @return One of {@link #MARKER}, {@link #LINE}, or {@link #SHAPE}
* identifying the type of this feature
*/
public int getType() {
return featureInfo.getType();
}
/**
* Retrieves the current color of this feature as an ARGB color integer.
* That color is stored in the feature metadata.
*
* @return The ARGB color of this feature
*/
public int getColor() {
return featureInfo.getColor();
}
/**
* Retrieves the current line width of this feature. That line width is
* stored in the feature metadata.
*
* @return The line width of this feature
*/
public int getLineWidth() {
return featureInfo.getLineWidth();
}
/**
* Retrieves the current fill color of this feature as an ARGB color
* integer. That color is stored in the feature metadata.
*
* @return The ARGB fill color of this feature
*/
public int getFillColor() {
return featureInfo.getFillColor();
}
/**
* Retrieves the current icon url of this feature. That icon url is stored
* in the feature metadata.
*
* @return The icon url for this feature
*/
public String getIconUrl() {
return featureInfo.getIconUrl();
}
/**
* Sets the title of this feature. That title is stored in the feature
* metadata.
*
* @param title The new title of this feature
*/
public void setTitle(String title) {
featureInfo.setTitle(title);
}
/**
* Sets the type of this feature. That type is stored in the feature
* metadata.
*
* @param type The new type of the feature. That type must be one of
* {@link #MARKER}, {@link #LINE}, or {@link #SHAPE}
*/
public void setType(int type) {
featureInfo.setType(type);
}
/**
* Sets the ARGB color of this feature. That color is stored in the feature
* metadata.
*
* @param color The new ARGB color of this feature
*/
public void setColor(int color) {
featureInfo.setColor(color);
}
/**
* Sets the icon url of this feature. That icon url is stored in the feature
* metadata.
*
* @param url The new icon url of the feature
*/
public void setIconUrl(String url) {
featureInfo.setIconUrl(url);
}
}
@@ -0,0 +1,88 @@
// Copyright 2009 Google Inc. All Rights Reserved.
package com.google.android.apps.mytracks.io.gdata.maps;
/**
* Metadata about a maps feature.
*/
class MapsFeatureMetadata {
private static final String BLUE_DOT_URL =
"http://maps.google.com/mapfiles/ms/micons/blue-dot.png";
private static final int DEFAULT_COLOR = 0x800000FF;
private static final int DEFAULT_FILL_COLOR = 0xC00000FF;
private String title;
private String description;
private int type;
private int color;
private int lineWidth;
private int fillColor;
private String iconUrl;
public MapsFeatureMetadata() {
title = "";
description = "";
type = MapsFeature.MARKER;
color = DEFAULT_COLOR;
lineWidth = 5;
fillColor = DEFAULT_FILL_COLOR;
iconUrl = BLUE_DOT_URL;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public int getType() {
return type;
}
public void setType(int type) {
this.type = type;
}
public int getColor() {
return color;
}
public void setColor(int color) {
this.color = color;
}
public int getLineWidth() {
return lineWidth;
}
public void setLineWidth(int width) {
lineWidth = width;
}
public int getFillColor() {
return fillColor;
}
public void setFillColor(int color) {
fillColor = color;
}
public String getIconUrl() {
return iconUrl;
}
public void setIconUrl(String url) {
iconUrl = url;
}
}
@@ -0,0 +1,178 @@
// Copyright 2009 Google Inc. All Rights Reserved.
package com.google.android.apps.mytracks.io.gdata.maps;
import com.google.wireless.gdata.data.Entry;
import com.google.wireless.gdata.data.StringUtils;
import android.graphics.Color;
import android.util.Log;
import java.io.IOException;
import java.io.StringWriter;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;
import org.xmlpull.v1.XmlSerializer;
/**
* Converter from GData objects to Maps objects.
*/
public class MapsGDataConverter {
private final XmlSerializer xmlSerializer;
public MapsGDataConverter() throws XmlPullParserException {
xmlSerializer = XmlPullParserFactory.newInstance().newSerializer();
}
public static MapsMapMetadata getMapMetadataForEntry(
MapFeatureEntry entry) {
MapsMapMetadata metadata = new MapsMapMetadata();
if ("public".equals(entry.getPrivacy())) {
metadata.setSearchable(true);
} else {
metadata.setSearchable(false);
}
metadata.setTitle(entry.getTitle());
metadata.setDescription(entry.getSummary());
String editUri = entry.getEditUri();
if (editUri != null) {
metadata.setGDataEditUri(editUri);
}
return metadata;
}
public static String getMapidForEntry(Entry entry) {
return MapsClient.getMapIdFromMapEntryId(entry.getId());
}
public static Entry getMapEntryForMetadata(MapsMapMetadata metadata) {
MapFeatureEntry entry = new MapFeatureEntry();
entry.setEditUri(metadata.getGDataEditUri());
entry.setTitle(metadata.getTitle());
entry.setSummary(metadata.getDescription());
entry.setPrivacy(metadata.getSearchable() ? "public" : "unlisted");
entry.setAuthor("android");
entry.setEmail("nobody@google.com");
return entry;
}
public MapFeatureEntry getEntryForFeature(MapsFeature feature) {
MapFeatureEntry entry = new MapFeatureEntry();
entry.setTitle(feature.getTitle());
entry.setAuthor("android");
entry.setEmail("nobody@google.com");
entry.setCategoryScheme("http://schemas.google.com/g/2005#kind");
entry.setCategory("http://schemas.google.com/g/2008#mapfeature");
entry.setEditUri("");
if (!StringUtils.isEmpty(feature.getAndroidId())) {
entry.setAttribute("_androidId", feature.getAndroidId());
}
try {
StringWriter writer = new StringWriter();
xmlSerializer.setOutput(writer);
xmlSerializer.startTag(null, "Placemark");
xmlSerializer.attribute(null, "xmlns", "http://earth.google.com/kml/2.2");
xmlSerializer.startTag(null, "Style");
if (feature.getType() == MapsFeature.MARKER) {
xmlSerializer.startTag(null, "IconStyle");
xmlSerializer.startTag(null, "Icon");
xmlSerializer.startTag(null, "href");
xmlSerializer.text(feature.getIconUrl());
xmlSerializer.endTag(null, "href");
xmlSerializer.endTag(null, "Icon");
xmlSerializer.endTag(null, "IconStyle");
} else {
xmlSerializer.startTag(null, "LineStyle");
xmlSerializer.startTag(null, "color");
int color = feature.getColor();
// Reverse the color because KML is ABGR and Android is ARGB
xmlSerializer.text(Integer.toHexString(
Color.argb(Color.alpha(color), Color.blue(color),
Color.green(color), Color.red(color))));
xmlSerializer.endTag(null, "color");
xmlSerializer.startTag(null, "width");
xmlSerializer.text(Integer.toString(feature.getLineWidth()));
xmlSerializer.endTag(null, "width");
xmlSerializer.endTag(null, "LineStyle");
if (feature.getType() == MapsFeature.SHAPE) {
xmlSerializer.startTag(null, "PolyStyle");
xmlSerializer.startTag(null, "color");
int fcolor = feature.getFillColor();
// Reverse the color because KML is ABGR and Android is ARGB
xmlSerializer.text(Integer.toHexString(Color.argb(Color.alpha(fcolor),
Color.blue(fcolor), Color.green(fcolor), Color.red(fcolor))));
xmlSerializer.endTag(null, "color");
xmlSerializer.startTag(null, "fill");
xmlSerializer.text("1");
xmlSerializer.endTag(null, "fill");
xmlSerializer.startTag(null, "outline");
xmlSerializer.text("1");
xmlSerializer.endTag(null, "outline");
xmlSerializer.endTag(null, "PolyStyle");
}
}
xmlSerializer.endTag(null, "Style");
xmlSerializer.startTag(null, "name");
xmlSerializer.text(feature.getTitle());
xmlSerializer.endTag(null, "name");
xmlSerializer.startTag(null, "description");
xmlSerializer.cdsect(feature.getDescription());
xmlSerializer.endTag(null, "description");
StringBuilder pointBuilder = new StringBuilder();
for (int i = 0; i < feature.getPointCount(); ++i) {
if (i > 0) {
pointBuilder.append('\n');
}
pointBuilder.append(feature.getPoint(i).getLongitude());
pointBuilder.append(',');
pointBuilder.append(feature.getPoint(i).getLatitude());
pointBuilder.append(",0.000000");
}
String pointString = pointBuilder.toString();
if (feature.getType() == MapsFeature.MARKER) {
xmlSerializer.startTag(null, "Point");
xmlSerializer.startTag(null, "coordinates");
xmlSerializer.text(pointString);
xmlSerializer.endTag(null, "coordinates");
xmlSerializer.endTag(null, "Point");
} else if (feature.getType() == MapsFeature.LINE) {
xmlSerializer.startTag(null, "LineString");
xmlSerializer.startTag(null, "tessellate");
xmlSerializer.text("1");
xmlSerializer.endTag(null, "tessellate");
xmlSerializer.startTag(null, "coordinates");
xmlSerializer.text(pointString);
xmlSerializer.endTag(null, "coordinates");
xmlSerializer.endTag(null, "LineString");
} else {
xmlSerializer.startTag(null, "Polygon");
xmlSerializer.startTag(null, "outerBoundaryIs");
xmlSerializer.startTag(null, "LinearRing");
xmlSerializer.startTag(null, "tessellate");
xmlSerializer.text("1");
xmlSerializer.endTag(null, "tessellate");
xmlSerializer.startTag(null, "coordinates");
xmlSerializer.text(pointString + "\n"
+ Double.toString(feature.getPoint(0).getLongitude())
+ ","
+ Double.toString(feature.getPoint(0).getLatitude())
+ ",0.000000");
xmlSerializer.endTag(null, "coordinates");
xmlSerializer.endTag(null, "LinearRing");
xmlSerializer.endTag(null, "outerBoundaryIs");
xmlSerializer.endTag(null, "Polygon");
}
xmlSerializer.endTag(null, "Placemark");
xmlSerializer.flush();
entry.setContent(writer.toString());
Log.d("My Google Maps", "Edit URI: " + entry.getEditUri());
} catch (IOException e) {
e.printStackTrace();
}
return entry;
}
}
@@ -0,0 +1,52 @@
// Copyright 2009 Google Inc. All Rights Reserved.
package com.google.android.apps.mytracks.io.gdata.maps;
/**
* Metadata about a Google Maps map.
*/
public class MapsMapMetadata {
private String title;
private String description;
private String gdataEditUri;
private boolean searchable;
public MapsMapMetadata() {
title = "";
description = "";
gdataEditUri = "";
searchable = false;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public boolean getSearchable() {
return searchable;
}
public void setSearchable(boolean searchable) {
this.searchable = searchable;
}
public String getGDataEditUri() {
return gdataEditUri;
}
public void setGDataEditUri(String editUri) {
this.gdataEditUri = editUri;
}
}
@@ -0,0 +1,163 @@
// Copyright 2009 Google Inc. All Rights Reserved.
package com.google.android.apps.mytracks.io.gdata.maps;
import com.google.wireless.gdata.data.Entry;
import com.google.wireless.gdata.data.Feed;
import com.google.wireless.gdata.data.XmlUtils;
import com.google.wireless.gdata.parser.ParseException;
import com.google.wireless.gdata.parser.xml.XmlGDataParser;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import java.io.InputStream;
/**
* Parser for XML gdata maps data.
*/
class XmlMapsGDataParser extends XmlGDataParser {
public XmlMapsGDataParser(InputStream is, XmlPullParser xpp)
throws ParseException {
super(is, xpp);
}
@Override
protected Feed createFeed() {
return new Feed();
}
@Override
protected Entry createEntry() {
return new MapFeatureEntry();
}
@Override
protected void handleExtraElementInFeed(Feed feed) {
// Do nothing
}
@Override
protected void handleExtraLinkInEntry(
String rel, String type, String href, Entry entry)
throws XmlPullParserException, IOException {
if (!(entry instanceof MapFeatureEntry)) {
throw new IllegalArgumentException("Expected MapFeatureEntry!");
}
if (rel.endsWith("#view")) {
return;
}
super.handleExtraLinkInEntry(rel, type, href, entry);
}
/**
* Parses the current entry in the XML document. Assumes that the parser is
* currently pointing just after an &lt;entry&gt;.
*
* @param plainEntry The entry that will be filled.
* @throws XmlPullParserException Thrown if the XML cannot be parsed.
* @throws IOException Thrown if the underlying inputstream cannot be read.
*/
@Override
protected void handleEntry(Entry plainEntry)
throws XmlPullParserException, IOException, ParseException {
XmlPullParser parser = getParser();
if (!(plainEntry instanceof MapFeatureEntry)) {
throw new IllegalArgumentException("Expected MapFeatureEntry!");
}
MapFeatureEntry entry = (MapFeatureEntry) plainEntry;
int eventType = parser.getEventType();
entry.setPrivacy("public");
while (eventType != XmlPullParser.END_DOCUMENT) {
switch (eventType) {
case XmlPullParser.START_TAG:
String name = parser.getName();
if ("entry".equals(name)) {
// stop parsing here.
return;
} else if ("id".equals(name)) {
entry.setId(XmlUtils.extractChildText(parser));
} else if ("title".equals(name)) {
entry.setTitle(XmlUtils.extractChildText(parser));
} else if ("link".equals(name)) {
String rel = parser.getAttributeValue(null /* ns */, "rel");
String type = parser.getAttributeValue(null /* ns */, "type");
String href = parser.getAttributeValue(null /* ns */, "href");
if ("edit".equals(rel)) {
entry.setEditUri(href);
} else if ("alternate".equals(rel) && "text/html".equals(type)) {
entry.setHtmlUri(href);
} else {
handleExtraLinkInEntry(rel, type, href, entry);
}
} else if ("summary".equals(name)) {
entry.setSummary(XmlUtils.extractChildText(parser));
} else if ("content".equals(name)) {
StringBuilder contentBuilder = new StringBuilder();
int parentDepth = parser.getDepth();
while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
int etype = parser.next();
switch (etype) {
case XmlPullParser.START_TAG:
contentBuilder.append('<');
contentBuilder.append(parser.getName());
contentBuilder.append('>');
break;
case XmlPullParser.TEXT:
contentBuilder.append("<![CDATA[");
contentBuilder.append(parser.getText());
contentBuilder.append("]]>");
break;
case XmlPullParser.END_TAG:
if (parser.getDepth() > parentDepth) {
contentBuilder.append("</");
contentBuilder.append(parser.getName());
contentBuilder.append('>');
}
break;
}
if (etype == XmlPullParser.END_TAG
&& parser.getDepth() == parentDepth) {
break;
}
}
entry.setContent(contentBuilder.toString());
} else if ("category".equals(name)) {
String category = parser.getAttributeValue(null /* ns */, "term");
if (category != null && category.length() > 0) {
entry.setCategory(category);
}
String categoryScheme =
parser.getAttributeValue(null /* ns */, "scheme");
if (categoryScheme != null && category.length() > 0) {
entry.setCategoryScheme(categoryScheme);
}
} else if ("published".equals(name)) {
entry.setPublicationDate(XmlUtils.extractChildText(parser));
} else if ("updated".equals(name)) {
entry.setUpdateDate(XmlUtils.extractChildText(parser));
} else if ("deleted".equals(name)) {
entry.setDeleted(true);
} else if ("draft".equals(name)) {
String draft = XmlUtils.extractChildText(parser);
entry.setPrivacy("yes".equals(draft) ? "unlisted" : "public");
} else if ("customProperty".equals(name)) {
String attrName = parser.getAttributeValue(null, "name");
String attrValue = XmlUtils.extractChildText(parser);
entry.setAttribute(attrName, attrValue);
} else if ("deleted".equals(name)) {
entry.setDeleted(true);
} else {
handleExtraElementInEntry(entry);
}
break;
default:
break;
}
eventType = parser.next();
}
}
}
@@ -0,0 +1,94 @@
// Copyright 2010 Google Inc. All Rights Reserved.
package com.google.android.apps.mytracks.io.gdata.maps;
import com.google.wireless.gdata.client.GDataParserFactory;
import com.google.wireless.gdata.data.Entry;
import com.google.wireless.gdata.parser.GDataParser;
import com.google.wireless.gdata.parser.ParseException;
import com.google.wireless.gdata.parser.xml.XmlGDataParser;
import com.google.wireless.gdata.parser.xml.XmlParserFactory;
import com.google.wireless.gdata.serializer.GDataSerializer;
import com.google.wireless.gdata.serializer.xml.XmlEntryGDataSerializer;
import android.util.Log;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import org.xmlpull.v1.XmlPullParserException;
/**
* Factory of Xml parsers for gdata maps data.
*/
public class XmlMapsGDataParserFactory implements GDataParserFactory {
private XmlParserFactory xmlFactory;
public XmlMapsGDataParserFactory(XmlParserFactory xmlFactory) {
this.xmlFactory = xmlFactory;
}
@Override
public GDataParser createParser(InputStream is) throws ParseException {
is = maybeLogCommunication(is);
try {
return new XmlGDataParser(is, xmlFactory.createParser());
} catch (XmlPullParserException e) {
e.printStackTrace();
return null;
}
}
@SuppressWarnings({ "unchecked", "rawtypes" })
@Override
public GDataParser createParser(Class cls, InputStream is)
throws ParseException {
is = maybeLogCommunication(is);
try {
return createParserForClass(cls, is);
} catch (XmlPullParserException e) {
e.printStackTrace();
return null;
}
}
private InputStream maybeLogCommunication(InputStream is)
throws ParseException {
if (MapsClient.LOG_COMMUNICATION) {
StringBuilder builder = new StringBuilder();
byte[] buffer = new byte[2048];
try {
for (int n = is.read(buffer); n >= 0; n = is.read(buffer)) {
String part = new String(buffer, 0, n);
builder.append(part);
Log.d("Response part", part);
}
} catch (IOException e) {
throw new ParseException("Could not read stream", e);
}
String whole = builder.toString();
Log.d("Response", whole);
is = new ByteArrayInputStream(whole.getBytes());
}
return is;
}
private GDataParser createParserForClass(
Class<? extends Entry> cls, InputStream is)
throws ParseException, XmlPullParserException {
if (cls == MapFeatureEntry.class) {
return new XmlMapsGDataParser(is, xmlFactory.createParser());
} else {
return new XmlGDataParser(is, xmlFactory.createParser());
}
}
@Override
public GDataSerializer createSerializer(Entry en) {
if (en instanceof MapFeatureEntry) {
return new XmlMapsGDataSerializer(xmlFactory, (MapFeatureEntry) en);
} else {
return new XmlEntryGDataSerializer(xmlFactory, en);
}
}
}
@@ -0,0 +1,262 @@
// Copyright 2010 Google Inc. All Rights Reserved.
package com.google.android.apps.mytracks.io.gdata.maps;
import com.google.android.apps.mytracks.io.sync.SyncUtils;
import com.google.wireless.gdata.data.StringUtils;
import com.google.wireless.gdata.parser.ParseException;
import com.google.wireless.gdata.parser.xml.XmlGDataParser;
import com.google.wireless.gdata.parser.xml.XmlParserFactory;
import com.google.wireless.gdata.serializer.xml.XmlEntryGDataSerializer;
import android.util.Log;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Map;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlSerializer;
/**
* Serializer of maps data for GData.
*/
class XmlMapsGDataSerializer extends XmlEntryGDataSerializer {
private static final String APP_NAMESPACE = "http://www.w3.org/2007/app";
private MapFeatureEntry entry;
private XmlParserFactory factory;
private OutputStream stream;
public XmlMapsGDataSerializer(XmlParserFactory factory, MapFeatureEntry entry) {
super(factory, entry);
this.factory = factory;
this.entry = entry;
}
@Override
public void serialize(OutputStream out, int format)
throws IOException, ParseException {
XmlSerializer serializer = null;
try {
serializer = factory.createSerializer();
} catch (XmlPullParserException e) {
throw new ParseException("Unable to create XmlSerializer.", e);
}
ByteArrayOutputStream printStream;
if (MapsClient.LOG_COMMUNICATION) {
printStream = new ByteArrayOutputStream();
serializer.setOutput(printStream, "UTF-8");
} else {
serializer.setOutput(out, "UTF-8");
}
serializer.startDocument("UTF-8", Boolean.FALSE);
declareEntryNamespaces(serializer);
serializer.startTag(XmlGDataParser.NAMESPACE_ATOM_URI, "entry");
if (MapsClient.LOG_COMMUNICATION) {
stream = printStream;
} else {
stream = out;
}
serializeEntryContents(serializer, format);
serializer.endTag(XmlGDataParser.NAMESPACE_ATOM_URI, "entry");
serializer.endDocument();
serializer.flush();
if (MapsClient.LOG_COMMUNICATION) {
Log.d("Request", printStream.toString());
out.write(printStream.toByteArray());
stream = out;
}
}
private final void declareEntryNamespaces(XmlSerializer serializer)
throws IOException {
serializer.setPrefix(
"" /* default ns */, XmlGDataParser.NAMESPACE_ATOM_URI);
serializer.setPrefix(
XmlGDataParser.NAMESPACE_GD, XmlGDataParser.NAMESPACE_GD_URI);
declareExtraEntryNamespaces(serializer);
}
private final void serializeEntryContents(XmlSerializer serializer,
int format) throws IOException {
if (format != FORMAT_CREATE) {
serializeId(serializer, entry.getId());
}
serializeTitle(serializer, entry.getTitle());
if (format != FORMAT_CREATE) {
serializeLink(serializer,
"edit" /* rel */, entry.getEditUri(), null /* type */);
serializeLink(serializer,
"alternate" /* rel */, entry.getHtmlUri(), "text/html" /* type */);
}
serializeSummary(serializer, entry.getSummary());
serializeContent(serializer, entry.getContent());
serializeAuthor(serializer, entry.getAuthor(), entry.getEmail());
serializeCategory(serializer,
entry.getCategory(), entry.getCategoryScheme());
if (format == FORMAT_FULL) {
serializePublicationDate(serializer, entry.getPublicationDate());
}
if (format != FORMAT_CREATE) {
serializeUpdateDate(serializer, entry.getUpdateDate());
}
serializeExtraEntryContents(serializer, format);
}
private static void serializeId(XmlSerializer serializer, String id)
throws IOException {
if (StringUtils.isEmpty(id)) {
return;
}
serializer.startTag(null /* ns */, "id");
serializer.text(id);
serializer.endTag(null /* ns */, "id");
}
private static void serializeTitle(XmlSerializer serializer, String title)
throws IOException {
if (StringUtils.isEmpty(title)) {
return;
}
serializer.startTag(null /* ns */, "title");
serializer.text(title);
serializer.endTag(null /* ns */, "title");
}
public static void serializeLink(XmlSerializer serializer, String rel,
String href, String type) throws IOException {
if (StringUtils.isEmpty(href)) {
return;
}
serializer.startTag(null /* ns */, "link");
serializer.attribute(null /* ns */, "rel", rel);
serializer.attribute(null /* ns */, "href", href);
if (!StringUtils.isEmpty(type)) {
serializer.attribute(null /* ns */, "type", type);
}
serializer.endTag(null /* ns */, "link");
}
private static void serializeSummary(XmlSerializer serializer, String summary)
throws IOException {
if (StringUtils.isEmpty(summary)) {
return;
}
serializer.startTag(null /* ns */, "summary");
serializer.text(summary);
serializer.endTag(null /* ns */, "summary");
}
private void serializeContent(XmlSerializer serializer, String content)
throws IOException {
if (content == null) {
return;
}
serializer.startTag(null /* ns */, "content");
if (content.contains("</Placemark>")) {
serializer.attribute(null /* ns */, "type", SyncUtils.KML_MIME_TYPE);
serializer.flush();
stream.write(content.getBytes());
} else {
serializer.text(content);
}
serializer.endTag(null /* ns */, "content");
}
private static void serializeAuthor(XmlSerializer serializer, String author,
String email) throws IOException {
if (StringUtils.isEmpty(author) || StringUtils.isEmpty(email)) {
return;
}
serializer.startTag(null /* ns */, "author");
serializer.startTag(null /* ns */, "name");
serializer.text(author);
serializer.endTag(null /* ns */, "name");
serializer.startTag(null /* ns */, "email");
serializer.text(email);
serializer.endTag(null /* ns */, "email");
serializer.endTag(null /* ns */, "author");
}
private static void serializeCategory(XmlSerializer serializer,
String category, String categoryScheme) throws IOException {
if (StringUtils.isEmpty(category) && StringUtils.isEmpty(categoryScheme)) {
return;
}
serializer.startTag(null /* ns */, "category");
if (!StringUtils.isEmpty(category)) {
serializer.attribute(null /* ns */, "term", category);
}
if (!StringUtils.isEmpty(categoryScheme)) {
serializer.attribute(null /* ns */, "scheme", categoryScheme);
}
serializer.endTag(null /* ns */, "category");
}
private static void serializePublicationDate(XmlSerializer serializer,
String publicationDate) throws IOException {
if (StringUtils.isEmpty(publicationDate)) {
return;
}
serializer.startTag(null /* ns */, "published");
serializer.text(publicationDate);
serializer.endTag(null /* ns */, "published");
}
private static void serializeUpdateDate(XmlSerializer serializer,
String updateDate) throws IOException {
if (StringUtils.isEmpty(updateDate)) {
return;
}
serializer.startTag(null /* ns */, "updated");
serializer.text(updateDate);
serializer.endTag(null /* ns */, "updated");
}
@Override
protected void serializeExtraEntryContents(XmlSerializer serializer,
int format) throws IOException {
Map<String, String> attrs = entry.getAllAttributes();
for (Map.Entry<String, String> attr : attrs.entrySet()) {
serializer.startTag("http://schemas.google.com/g/2005", "customProperty");
serializer.attribute(null, "name", attr.getKey());
serializer.text(attr.getValue());
serializer.endTag("http://schemas.google.com/g/2005", "customProperty");
}
String privacy = entry.getPrivacy();
if (!StringUtils.isEmpty(privacy)) {
serializer.setPrefix("app", APP_NAMESPACE);
if ("public".equals(privacy)) {
serializer.startTag(APP_NAMESPACE, "control");
serializer.startTag(APP_NAMESPACE, "draft");
serializer.text("no");
serializer.endTag(APP_NAMESPACE, "draft");
serializer.endTag(APP_NAMESPACE, "control");
}
if ("unlisted".equals(privacy)) {
serializer.startTag(APP_NAMESPACE, "control");
serializer.startTag(APP_NAMESPACE, "draft");
serializer.text("yes");
serializer.endTag(APP_NAMESPACE, "draft");
serializer.endTag(APP_NAMESPACE, "control");
}
}
}
}
@@ -0,0 +1,71 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.maps;
import com.google.android.apps.mytracks.io.fusiontables.SendFusionTablesActivity;
import com.google.android.apps.mytracks.io.sendtogoogle.AbstractSendActivity;
import com.google.android.apps.mytracks.io.sendtogoogle.AbstractSendAsyncTask;
import com.google.android.apps.mytracks.io.sendtogoogle.SendRequest;
import com.google.android.apps.mytracks.io.sendtogoogle.UploadResultActivity;
import com.google.android.apps.mytracks.io.spreadsheets.SendSpreadsheetsActivity;
import com.google.android.apps.mytracks.util.IntentUtils;
import com.google.android.maps.mytracks.R;
import com.google.common.annotations.VisibleForTesting;
import android.content.Intent;
/**
* An activity to send a track to Google Maps.
*
* @author Jimmy Shih
*/
public class SendMapsActivity extends AbstractSendActivity {
@Override
protected AbstractSendAsyncTask createAsyncTask() {
return new SendMapsAsyncTask(this, sendRequest.getTrackId(), sendRequest.getAccount());
}
@Override
protected String getServiceName() {
return getString(R.string.export_google_maps);
}
@Override
protected void startNextActivity(boolean success, boolean isCancel) {
sendRequest.setMapsSuccess(success);
Class<?> next = getNextClass(sendRequest, isCancel);
Intent intent = IntentUtils.newIntent(this, next)
.putExtra(SendRequest.SEND_REQUEST_KEY, sendRequest);
startActivity(intent);
finish();
}
@VisibleForTesting
Class<?> getNextClass(SendRequest request, boolean isCancel) {
if (isCancel) {
return UploadResultActivity.class;
} else {
if (request.isSendFusionTables()) {
return SendFusionTablesActivity.class;
} else if (request.isSendSpreadsheets()) {
return SendSpreadsheetsActivity.class;
} else {
return UploadResultActivity.class;
}
}
}
}
@@ -0,0 +1,505 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.maps;
import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.content.DescriptionGenerator;
import com.google.android.apps.mytracks.content.DescriptionGeneratorImpl;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils.LocationIterator;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.io.gdata.GDataClientFactory;
import com.google.android.apps.mytracks.io.gdata.maps.MapsClient;
import com.google.android.apps.mytracks.io.gdata.maps.MapsConstants;
import com.google.android.apps.mytracks.io.gdata.maps.MapsGDataConverter;
import com.google.android.apps.mytracks.io.gdata.maps.XmlMapsGDataParserFactory;
import com.google.android.apps.mytracks.io.sendtogoogle.AbstractSendAsyncTask;
import com.google.android.apps.mytracks.io.sendtogoogle.SendToGoogleUtils;
import com.google.android.apps.mytracks.stats.TripStatisticsUpdater;
import com.google.android.apps.mytracks.util.CalorieUtils.ActivityType;
import com.google.android.apps.mytracks.util.LocationUtils;
import com.google.android.apps.mytracks.util.PreferencesUtils;
import com.google.android.common.gdata.AndroidXmlParserFactory;
import com.google.android.maps.mytracks.R;
import com.google.common.annotations.VisibleForTesting;
import com.google.wireless.gdata.client.GDataClient;
import com.google.wireless.gdata.client.HttpException;
import com.google.wireless.gdata.parser.ParseException;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.AuthenticatorException;
import android.accounts.OperationCanceledException;
import android.content.Context;
import android.database.Cursor;
import android.location.Location;
import android.util.Log;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Vector;
import org.xmlpull.v1.XmlPullParserException;
/**
* AsyncTask to send a track to Google Maps.
* <p>
* IMPORTANT: While this code is Apache-licensed, please notice that usage of
* the Google Maps servers through this API is only allowed for the My Tracks
* application. Other applications looking to upload maps data should look into
* using the Google Fusion Tables API.
*
* @author Jimmy Shih
*/
public class SendMapsAsyncTask extends AbstractSendAsyncTask {
private static final String
START_ICON_URL = "http://maps.google.com/mapfiles/ms/micons/green-dot.png";
private static final String
END_ICON_URL = "http://maps.google.com/mapfiles/ms/micons/red-dot.png";
private static final int MAX_POINTS_PER_UPLOAD = 500;
private static final int PROGRESS_FETCH_MAP_ID = 5;
@VisibleForTesting
static final int PROGRESS_UPLOAD_DATA_MIN = 10;
@VisibleForTesting
static final int PROGRESS_UPLOAD_DATA_MAX = 90;
private static final int PROGRESS_UPLOAD_WAYPOINTS = 95;
private static final int PROGRESS_COMPLETE = 100;
private static final String TAG = SendMapsAsyncTask.class.getSimpleName();
private final long trackId;
private final Account account;
private final MyTracksProviderUtils myTracksProviderUtils;
private final Context context;
private final GDataClient gDataClient;
private final MapsClient mapsClient;
// The following variables are for per upload states
private MapsGDataConverter mapsGDataConverter;
private String authToken;
private String mapId;
int currentSegment;
public SendMapsAsyncTask(SendMapsActivity activity, long trackId, Account account) {
this(activity, trackId, account, MyTracksProviderUtils.Factory.get(
activity.getApplicationContext()));
}
/**
* This constructor is created for test.
*/
@VisibleForTesting
public SendMapsAsyncTask(SendMapsActivity activity, long trackId, Account account,
MyTracksProviderUtils myTracksProviderUtils) {
super(activity);
this.trackId = trackId;
this.account = account;
this.myTracksProviderUtils = myTracksProviderUtils;
context = activity.getApplicationContext();
gDataClient = GDataClientFactory.getGDataClient(context);
mapsClient = new MapsClient(
gDataClient, new XmlMapsGDataParserFactory(new AndroidXmlParserFactory()));
}
@Override
protected void closeConnection() {
if (gDataClient != null) {
gDataClient.close();
}
}
@Override
protected boolean performTask() {
// Reset the per upload states
mapsGDataConverter = null;
authToken = null;
mapId = null;
currentSegment = 1;
// Create a maps gdata converter
try {
mapsGDataConverter = new MapsGDataConverter();
} catch (XmlPullParserException e) {
Log.d(TAG, "Unable to create a maps gdata converter", e);
return false;
}
// Get auth token
try {
authToken = AccountManager.get(context)
.blockingGetAuthToken(account, MapsConstants.SERVICE_NAME, false);
} catch (OperationCanceledException e) {
Log.d(TAG, "Unable to get auth token", e);
return retryTask();
} catch (AuthenticatorException e) {
Log.d(TAG, "Unable to get auth token", e);
return retryTask();
} catch (IOException e) {
Log.d(TAG, "Unable to get auth token", e);
return retryTask();
}
// Get the track
Track track = myTracksProviderUtils.getTrack(trackId);
if (track == null) {
Log.d(TAG, "No track for " + trackId);
return false;
}
// Fetch the mapId, create a new map if necessary
publishProgress(PROGRESS_FETCH_MAP_ID);
if (!fetchSendMapId(track)) {
Log.d("TAG", "Unable to upload all track points");
return retryTask();
}
// Upload all the track points plus the start and end markers
publishProgress(PROGRESS_UPLOAD_DATA_MIN);
if (!uploadAllTrackPoints(track)) {
Log.d("TAG", "Unable to upload all track points");
return retryTask();
}
// Upload all the waypoints
publishProgress(PROGRESS_UPLOAD_WAYPOINTS);
if (!uploadWaypoints()) {
Log.d("TAG", "Unable to upload waypoints");
return false;
}
publishProgress(PROGRESS_COMPLETE);
return true;
}
@Override
protected void invalidateToken() {
AccountManager.get(context).invalidateAuthToken(Constants.ACCOUNT_TYPE, authToken);
}
/**
* Fetches the {@link SendMapsAsyncTask#mapId} instance variable for sending a
* track to Google Maps.
*
* @param track the Track
* @return true if able to fetch the mapId variable.
*/
@VisibleForTesting
boolean fetchSendMapId(Track track) {
if (isCancelled()) {
return false;
}
boolean defaultMapPublic = PreferencesUtils.getBoolean(context,
R.string.export_google_maps_public_key, PreferencesUtils.EXPORT_GOOGLE_MAPS_PUBLIC_DEFAULT);
try {
String description = track.getCategory() + "\n" + track.getDescription() + "\n"
+ context.getString(R.string.send_google_by_my_tracks, "", "");
mapId = SendMapsUtils.createNewMap(
track.getName(), description, defaultMapPublic, mapsClient, authToken);
shareUrl = MapsClient.buildMapUrl(mapId);
} catch (ParseException e) {
Log.d(TAG, "Unable to create a new map", e);
return false;
} catch (HttpException e) {
Log.d(TAG, "Unable to create a new map", e);
return false;
} catch (IOException e) {
Log.d(TAG, "Unable to create a new map", e);
return false;
}
return mapId != null;
}
/**
* Uploads all the points in a track.
*
* @param track the track
* @return true if success.
*/
@VisibleForTesting
boolean uploadAllTrackPoints(Track track) {
int numberOfPoints = track.getNumberOfPoints();
List<Location> locations = new ArrayList<Location>(MAX_POINTS_PER_UPLOAD);
Location lastValidLocation = null;
boolean sentStartMarker = false;
// For chart server, limit the number of elevation readings to 250.
int elevationSamplingFrequency = Math.max(1, (int) (numberOfPoints / 250.0));
Vector<Double> distances = new Vector<Double>();
Vector<Double> elevations = new Vector<Double>();
TripStatisticsUpdater tripStatisticsUpdater = new TripStatisticsUpdater(
track.getTripStatistics().getStartTime());
int recordingDistanceInterval = PreferencesUtils.getInt(context,
R.string.recording_distance_interval_key,
PreferencesUtils.RECORDING_DISTANCE_INTERVAL_DEFAULT);
int readCount = 0;
LocationIterator locationIterator = null;
try {
locationIterator = myTracksProviderUtils.getTrackPointLocationIterator(
trackId, -1L, false, MyTracksProviderUtils.DEFAULT_LOCATION_FACTORY);
while (locationIterator.hasNext()) {
Location location = locationIterator.next();
locations.add(location);
if (LocationUtils.isValidLocation(location)) {
lastValidLocation = location;
}
if (!sentStartMarker && lastValidLocation != null) {
// Create a start marker
if (!uploadMarker(context.getString(R.string.marker_label_start, track.getName()), "",
START_ICON_URL, lastValidLocation)) {
Log.d(TAG, "Unable to create a start marker");
return false;
}
sentStartMarker = true;
}
tripStatisticsUpdater.addLocation(
location, recordingDistanceInterval, false, ActivityType.INVALID, 0.0);
if (readCount % elevationSamplingFrequency == 0) {
distances.add(tripStatisticsUpdater.getTripStatistics().getTotalDistance());
elevations.add(tripStatisticsUpdater.getSmoothedElevation());
}
// Upload periodically
readCount++;
if (readCount % MAX_POINTS_PER_UPLOAD == 0) {
if (!prepareAndUploadPoints(track, locations, false)) {
Log.d(TAG, "Unable to upload points");
return false;
}
updateProgress(readCount, numberOfPoints);
locations.clear();
}
}
// Do a final upload with the remaining locations
if (!prepareAndUploadPoints(track, locations, true)) {
Log.d(TAG, "Unable to upload points");
return false;
}
// Create an end marker
if (lastValidLocation != null) {
distances.add(tripStatisticsUpdater.getTripStatistics().getTotalDistance());
elevations.add(tripStatisticsUpdater.getSmoothedElevation());
DescriptionGenerator descriptionGenerator = new DescriptionGeneratorImpl(context);
track.setDescription(
descriptionGenerator.generateTrackDescription(track, distances, elevations, true));
if (!uploadMarker(context.getString(R.string.marker_label_end, track.getName()),
track.getDescription(), END_ICON_URL, lastValidLocation)) {
Log.d(TAG, "Unable to create an end marker");
return false;
}
}
return true;
} finally {
if (locationIterator != null) {
locationIterator.close();
}
}
}
/**
* Prepares and uploads a list of locations from a track.
*
* @param track the track
* @param locations the locations from the track
* @param lastBatch true if it is the last batch of locations
* @return true if success.
*/
@VisibleForTesting
boolean prepareAndUploadPoints(Track track, List<Location> locations, boolean lastBatch) {
// Prepare locations
ArrayList<Track> splitTracks = SendToGoogleUtils.prepareLocations(track, locations);
// Upload segments
boolean onlyOneSegment = lastBatch && currentSegment == 1 && splitTracks.size() == 1;
for (Track segment : splitTracks) {
if (!onlyOneSegment) {
segment.setName(context.getString(
R.string.send_google_track_part_label, segment.getName(), currentSegment));
}
if (!uploadSegment(segment.getName(), segment.getLocations())) {
Log.d(TAG, "Unable to upload segment");
return false;
}
currentSegment++;
}
return true;
}
/**
* Uploads a marker.
*
* @param title marker title
* @param description marker description
* @param iconUrl marker marker icon
* @param location marker location
* @return true if success.
*/
@VisibleForTesting
boolean uploadMarker(String title, String description, String iconUrl, Location location) {
if (isCancelled()) {
return false;
}
try {
if (!SendMapsUtils.uploadMarker(mapId, title, description, iconUrl, location, mapsClient,
authToken, mapsGDataConverter)) {
Log.d(TAG, "Unable to upload marker");
return false;
}
} catch (ParseException e) {
Log.d(TAG, "Unable to upload marker", e);
return false;
} catch (HttpException e) {
Log.d(TAG, "Unable to upload marker", e);
return false;
} catch (IOException e) {
Log.d(TAG, "Unable to upload marker", e);
return false;
}
return true;
}
/**
* Uploads a segment
*
* @param title segment title
* @param locations segment locations
* @return true if success
*/
private boolean uploadSegment(String title, ArrayList<Location> locations) {
if (isCancelled()) {
return false;
}
try {
if (!SendMapsUtils.uploadSegment(
mapId, title, locations, mapsClient, authToken, mapsGDataConverter)) {
Log.d(TAG, "Unable to upload track points");
return false;
}
} catch (ParseException e) {
Log.d(TAG, "Unable to upload track points", e);
return false;
} catch (HttpException e) {
Log.d(TAG, "Unable to upload track points", e);
return false;
} catch (IOException e) {
Log.d(TAG, "Unable to upload track points", e);
return false;
}
return true;
}
/**
* Uploads all the waypoints.
*
* @return true if success.
*/
@VisibleForTesting
boolean uploadWaypoints() {
Cursor cursor = null;
try {
cursor = myTracksProviderUtils.getWaypointCursor(
trackId, -1L, Constants.MAX_LOADED_WAYPOINTS_POINTS);
if (cursor != null && cursor.moveToFirst()) {
// This will skip the first waypoint (it carries the stats for the
// track).
while (cursor.moveToNext()) {
if (isCancelled()) {
return false;
}
Waypoint waypoint = myTracksProviderUtils.createWaypoint(cursor);
try {
if (!SendMapsUtils.uploadWaypoint(
mapId, waypoint, mapsClient, authToken, mapsGDataConverter)) {
Log.d(TAG, "Unable to upload waypoint");
return false;
}
} catch (ParseException e) {
Log.d(TAG, "Unable to upload waypoint", e);
return false;
} catch (HttpException e) {
Log.d(TAG, "Unable to upload waypoint", e);
return false;
} catch (IOException e) {
Log.d(TAG, "Unable to upload waypoint", e);
return false;
}
}
}
return true;
} finally {
if (cursor != null) {
cursor.close();
}
}
}
/**
* Updates the progress based on the number of locations uploaded.
*
* @param uploaded the number of uploaded locations
* @param total the number of total locations
*/
@VisibleForTesting
void updateProgress(int uploaded, int total) {
publishProgress(getPercentage(uploaded, total));
}
/**
* Count the percentage of the number of locations uploaded.
*
* @param uploaded the number of uploaded locations
* @param total the number of total locations
*/
@VisibleForTesting
static int getPercentage(int uploaded, int total) {
double totalPercentage = (double) uploaded / total;
double scaledPercentage = totalPercentage
* (PROGRESS_UPLOAD_DATA_MAX - PROGRESS_UPLOAD_DATA_MIN) + PROGRESS_UPLOAD_DATA_MIN;
return (int) scaledPercentage;
}
/**
* Gets the mapID.
*
* @return mapId
*/
@VisibleForTesting
String getMapId() {
return mapId;
}
/**
* Sets the value of mapsGDataConverter.
*
* @param mapsGDataConverter new value of mapsGDataConverter
*/
@VisibleForTesting
void setMapsGDataConverter(MapsGDataConverter mapsGDataConverter) {
this.mapsGDataConverter = mapsGDataConverter;
}
}
@@ -0,0 +1,199 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.maps;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.io.gdata.maps.MapsClient;
import com.google.android.apps.mytracks.io.gdata.maps.MapsFeature;
import com.google.android.apps.mytracks.io.gdata.maps.MapsGDataConverter;
import com.google.android.apps.mytracks.io.gdata.maps.MapsMapMetadata;
import com.google.common.annotations.VisibleForTesting;
import com.google.wireless.gdata.client.HttpException;
import com.google.wireless.gdata.data.Entry;
import com.google.wireless.gdata.parser.ParseException;
import android.location.Location;
import android.text.TextUtils;
import android.util.Log;
import java.io.IOException;
import java.util.ArrayList;
/**
* Utilities for sending a track to Google Maps.
*
* @author Jimmy Shih
*/
public class SendMapsUtils {
private static final String EMPTY_TITLE = "-";
private static final int LINE_COLOR = 0x80FF0000;
private static final String TAG = SendMapsUtils.class.getSimpleName();
private SendMapsUtils() {}
/**
* Creates a new Google Map.
*
* @param title title of the map
* @param description description of the map
* @param isPublic true if the map can be public
* @param mapsClient the maps client
* @param authToken the auth token
* @return map id of the created map if successful.
*/
public static String createNewMap(
String title, String description, boolean isPublic, MapsClient mapsClient, String authToken)
throws ParseException, HttpException, IOException {
String mapFeed = MapsClient.getMapsFeed();
MapsMapMetadata metaData = new MapsMapMetadata();
metaData.setTitle(title);
metaData.setDescription(description);
metaData.setSearchable(isPublic);
Entry entry = MapsGDataConverter.getMapEntryForMetadata(metaData);
Entry result = mapsClient.createEntry(mapFeed, authToken, entry);
if (result == null) {
Log.d(TAG, "No result when creating a new map");
return null;
}
return MapsClient.getMapIdFromMapEntryId(result.getId());
}
/**
* Uploads a start/end marker to Google Maps.
*
* @param mapId the map id
* @param title the marker title
* @param description the marker description
* @param iconUrl the marker icon URL
* @param location the marker location
* @param mapsClient the maps client
* @param authToken the auth token
* @param mapsGDataConverter the maps gdata converter
* @return true if success.
*/
public static boolean uploadMarker(String mapId, String title, String description, String iconUrl,
Location location, MapsClient mapsClient, String authToken,
MapsGDataConverter mapsGDataConverter) throws ParseException, HttpException, IOException {
String featuresFeed = MapsClient.getFeaturesFeed(mapId);
MapsFeature mapsFeature = buildMapsMarkerFeature(
title, description, iconUrl, location);
Entry entry = mapsGDataConverter.getEntryForFeature(mapsFeature);
try {
mapsClient.createEntry(featuresFeed, authToken, entry);
} catch (IOException e) {
// Retry once (often IOException is thrown on a timeout)
Log.d(TAG, "Retry upload marker", e);
mapsClient.createEntry(featuresFeed, authToken, entry);
}
return true;
}
/**
* Uploads a waypoint as a marker feature to Google Maps.
*
* @param mapId the map id
* @param waypoint the waypoint
* @param mapsClient the maps client
* @param authToken the auth token
* @param mapsGDataConverter the maps gdata converter
* @return true if success.
*/
public static boolean uploadWaypoint(String mapId, Waypoint waypoint, MapsClient mapsClient,
String authToken, MapsGDataConverter mapsGDataConverter)
throws ParseException, HttpException, IOException {
String featuresFeed = MapsClient.getFeaturesFeed(mapId);
MapsFeature feature = buildMapsMarkerFeature(waypoint.getName(), waypoint.getDescription(),
waypoint.getIcon(), waypoint.getLocation());
Entry entry = mapsGDataConverter.getEntryForFeature(feature);
try {
mapsClient.createEntry(featuresFeed, authToken, entry);
} catch (IOException e) {
// Retry once (often IOException is thrown on a timeout)
Log.d(TAG, "Retry upload waypoint", e);
mapsClient.createEntry(featuresFeed, authToken, entry);
}
return true;
}
/**
* Uploads a segment as a line feature to Google Maps.
*
* @param mapId the map id
* @param title the segment title
* @param locations the segment locations
* @param mapsClient the maps client
* @param authToken the auth token
* @param mapsGDataConverter the maps gdata converter
* @return true if success.
*/
public static boolean uploadSegment(String mapId, String title, ArrayList<Location> locations,
MapsClient mapsClient, String authToken, MapsGDataConverter mapsGDataConverter)
throws ParseException, HttpException, IOException {
String featuresFeed = MapsClient.getFeaturesFeed(mapId);
Entry entry = mapsGDataConverter.getEntryForFeature(buildMapsLineFeature(title, locations));
try {
mapsClient.createEntry(featuresFeed, authToken, entry);
} catch (IOException e) {
// Retry once (often IOException is thrown on a timeout)
Log.d(TAG, "Retry upload track points", e);
mapsClient.createEntry(featuresFeed, authToken, entry);
}
return true;
}
/**
* Builds a map marker feature.
*
* @param title feature title
* @param description the feature description
* @param iconUrl the feature icon URL
* @param location the marker
*/
@VisibleForTesting
static MapsFeature buildMapsMarkerFeature(
String title, String description, String iconUrl, Location location) {
MapsFeature mapsFeature = new MapsFeature();
mapsFeature.setType(MapsFeature.MARKER);
mapsFeature.generateAndroidId();
// Feature must have a name (otherwise GData upload may fail)
mapsFeature.setTitle(TextUtils.isEmpty(title) ? EMPTY_TITLE : title);
mapsFeature.setDescription(description.replaceAll("\n", "<br>"));
mapsFeature.setIconUrl(iconUrl);
mapsFeature.addPoint(location);
return mapsFeature;
}
/**
* Builds a maps line feature from a set of locations.
*
* @param title the feature title
* @param locations set of locations
*/
@VisibleForTesting
static MapsFeature buildMapsLineFeature(String title, ArrayList<Location> locations) {
MapsFeature mapsFeature = new MapsFeature();
mapsFeature.setType(MapsFeature.LINE);
mapsFeature.generateAndroidId();
// Feature must have a name (otherwise GData upload may fail)
mapsFeature.setTitle(TextUtils.isEmpty(title) ? EMPTY_TITLE : title);
mapsFeature.setColor(LINE_COLOR);
for (Location location : locations) {
mapsFeature.addPoint(location);
}
return mapsFeature;
}
}

Some files were not shown because too many files have changed in this diff Show More