forked from upstream-mirrors/OpenTracks
In MapFragment, use MyTracksLocationManager directly to obtain the current location.
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* 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 android.os.Bundle;
|
||||
import android.speech.tts.TextToSpeech;
|
||||
import android.support.v4.app.FragmentActivity;
|
||||
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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the layout resource id.
|
||||
*/
|
||||
protected abstract int getLayoutResId();
|
||||
|
||||
/**
|
||||
* Returns true to hide the title. Be default, do not hide the title.
|
||||
*/
|
||||
protected boolean hideTitle() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true to configure the action bar home button as the up button.
|
||||
*/
|
||||
protected boolean configureActionBarHomeAsUp() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback when the home menu item is selected.
|
||||
*/
|
||||
protected void onHomeSelected() {
|
||||
finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (item.getItemId() == android.R.id.home) {
|
||||
onHomeSelected();
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,439 @@
|
||||
/*
|
||||
* 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.AddEmailsDialogFragment;
|
||||
import com.google.android.apps.mytracks.fragments.AddEmailsDialogFragment.AddEmailsCaller;
|
||||
import com.google.android.apps.mytracks.fragments.CheckPermissionFragment;
|
||||
import com.google.android.apps.mytracks.fragments.CheckPermissionFragment.CheckPermissionCaller;
|
||||
import com.google.android.apps.mytracks.fragments.ChooseAccountDialogFragment;
|
||||
import com.google.android.apps.mytracks.fragments.ChooseAccountDialogFragment.ChooseAccountCaller;
|
||||
import com.google.android.apps.mytracks.fragments.ChooseActivityDialogFragment;
|
||||
import com.google.android.apps.mytracks.fragments.ChooseActivityDialogFragment.ChooseActivityCaller;
|
||||
import com.google.android.apps.mytracks.fragments.ConfirmDialogFragment;
|
||||
import com.google.android.apps.mytracks.fragments.ConfirmDialogFragment.ConfirmCaller;
|
||||
import com.google.android.apps.mytracks.fragments.InstallEarthDialogFragment;
|
||||
import com.google.android.apps.mytracks.io.drive.SendDriveActivity;
|
||||
import com.google.android.apps.mytracks.io.file.SaveActivity;
|
||||
import com.google.android.apps.mytracks.io.file.TrackFileFormat;
|
||||
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.ChooseMapActivity;
|
||||
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.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.StringUtils;
|
||||
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.ContentResolver;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcelable;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* An abstract class for sending a track to Google services.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public abstract class AbstractSendToGoogleActivity extends AbstractMyTracksActivity implements
|
||||
ChooseAccountCaller, CheckPermissionCaller, AddEmailsCaller, ChooseActivityCaller,
|
||||
ConfirmCaller {
|
||||
|
||||
private static final String TAG = AbstractMyTracksActivity.class.getSimpleName();
|
||||
private static final String SEND_REQUEST_KEY = "send_request_key";
|
||||
|
||||
private SendRequest sendRequest;
|
||||
|
||||
public void sendToGoogle(SendRequest request) {
|
||||
sendRequest = request;
|
||||
new ChooseAccountDialogFragment().show(
|
||||
getSupportFragmentManager(), ChooseAccountDialogFragment.CHOOSE_ACCOUNT_DIALOG_TAG);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
if (savedInstanceState != null) {
|
||||
sendRequest = savedInstanceState.getParcelable(SEND_REQUEST_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
@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 SendToGoogleUtils.DRIVE_PERMISSION_REQUEST_CODE:
|
||||
SendToGoogleUtils.cancelNotification(this, SendToGoogleUtils.DRIVE_NOTIFICATION_ID);
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
onDrivePermissionSuccess();
|
||||
} else {
|
||||
onPermissionFailure();
|
||||
}
|
||||
break;
|
||||
case SendToGoogleUtils.FUSION_TABLES_PERMISSION_REQUEST_CODE:
|
||||
SendToGoogleUtils.cancelNotification(this, SendToGoogleUtils.FUSION_TABLES_NOTIFICATION_ID);
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
onFusionTablesSuccess();
|
||||
} else {
|
||||
onPermissionFailure();
|
||||
}
|
||||
break;
|
||||
case SendToGoogleUtils.SPREADSHEET_PERMISSION_REQUEST_CODE:
|
||||
SendToGoogleUtils.cancelNotification(this, SendToGoogleUtils.SPREADSHEET_NOTIFICATION_ID);
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
onSpreadsheetsPermissionSuccess();
|
||||
} else {
|
||||
onPermissionFailure();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
}
|
||||
}
|
||||
|
||||
public void onChooseAccountDone() {
|
||||
String googleAccount = PreferencesUtils.getString(
|
||||
this, R.string.google_account_key, PreferencesUtils.GOOGLE_ACCOUNT_DEFAULT);
|
||||
if (googleAccount == null || googleAccount.equals(PreferencesUtils.GOOGLE_ACCOUNT_DEFAULT)) {
|
||||
return;
|
||||
}
|
||||
sendRequest.setAccount(new Account(googleAccount, Constants.ACCOUNT_TYPE));
|
||||
|
||||
// Check Drive permission
|
||||
boolean needDrivePermission = sendRequest.isSendDrive();
|
||||
if (!needDrivePermission) {
|
||||
boolean defaultTablePublic = PreferencesUtils.getBoolean(
|
||||
this, R.string.default_table_public_key, PreferencesUtils.DEFAULT_TABLE_PUBLIC_DEFAULT);
|
||||
needDrivePermission = defaultTablePublic && sendRequest.isSendFusionTables();
|
||||
}
|
||||
if (!needDrivePermission) {
|
||||
needDrivePermission = sendRequest.isSendSpreadsheets();
|
||||
}
|
||||
|
||||
if (needDrivePermission) {
|
||||
Fragment fragment = CheckPermissionFragment.newInstance(
|
||||
sendRequest.getAccount().name, SendToGoogleUtils.DRIVE_SCOPE);
|
||||
getSupportFragmentManager()
|
||||
.beginTransaction().add(fragment, CheckPermissionFragment.CHECK_PERMISSION_TAG).commit();
|
||||
} else {
|
||||
onDrivePermissionSuccess();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCheckPermissionDone(String scope, boolean success, Intent intent) {
|
||||
if (success) {
|
||||
if (scope.equals(SendToGoogleUtils.DRIVE_SCOPE)) {
|
||||
onDrivePermissionSuccess();
|
||||
} else if (scope.equals(SendToGoogleUtils.FUSION_TABLES_SCOPE)) {
|
||||
onFusionTablesSuccess();
|
||||
} else {
|
||||
onSpreadsheetsPermissionSuccess();
|
||||
}
|
||||
} else {
|
||||
if (intent != null) {
|
||||
int requestCode;
|
||||
if (scope.equals(SendToGoogleUtils.DRIVE_SCOPE)) {
|
||||
requestCode = SendToGoogleUtils.DRIVE_PERMISSION_REQUEST_CODE;
|
||||
} else if (scope.equals(SendToGoogleUtils.FUSION_TABLES_SCOPE)) {
|
||||
requestCode = SendToGoogleUtils.FUSION_TABLES_PERMISSION_REQUEST_CODE;
|
||||
} else {
|
||||
requestCode = SendToGoogleUtils.SPREADSHEET_PERMISSION_REQUEST_CODE;
|
||||
}
|
||||
startActivityForResult(intent, requestCode);
|
||||
} else {
|
||||
onPermissionFailure();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAddEmailsDone(String emails) {
|
||||
if (emails != null && !emails.equals("")) {
|
||||
sendRequest.setDriveShareEmails(emails);
|
||||
Intent intent = IntentUtils.newIntent(this, SendDriveActivity.class)
|
||||
.putExtra(SendRequest.SEND_REQUEST_KEY, sendRequest);
|
||||
startActivity(intent);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChooseActivityDone(String packageName, String className) {
|
||||
if (packageName != null && className != null) {
|
||||
sendRequest.setMapsSharePackageName(packageName);
|
||||
sendRequest.setMapsShareClassName(className);
|
||||
Intent intent = IntentUtils.newIntent(
|
||||
this, sendRequest.isMapsExistingMap() ? ChooseMapActivity.class : SendMapsActivity.class)
|
||||
.putExtra(SendRequest.SEND_REQUEST_KEY, sendRequest);
|
||||
startActivity(intent);
|
||||
}
|
||||
}
|
||||
|
||||
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()) {
|
||||
Fragment fragment = CheckPermissionFragment.newInstance(
|
||||
sendRequest.getAccount().name, SendToGoogleUtils.FUSION_TABLES_SCOPE);
|
||||
getSupportFragmentManager()
|
||||
.beginTransaction().add(fragment, CheckPermissionFragment.CHECK_PERMISSION_TAG).commit();
|
||||
} else {
|
||||
onFusionTablesSuccess();
|
||||
}
|
||||
}
|
||||
|
||||
private void onFusionTablesSuccess() {
|
||||
// Check Spreadsheets permission
|
||||
if (sendRequest.isSendSpreadsheets()) {
|
||||
Fragment fragment = CheckPermissionFragment.newInstance(
|
||||
sendRequest.getAccount().name, SendToGoogleUtils.SPREADSHEET_SCOPE);
|
||||
getSupportFragmentManager()
|
||||
.beginTransaction().add(fragment, CheckPermissionFragment.CHECK_PERMISSION_TAG).commit();
|
||||
} else {
|
||||
onSpreadsheetsPermissionSuccess();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* On spreadsheets permission success. If
|
||||
* <p>
|
||||
* isSendDrive and isDriveEnableSync -> enable sync
|
||||
* <p>
|
||||
* isSendDrive and isDriveShare -> show {@link AddEmailsDialogFragment}
|
||||
* <p>
|
||||
* isSendDrive -> start {@link SendDriveActivity}
|
||||
* <p>
|
||||
* isSendMaps and isMapShare -> show {@link ChooseActivityDialogFragment}
|
||||
* <p>
|
||||
* isSendMaps and isMapsExistingMap -> start {@link ChooseMapActivity}
|
||||
* <p>
|
||||
* isSendMaps and !isMapsExistingMap -> {@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.isDriveEnableSync()) {
|
||||
PreferencesUtils.setBoolean(this, R.string.drive_sync_key, true);
|
||||
|
||||
// Turn off everything
|
||||
SyncUtils.disableSync(this);
|
||||
|
||||
// Turn on sync
|
||||
ContentResolver.setMasterSyncAutomatically(true);
|
||||
|
||||
// Enable sync for account
|
||||
SyncUtils.enableSync(sendRequest.getAccount());
|
||||
return;
|
||||
} else if (sendRequest.isDriveShare()) {
|
||||
AddEmailsDialogFragment.newInstance(sendRequest.getTrackId())
|
||||
.show(getSupportFragmentManager(), AddEmailsDialogFragment.ADD_EMAILS_DIALOG_TAG);
|
||||
return;
|
||||
} else {
|
||||
next = SendDriveActivity.class;
|
||||
}
|
||||
} else if (sendRequest.isSendMaps()) {
|
||||
if (sendRequest.isMapsShare()) {
|
||||
new ChooseActivityDialogFragment().show(
|
||||
getSupportFragmentManager(), ChooseActivityDialogFragment.CHOOSE_ACTIVITY_DIALOG_TAG);
|
||||
return;
|
||||
}
|
||||
next = sendRequest.isMapsExistingMap() ? ChooseMapActivity.class : 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirm playing tracks in Google Earth.
|
||||
*
|
||||
* @param trackIds the track ids
|
||||
*/
|
||||
protected void confirmPlay(long[] trackIds) {
|
||||
if (GoogleEarthUtils.isEarthInstalled(this)) {
|
||||
ConfirmDialogFragment.newInstance(R.string.confirm_play_earth_key,
|
||||
PreferencesUtils.CONFIRM_PLAY_EARTH_DEFAULT,
|
||||
getString(R.string.track_detail_play_confirm_message), trackIds)
|
||||
.show(getSupportFragmentManager(), ConfirmDialogFragment.CONFIRM_DIALOG_TAG);
|
||||
} else {
|
||||
new InstallEarthDialogFragment().show(
|
||||
getSupportFragmentManager(), InstallEarthDialogFragment.INSTALL_EARTH_DIALOG_TAG);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirm sharing a track.
|
||||
*
|
||||
* @param trackId the track id
|
||||
*/
|
||||
protected void confirmShare(long trackId) {
|
||||
String shareTrack = PreferencesUtils.getString(
|
||||
this, R.string.share_track_key, PreferencesUtils.SHARE_TRACK_DEFAULT);
|
||||
String driveValue = getString(R.string.settings_sharing_share_track_drive_value);
|
||||
String mapsValue = getString(R.string.settings_sharing_share_track_maps_value);
|
||||
if (shareTrack.equals(driveValue)) {
|
||||
ConfirmDialogFragment.newInstance(R.string.confirm_share_drive_key,
|
||||
PreferencesUtils.CONFIRM_SHARE_DRIVE_DEFAULT,
|
||||
getString(R.string.share_track_drive_confirm_message), new long[] { trackId })
|
||||
.show(getSupportFragmentManager(), ConfirmDialogFragment.CONFIRM_DIALOG_TAG);
|
||||
} else if (shareTrack.equals(mapsValue)) {
|
||||
ConfirmDialogFragment.newInstance(R.string.confirm_share_maps_key,
|
||||
PreferencesUtils.CONFIRM_SHARE_MAPS_DEFAULT, StringUtils.getHtml(
|
||||
this, R.string.share_track_maps_confirm_message, R.string.maps_public_unlisted_url),
|
||||
new long[] { trackId })
|
||||
.show(getSupportFragmentManager(), ConfirmDialogFragment.CONFIRM_DIALOG_TAG);
|
||||
} else {
|
||||
ConfirmDialogFragment.newInstance(R.string.confirm_share_file_key,
|
||||
PreferencesUtils.CONFIRM_SHARE_FILE_DEFAULT,
|
||||
getString(R.string.share_track_file_confirm_message), new long[] { trackId })
|
||||
.show(getSupportFragmentManager(), ConfirmDialogFragment.CONFIRM_DIALOG_TAG);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfirmDone(int confirmId, long[] trackIds) {
|
||||
Intent intent;
|
||||
SendRequest newRequest;
|
||||
|
||||
switch (confirmId) {
|
||||
case R.string.confirm_play_earth_key:
|
||||
AnalyticsUtils.sendPageViews(this, "/action/play");
|
||||
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);
|
||||
break;
|
||||
case R.string.confirm_share_drive_key:
|
||||
AnalyticsUtils.sendPageViews(this, "/action/share_drive");
|
||||
newRequest = new SendRequest(trackIds[0]);
|
||||
newRequest.setSendDrive(true);
|
||||
newRequest.setDriveShare(true);
|
||||
sendToGoogle(newRequest);
|
||||
break;
|
||||
case R.string.confirm_share_maps_key:
|
||||
AnalyticsUtils.sendPageViews(this, "/action/share_maps");
|
||||
newRequest = new SendRequest(trackIds[0]);
|
||||
newRequest.setSendMaps(true);
|
||||
newRequest.setMapsShare(true);
|
||||
sendToGoogle(newRequest);
|
||||
break;
|
||||
case R.string.confirm_share_file_key:
|
||||
String shareTrack = PreferencesUtils.getString(
|
||||
this, R.string.share_track_key, PreferencesUtils.SHARE_TRACK_DEFAULT);
|
||||
TrackFileFormat trackFileFormat;
|
||||
if (shareTrack.equals(TrackFileFormat.GPX.name())) {
|
||||
trackFileFormat = TrackFileFormat.GPX;
|
||||
} else if (shareTrack.equals(TrackFileFormat.KML.name())) {
|
||||
trackFileFormat = TrackFileFormat.KML;
|
||||
} else if (shareTrack.equals(TrackFileFormat.CSV.name())) {
|
||||
trackFileFormat = TrackFileFormat.CSV;
|
||||
} else {
|
||||
trackFileFormat = TrackFileFormat.TCX;
|
||||
}
|
||||
intent = IntentUtils.newIntent(this, SaveActivity.class)
|
||||
.putExtra(SaveActivity.EXTRA_TRACK_IDS, trackIds)
|
||||
.putExtra(SaveActivity.EXTRA_TRACK_FILE_FORMAT, (Parcelable) trackFileFormat)
|
||||
.putExtra(SaveActivity.EXTRA_SHARE_TRACK, true);
|
||||
startActivity(intent);
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* 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.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, getTripStatistics());
|
||||
StatsUtils.setLocationValues(this, 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,65 @@
|
||||
package com.google.android.apps.mytracks;
|
||||
|
||||
import com.google.android.apps.mytracks.util.PreferencesUtils;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.content.Context;
|
||||
import android.preference.EditTextPreference;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewParent;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.AutoCompleteTextView;
|
||||
|
||||
/**
|
||||
* The {@link AutoCompleteTextPreference} class is a preference that allows for
|
||||
* string input using auto complete . It is a subclass of
|
||||
* {@link EditTextPreference} and shows the {@link AutoCompleteTextView} in a
|
||||
* dialog.
|
||||
* <p>
|
||||
* This preference will store a string into the SharedPreferences.
|
||||
*
|
||||
* @author Rimas Trumpa (with Matt Levan)
|
||||
*/
|
||||
public class AutoCompleteTextPreference extends EditTextPreference {
|
||||
|
||||
private AutoCompleteTextView mEditText = null;
|
||||
|
||||
public AutoCompleteTextPreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
mEditText = new AutoCompleteTextView(context, attrs);
|
||||
mEditText.setThreshold(0);
|
||||
|
||||
// Gets autocomplete values for 'Default Activity' preference
|
||||
if (PreferencesUtils.getKey(context, R.string.default_activity_key).equals(getKey())) {
|
||||
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(
|
||||
context, R.array.activity_types, android.R.layout.simple_dropdown_item_1line);
|
||||
mEditText.setAdapter(adapter);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onBindDialogView(View view) {
|
||||
AutoCompleteTextView editText = mEditText;
|
||||
editText.setText(getText());
|
||||
|
||||
ViewParent oldParent = editText.getParent();
|
||||
if (oldParent != view) {
|
||||
if (oldParent != null) {
|
||||
((ViewGroup) oldParent).removeView(editText);
|
||||
}
|
||||
onAddEditTextToDialogView(view, editText);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDialogClosed(boolean positiveResult) {
|
||||
if (positiveResult) {
|
||||
String value = mEditText.getText().toString();
|
||||
if (callChangeListener(value)) {
|
||||
setText(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* 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 static com.google.android.apps.mytracks.Constants.TAG;
|
||||
|
||||
import com.google.android.apps.mytracks.services.RemoveTempFilesService;
|
||||
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 {
|
||||
|
||||
@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);
|
||||
|
||||
Intent removeTempFilesIntent = new Intent(context, RemoveTempFilesService.class);
|
||||
context.startService(removeTempFilesIntent);
|
||||
} 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,70 @@
|
||||
/*
|
||||
* 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 track points displayed by the map overlay. Set to 2X of
|
||||
* {@link Constants#TARGET_DISPLAYED_TRACK_POINTS}
|
||||
*/
|
||||
public static final int MAX_DISPLAYED_TRACK_POINTS = 10000;
|
||||
|
||||
/**
|
||||
* Maximum number of waypoints displayed by the map overlay.
|
||||
*/
|
||||
public static final int MAX_DISPLAYED_WAYPOINTS_POINTS = 128;
|
||||
|
||||
/**
|
||||
* Maximum number of track points that will be loaded at one time. With
|
||||
* recording frequency of 2 seconds, 20000 corresponds to 11.1 hours.
|
||||
*/
|
||||
public static final int MAX_LOADED_TRACK_POINTS = 20000;
|
||||
|
||||
/**
|
||||
* 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";
|
||||
|
||||
/**
|
||||
* The log tag.
|
||||
*/
|
||||
public static final String TAG = "MyTracks";
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* 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
|
||||
*/
|
||||
public void onPrepare(Menu menu, int[] positions, long[] ids);
|
||||
|
||||
/**
|
||||
* 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,81 @@
|
||||
/*
|
||||
* 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.AboutDialogFragment;
|
||||
import com.google.android.apps.mytracks.fragments.AboutDialogFragment.AboutCaller;
|
||||
import com.google.android.apps.mytracks.fragments.EulaDialogFragment;
|
||||
import com.google.android.apps.mytracks.fragments.EulaDialogFragment.EulaCaller;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.webkit.WebView;
|
||||
import android.webkit.WebViewClient;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* An activity that displays the help page.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class HelpActivity extends AbstractMyTracksActivity implements AboutCaller, EulaCaller {
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
WebView webView = (WebView) findViewById(R.id.help_webview);
|
||||
String language = Locale.getDefault().getLanguage();
|
||||
if (language == null || language.equals("")) {
|
||||
language = "en";
|
||||
}
|
||||
webView.loadUrl(getString(R.string.my_tracks_help_url, language));
|
||||
webView.getSettings().setJavaScriptEnabled(true);
|
||||
webView.setWebViewClient(new WebViewClient());
|
||||
|
||||
findViewById(R.id.help_ok).setOnClickListener(new OnClickListener() {
|
||||
public void onClick(View v) {
|
||||
finish();
|
||||
}
|
||||
});
|
||||
findViewById(R.id.help_about).setOnClickListener(new OnClickListener() {
|
||||
public void onClick(View v) {
|
||||
new AboutDialogFragment().show(
|
||||
getSupportFragmentManager(), AboutDialogFragment.ABOUT_DIALOG_TAG);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getLayoutResId() {
|
||||
return R.layout.help;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAboutLicense() {
|
||||
EulaDialogFragment.newInstance(true)
|
||||
.show(getSupportFragmentManager(), EulaDialogFragment.EULA_DIALOG_TAG);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEulaDone() {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* Copyright 2010 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
|
||||
package com.google.android.apps.mytracks;
|
||||
|
||||
import android.content.Context;
|
||||
import android.preference.ListPreference;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
/**
|
||||
* A list preference which persists its values as integers instead of strings.
|
||||
* Code reading the values should use
|
||||
* {@link android.content.SharedPreferences#getInt}.
|
||||
* When using XML-declared arrays for entry values, the arrays should be regular
|
||||
* string arrays containing valid integer values.
|
||||
*
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
public class IntegerListPreference extends ListPreference {
|
||||
|
||||
public IntegerListPreference(Context context) {
|
||||
super(context);
|
||||
|
||||
verifyEntryValues(null);
|
||||
}
|
||||
|
||||
public IntegerListPreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
|
||||
verifyEntryValues(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEntryValues(CharSequence[] entryValues) {
|
||||
CharSequence[] oldValues = getEntryValues();
|
||||
super.setEntryValues(entryValues);
|
||||
verifyEntryValues(oldValues);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEntryValues(int entryValuesResId) {
|
||||
CharSequence[] oldValues = getEntryValues();
|
||||
super.setEntryValues(entryValuesResId);
|
||||
verifyEntryValues(oldValues);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getPersistedString(String defaultReturnValue) {
|
||||
// During initial load, there's no known default value
|
||||
int defaultIntegerValue = Integer.MIN_VALUE;
|
||||
if (defaultReturnValue != null) {
|
||||
defaultIntegerValue = Integer.parseInt(defaultReturnValue);
|
||||
}
|
||||
|
||||
// When the list preference asks us to read a string, instead read an
|
||||
// integer.
|
||||
int value = getPersistedInt(defaultIntegerValue);
|
||||
return Integer.toString(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean persistString(String value) {
|
||||
// When asked to save a string, instead save an integer
|
||||
return persistInt(Integer.parseInt(value));
|
||||
}
|
||||
|
||||
private void verifyEntryValues(CharSequence[] oldValues) {
|
||||
CharSequence[] entryValues = getEntryValues();
|
||||
if (entryValues == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (CharSequence entryValue : entryValues) {
|
||||
try {
|
||||
Integer.parseInt(entryValue.toString());
|
||||
} catch (NumberFormatException nfe) {
|
||||
super.setEntryValues(oldValues);
|
||||
throw nfe;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
/*
|
||||
* 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.Constants.TAG;
|
||||
|
||||
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.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 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 int speed;
|
||||
|
||||
/**
|
||||
* Constructor for an invalid cached location.
|
||||
*/
|
||||
public CachedLocation() {
|
||||
this.valid = false;
|
||||
this.latLng = null;
|
||||
this.speed = -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = (int) Math.floor(location.getSpeed() * UnitConversions.MS_TO_KMH);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the location is valid.
|
||||
*/
|
||||
public boolean isValid() {
|
||||
return valid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the speed in kilometers per hour.
|
||||
*/
|
||||
public int 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);
|
||||
this.pendingLocations = new ArrayBlockingQueue<CachedLocation>(
|
||||
Constants.MAX_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 reload true to reload all points
|
||||
*/
|
||||
public void update(GoogleMap googleMap, ArrayList<Polyline> paths, boolean reload) {
|
||||
synchronized (locations) {
|
||||
// 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() || reload) {
|
||||
googleMap.clear();
|
||||
paths.clear();
|
||||
trackPath.updatePath(googleMap, paths, 0, locations);
|
||||
updateStartAndEndMarkers(googleMap);
|
||||
updateWaypoints(googleMap);
|
||||
} else {
|
||||
if (newLocations != 0) {
|
||||
int numLocations = locations.size();
|
||||
trackPath.updatePath(googleMap, paths, numLocations - newLocations, locations);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the start and end markers.
|
||||
*
|
||||
* @param googleMap the google map
|
||||
*/
|
||||
private void 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.red_dot));
|
||||
googleMap.addMarker(markerOptions);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add the start marker
|
||||
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.green_dot));
|
||||
googleMap.addMarker(markerOptions);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.yellow_pushpin
|
||||
: R.drawable.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,153 @@
|
||||
/*
|
||||
* 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;
|
||||
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.util.IntentUtils;
|
||||
import com.google.android.apps.mytracks.util.StatsUtils;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
/**
|
||||
* 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 MyTracksProviderUtils myTracksProviderUtils;
|
||||
private long markerId;
|
||||
private Waypoint waypoint;
|
||||
|
||||
private TextView name;
|
||||
private View waypointSection;
|
||||
private View statisticsSection;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle bundle) {
|
||||
super.onCreate(bundle);
|
||||
|
||||
myTracksProviderUtils = MyTracksProviderUtils.Factory.get(this);
|
||||
markerId = getIntent().getLongExtra(EXTRA_MARKER_ID, -1L);
|
||||
if (markerId == -1L) {
|
||||
Log.d(TAG, "invalid marker id");
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
name = (TextView) findViewById(R.id.marker_detail_name);
|
||||
waypointSection = findViewById(R.id.marker_detail_waypoint_section);
|
||||
statisticsSection = findViewById(R.id.marker_detail_statistics_section);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getLayoutResId() {
|
||||
return R.layout.marker_detail;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
waypoint = MyTracksProviderUtils.Factory.get(this).getWaypoint(markerId);
|
||||
if (waypoint == null) {
|
||||
Log.d(TAG, "waypoint is null");
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
name.setText(getString(R.string.generic_name_line, waypoint.getName()));
|
||||
if (waypoint.getType() == WaypointType.WAYPOINT) {
|
||||
waypointSection.setVisibility(View.VISIBLE);
|
||||
statisticsSection.setVisibility(View.GONE);
|
||||
|
||||
TextView markerType = (TextView) findViewById(R.id.marker_detail_waypoint_marker_type);
|
||||
markerType.setText(
|
||||
getString(R.string.marker_detail_waypoint_marker_type, waypoint.getCategory()));
|
||||
TextView description = (TextView) findViewById(R.id.marker_detail_waypoint_description);
|
||||
description.setText(getString(R.string.generic_description_line, waypoint.getDescription()));
|
||||
} else {
|
||||
waypointSection.setVisibility(View.GONE);
|
||||
statisticsSection.setVisibility(View.VISIBLE);
|
||||
StatsUtils.setTripStatisticsValues(this, waypoint.getTripStatistics());
|
||||
StatsUtils.setLocationValues(this, waypoint.getLocation(), false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
getMenuInflater().inflate(R.menu.marker_detail, menu);
|
||||
|
||||
Track track = myTracksProviderUtils.getTrack(waypoint.getTrackId());
|
||||
menu.findItem(R.id.marker_detail_edit).setVisible(!track.isSharedWithMe());
|
||||
menu.findItem(R.id.marker_detail_delete).setVisible(!track.isSharedWithMe());
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onHomeSelected() {
|
||||
Intent intent = IntentUtils.newIntent(this, MarkerListActivity.class)
|
||||
.putExtra(MarkerListActivity.EXTRA_TRACK_ID, waypoint.getTrackId());
|
||||
startActivity(intent);
|
||||
finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
Intent intent;
|
||||
switch (item.getItemId()) {
|
||||
case R.id.marker_detail_show_on_map:
|
||||
intent = IntentUtils.newIntent(this, TrackDetailActivity.class)
|
||||
.putExtra(TrackDetailActivity.EXTRA_MARKER_ID, markerId);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
case R.id.marker_detail_edit:
|
||||
intent = IntentUtils.newIntent(this, MarkerEditActivity.class)
|
||||
.putExtra(MarkerEditActivity.EXTRA_MARKER_ID, markerId);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
case R.id.marker_detail_delete:
|
||||
DeleteMarkerDialogFragment.newInstance(new long[] { markerId })
|
||||
.show(getSupportFragmentManager(), DeleteMarkerDialogFragment.DELETE_MARKER_DIALOG_TAG);
|
||||
return true;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDeleteMarkerDone() {
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
finish();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
/*
|
||||
* 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.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.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();
|
||||
}
|
||||
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);
|
||||
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,321 @@
|
||||
/*
|
||||
* 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 };
|
||||
|
||||
// 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) {
|
||||
menu.findItem(R.id.list_context_menu_play).setVisible(false);
|
||||
menu.findItem(R.id.list_context_menu_share).setVisible(false);
|
||||
menu.findItem(R.id.list_context_menu_show_on_map).setVisible(ids.length == 1);
|
||||
menu.findItem(R.id.list_context_menu_edit)
|
||||
.setVisible(ids.length == 1 && !track.isSharedWithMe());
|
||||
menu.findItem(R.id.list_context_menu_delete).setVisible(!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() {
|
||||
updateMenu();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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 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 = (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 categoryIndex = cursor.getColumnIndex(WaypointsColumns.CATEGORY);
|
||||
int timeIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.TIME);
|
||||
int descriptionIndex = cursor.getColumnIndex(WaypointsColumns.DESCRIPTION);
|
||||
|
||||
boolean statistics = WaypointType.values()[cursor.getInt(typeIndex)]
|
||||
== WaypointType.STATISTICS;
|
||||
int iconId = statistics ? R.drawable.yellow_pushpin : R.drawable.blue_pushpin;
|
||||
String category = statistics ? null : cursor.getString(categoryIndex);
|
||||
String description = statistics ? null : cursor.getString(descriptionIndex);
|
||||
ListItemUtils.setListItem(MarkerListActivity.this, view, false, true, iconId,
|
||||
R.string.icon_marker, cursor.getString(nameIndex), category, null, null,
|
||||
cursor.getLong(timeIndex), description, track.getSharedOwner());
|
||||
}
|
||||
};
|
||||
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();
|
||||
updateMenu();
|
||||
}
|
||||
|
||||
@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);
|
||||
updateMenu();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void updateMenu() {
|
||||
if (insertMarkerMenuItem != null) {
|
||||
insertMarkerMenuItem.setVisible(track.getId() == recordingTrackId && !recordingTrackPaused);
|
||||
}
|
||||
}
|
||||
|
||||
@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});
|
||||
}
|
||||
|
||||
@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:
|
||||
DeleteMarkerDialogFragment.newInstance(markerIds)
|
||||
.show(getSupportFragmentManager(), DeleteMarkerDialogFragment.DELETE_MARKER_DIALOG_TAG);
|
||||
return true;
|
||||
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,545 @@
|
||||
/*
|
||||
* 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.fragments.DeleteTrackDialogFragment;
|
||||
import com.google.android.apps.mytracks.fragments.DeleteTrackDialogFragment.DeleteTrackCaller;
|
||||
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 DeleteTrackCaller, 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 CATEGORY_FIELD = "category";
|
||||
private static final String TOTAL_TIME_FIELD = "totalTime";
|
||||
private static final String TOTAL_DISTANCE_FIELD = "totalDistance";
|
||||
private static final String START_TIME_FIELD = "startTime";
|
||||
private static final String DESCRIPTION_FIELD = "description";
|
||||
private static final String SHARED_OWNER_FIELD = "sharedOwner";
|
||||
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.metric_units_key))) {
|
||||
metricUnits = PreferencesUtils.getBoolean(SearchListActivity.this,
|
||||
R.string.metric_units_key, PreferencesUtils.METRIC_UNITS_DEFAULT);
|
||||
}
|
||||
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 shareWithMe = true;
|
||||
Long markerId = null;
|
||||
if (positions.length == 1) {
|
||||
Map<String, Object> item = arrayAdapter.getItem(positions[0]);
|
||||
Long trackId = (Long) item.get(TRACK_ID_FIELD);
|
||||
Track track = myTracksProviderUtils.getTrack(trackId);
|
||||
shareWithMe = track.isSharedWithMe();
|
||||
markerId = (Long) item.get(MARKER_ID_FIELD);
|
||||
}
|
||||
// Only one item, the item is a track
|
||||
menu.findItem(R.id.list_context_menu_play)
|
||||
.setVisible(positions.length == 1 && markerId == null);
|
||||
// Only one item, the item is a track, and the item is not a sharedWithMe track
|
||||
menu.findItem(R.id.list_context_menu_share)
|
||||
.setVisible(positions.length == 1 && markerId == null && !shareWithMe);
|
||||
// Only one item, the item is a marker
|
||||
menu.findItem(R.id.list_context_menu_show_on_map)
|
||||
.setVisible(positions.length == 1 && markerId != null);
|
||||
// Only one item, can be track or marker, but cannot be a sharedWithMe item
|
||||
menu.findItem(R.id.list_context_menu_edit)
|
||||
.setVisible(positions.length == 1 && !shareWithMe);
|
||||
// Only one item. If marker, cannot be a shareWithMe item. If track, no restriction
|
||||
menu.findItem(R.id.list_context_menu_delete)
|
||||
.setVisible(positions.length == 1 && (markerId == null || !shareWithMe));
|
||||
}
|
||||
};
|
||||
|
||||
private MyTracksProviderUtils myTracksProviderUtils;
|
||||
private SharedPreferences sharedPreferences;
|
||||
private TrackRecordingServiceConnection trackRecordingServiceConnection;
|
||||
private SearchEngine searchEngine;
|
||||
private SearchRecentSuggestions searchRecentSuggestions;
|
||||
private MyTracksLocationManager myTracksLocationManager;
|
||||
private ArrayAdapter<Map<String, Object>> arrayAdapter;
|
||||
|
||||
private boolean metricUnits = PreferencesUtils.METRIC_UNITS_DEFAULT;
|
||||
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 category = (String) resultMap.get(CATEGORY_FIELD);
|
||||
String totalTime = (String) resultMap.get(TOTAL_TIME_FIELD);
|
||||
String totalDistance = (String) resultMap.get(TOTAL_DISTANCE_FIELD);
|
||||
Long startTime = (Long) resultMap.get(START_TIME_FIELD);
|
||||
String description = (String) resultMap.get(DESCRIPTION_FIELD);
|
||||
String sharedOwner = (String) resultMap.get(SHARED_OWNER_FIELD);
|
||||
ListItemUtils.setListItem(SearchListActivity.this, view, isRecording, isPaused, iconId,
|
||||
iconContentDescriptionId, name, category, totalTime, totalDistance, startTime,
|
||||
description, sharedOwner);
|
||||
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 true;
|
||||
}
|
||||
|
||||
@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});
|
||||
}
|
||||
|
||||
@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:
|
||||
confirmPlay(new long[] {trackId});
|
||||
return true;
|
||||
case R.id.list_context_menu_share:
|
||||
confirmShare(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 {
|
||||
DeleteTrackDialogFragment.newInstance(false, new long[] { trackId })
|
||||
.show(getSupportFragmentManager(), DeleteTrackDialogFragment.DELETE_TRACK_DIALOG_TAG);
|
||||
}
|
||||
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);
|
||||
|
||||
myTracksLocationManager = new MyTracksLocationManager(this, Looper.myLooper());
|
||||
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;
|
||||
String sharedOwner = null;
|
||||
long trackId = waypoint.getTrackId();
|
||||
if (trackId != -1L) {
|
||||
Track track = myTracksProviderUtils.getTrack(trackId);
|
||||
if (track != null) {
|
||||
trackName = track.getName();
|
||||
sharedOwner = track.getSharedOwner();
|
||||
}
|
||||
}
|
||||
|
||||
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.yellow_pushpin : R.drawable.blue_pushpin);
|
||||
resultMap.put(ICON_CONTENT_DESCRIPTION_ID_FIELD, R.string.icon_marker);
|
||||
resultMap.put(NAME_FIELD, waypoint.getName());
|
||||
resultMap.put(CATEGORY_FIELD, statistics ? null : waypoint.getCategory());
|
||||
// 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(START_TIME_FIELD, waypoint.getLocation().getTime());
|
||||
resultMap.put(DESCRIPTION_FIELD, statistics ? null : waypoint.getDescription());
|
||||
resultMap.put(SHARED_OWNER_FIELD, sharedOwner);
|
||||
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.icon_track);
|
||||
resultMap.put(NAME_FIELD, track.getName());
|
||||
resultMap.put(CATEGORY_FIELD, category);
|
||||
resultMap.put(TOTAL_TIME_FIELD, StringUtils.formatElapsedTime(tripStatitics.getTotalTime()));
|
||||
resultMap.put(TOTAL_DISTANCE_FIELD,
|
||||
StringUtils.formatDistance(this, tripStatitics.getTotalDistance(), metricUnits));
|
||||
resultMap.put(START_TIME_FIELD, tripStatitics.getStartTime());
|
||||
resultMap.put(DESCRIPTION_FIELD, track.getDescription());
|
||||
resultMap.put(SHARED_OWNER_FIELD, track.getSharedOwner());
|
||||
resultMap.put(TRACK_ID_FIELD, track.getId());
|
||||
resultMap.put(MARKER_ID_FIELD, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TrackRecordingServiceConnection getTrackRecordingServiceConnection() {
|
||||
return trackRecordingServiceConnection;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDeleteTrackDone() {
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
handleIntent(getIntent());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDeleteMarkerDone() {
|
||||
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,308 @@
|
||||
/*
|
||||
* 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.maps.mytracks.R;
|
||||
import com.google.protobuf.InvalidProtocolBufferException;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.os.RemoteException;
|
||||
import android.text.format.DateFormat;
|
||||
import android.util.Log;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
|
||||
/**
|
||||
* An activity that displays information about sensors.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class SensorStateActivity extends AbstractMyTracksActivity {
|
||||
|
||||
private static final String TAG = SensorStateActivity.class.getName();
|
||||
private static final long REFRESH_PERIOD_MS = 250;
|
||||
|
||||
/**
|
||||
* A Runnable to update the UI.
|
||||
*/
|
||||
private final Runnable updateRunnable = new Runnable() {
|
||||
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();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* A TimeTask to update the UI.
|
||||
*/
|
||||
private class UpdateTimerTask extends TimerTask {
|
||||
@Override
|
||||
public void run() {
|
||||
if (isVisible) {
|
||||
runOnUiThread(updateRunnable);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private TrackRecordingServiceConnection trackRecordingServiceConnection;
|
||||
private boolean isVisible = false;
|
||||
private Timer timer = null;
|
||||
private SensorManager tempSensorManager = null;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
trackRecordingServiceConnection = new TrackRecordingServiceConnection(this, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStart() {
|
||||
super.onStart();
|
||||
TrackRecordingServiceConnectionUtils.startConnection(this, trackRecordingServiceConnection);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
isVisible = true;
|
||||
timer = new Timer();
|
||||
timer.schedule(new UpdateTimerTask(), 0, REFRESH_PERIOD_MS);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
isVisible = false;
|
||||
timer.cancel();
|
||||
timer.purge();
|
||||
timer = null;
|
||||
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,164 @@
|
||||
/*
|
||||
* 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.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();
|
||||
private static final int ONE_SECOND = 1000;
|
||||
|
||||
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;
|
||||
containerView.setVisibility(alwaysShow || isRecording ? View.VISIBLE : View.GONE);
|
||||
|
||||
if (!alwaysShow && !isRecording) {
|
||||
stopTimer();
|
||||
return;
|
||||
}
|
||||
|
||||
recordImageButton.setImageResource(
|
||||
isRecording && !isPaused ? R.drawable.btn_pause : R.drawable.btn_record);
|
||||
recordImageButton.setContentDescription(activity.getString(
|
||||
isRecording && !isPaused ? R.string.icon_pause_recording : R.string.icon_record_track));
|
||||
|
||||
stopImageButton.setImageResource(isRecording ? R.drawable.btn_stop_1 : R.drawable.btn_stop_0);
|
||||
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);
|
||||
}
|
||||
|
||||
stopTimer();
|
||||
totalTime = isRecording ? getTotalTime() : 0L;
|
||||
totalTimeTextView.setText(StringUtils.formatElapsedTimeWithHour(totalTime));
|
||||
if (isRecording && !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;
|
||||
stopTimer();
|
||||
}
|
||||
|
||||
public void hide() {
|
||||
containerView.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
public void show() {
|
||||
containerView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the timer.
|
||||
*/
|
||||
private void stopTimer() {
|
||||
handler.removeCallbacks(updateTotalTimeRunnable);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,564 @@
|
||||
/*
|
||||
* 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.WaypointCreationRequest;
|
||||
import com.google.android.apps.mytracks.fragments.ChartFragment;
|
||||
import com.google.android.apps.mytracks.fragments.ChooseUploadServiceDialogFragment;
|
||||
import com.google.android.apps.mytracks.fragments.ChooseUploadServiceDialogFragment.ChooseUploadServiceCaller;
|
||||
import com.google.android.apps.mytracks.fragments.DeleteTrackDialogFragment;
|
||||
import com.google.android.apps.mytracks.fragments.DeleteTrackDialogFragment.DeleteTrackCaller;
|
||||
import com.google.android.apps.mytracks.fragments.FileTypeDialogFragment;
|
||||
import com.google.android.apps.mytracks.fragments.FileTypeDialogFragment.FileTypeCaller;
|
||||
import com.google.android.apps.mytracks.fragments.FrequencyDialogFragment;
|
||||
import com.google.android.apps.mytracks.fragments.MyTracksMapFragment;
|
||||
import com.google.android.apps.mytracks.fragments.StatsFragment;
|
||||
import com.google.android.apps.mytracks.io.file.SaveActivity;
|
||||
import com.google.android.apps.mytracks.io.file.TrackFileFormat;
|
||||
import com.google.android.apps.mytracks.io.sendtogoogle.SendRequest;
|
||||
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.GoogleFeedbackUtils;
|
||||
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.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcelable;
|
||||
import android.support.v4.app.TaskStackBuilder;
|
||||
import android.support.v4.view.ViewPager;
|
||||
import android.util.Log;
|
||||
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 java.util.Locale;
|
||||
|
||||
/**
|
||||
* An activity to show the track detail.
|
||||
*
|
||||
* @author Leif Hendrik Wilden
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
public class TrackDetailActivity extends AbstractSendToGoogleActivity
|
||||
implements ChooseUploadServiceCaller, DeleteTrackCaller, FileTypeCaller {
|
||||
|
||||
public static final String EXTRA_TRACK_ID = "track_id";
|
||||
public static final String EXTRA_MARKER_ID = "marker_id";
|
||||
|
||||
private static final String TAG = TrackDetailActivity.class.getSimpleName();
|
||||
private static final String CURRENT_TAB_TAG_KEY = "current_tab_tag_key";
|
||||
|
||||
// The following are set in onCreate
|
||||
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 MenuItem insertMarkerMenuItem;
|
||||
private MenuItem playMenuItem;
|
||||
private MenuItem shareMenuItem;
|
||||
private MenuItem sendGoogleMenuItem;
|
||||
private MenuItem saveMenuItem;
|
||||
private MenuItem voiceFrequencyMenuItem;
|
||||
private MenuItem splitFrequencyMenuItem;
|
||||
private MenuItem feedbackMenuItem;
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* 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) {
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
boolean isRecording = trackId == recordingTrackId;
|
||||
updateMenuItems(isRecording, recordingTrackPaused);
|
||||
trackController.update(isRecording, recordingTrackPaused);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private final OnClickListener recordListener = new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (recordingTrackPaused) {
|
||||
// Paused -> Resume
|
||||
AnalyticsUtils.sendPageViews(TrackDetailActivity.this, "/action/resume_track");
|
||||
updateMenuItems(true, false);
|
||||
TrackRecordingServiceConnectionUtils.resumeTrack(trackRecordingServiceConnection);
|
||||
trackController.update(true, false);
|
||||
} else {
|
||||
// Recording -> Paused
|
||||
AnalyticsUtils.sendPageViews(TrackDetailActivity.this, "/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, "/action/stop_recording");
|
||||
updateMenuItems(false, true);
|
||||
TrackRecordingServiceConnectionUtils.stopRecording(
|
||||
TrackDetailActivity.this, trackRecordingServiceConnection, true);
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
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.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.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.tab_stats));
|
||||
tabsAdapter.addTab(statsTabSpec, StatsFragment.class, null);
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
tabHost.setCurrentTabByTag(savedInstanceState.getString(CURRENT_TAB_TAG_KEY));
|
||||
}
|
||||
trackController = new TrackController(
|
||||
this, trackRecordingServiceConnection, false, recordListener, stopListener);
|
||||
showMarker();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStart() {
|
||||
super.onStart();
|
||||
|
||||
sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
|
||||
sharedPreferenceChangeListener.onSharedPreferenceChanged(null, null);
|
||||
|
||||
TrackRecordingServiceConnectionUtils.startConnection(this, trackRecordingServiceConnection);
|
||||
trackDataHub.start();
|
||||
AnalyticsUtils.sendPageViews(this, "/page/track_detail");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
trackDataHub.loadTrack(trackId);
|
||||
|
||||
// Update UI
|
||||
boolean isRecording = trackId == recordingTrackId;
|
||||
updateMenuItems(isRecording, recordingTrackPaused);
|
||||
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());
|
||||
}
|
||||
|
||||
@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);
|
||||
showMarker();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
getMenuInflater().inflate(R.menu.track_detail, menu);
|
||||
|
||||
Track track = myTracksProviderUtils.getTrack(trackId);
|
||||
boolean sharedWithMe = track != null ? track.isSharedWithMe() : true;
|
||||
menu.findItem(R.id.track_detail_edit).setVisible(!sharedWithMe);
|
||||
shareMenuItem = menu.findItem(R.id.track_detail_share);
|
||||
shareMenuItem.setEnabled(!sharedWithMe);
|
||||
shareMenuItem.setVisible(!sharedWithMe);
|
||||
|
||||
insertMarkerMenuItem = menu.findItem(R.id.track_detail_insert_marker);
|
||||
playMenuItem = menu.findItem(R.id.track_detail_play);
|
||||
sendGoogleMenuItem = menu.findItem(R.id.track_detail_send_google);
|
||||
saveMenuItem = menu.findItem(R.id.track_detail_save);
|
||||
voiceFrequencyMenuItem = menu.findItem(R.id.track_detail_voice_frequency);
|
||||
splitFrequencyMenuItem = menu.findItem(R.id.track_detail_split_frequency);
|
||||
feedbackMenuItem = menu.findItem(R.id.track_detail_split_frequency);
|
||||
feedbackMenuItem.setVisible(GoogleFeedbackUtils.isAvailable(this));
|
||||
|
||||
updateMenuItems(trackId == recordingTrackId, recordingTrackPaused);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareOptionsMenu(Menu menu) {
|
||||
boolean showSensorState = !PreferencesUtils.SENSOR_TYPE_DEFAULT.equals(
|
||||
PreferencesUtils.getString(
|
||||
this, R.string.sensor_type_key, PreferencesUtils.SENSOR_TYPE_DEFAULT));
|
||||
menu.findItem(R.id.track_detail_sensor_state).setVisible(showSensorState);
|
||||
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, "/action/insert_marker");
|
||||
intent = IntentUtils.newIntent(this, MarkerEditActivity.class)
|
||||
.putExtra(MarkerEditActivity.EXTRA_TRACK_ID, trackId);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
case R.id.track_detail_play:
|
||||
confirmPlay(new long[] {trackId});
|
||||
return true;
|
||||
case R.id.track_detail_share:
|
||||
confirmShare(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_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_send_google:
|
||||
AnalyticsUtils.sendPageViews(this, "/action/send_google");
|
||||
Track track = myTracksProviderUtils.getTrack(trackId);
|
||||
boolean sharedWithMe = track != null ? track.isSharedWithMe() : true;
|
||||
ChooseUploadServiceDialogFragment.newInstance(sharedWithMe).show(
|
||||
getSupportFragmentManager(),
|
||||
ChooseUploadServiceDialogFragment.CHOOSE_UPLOAD_SERVICE_DIALOG_TAG);
|
||||
return true;
|
||||
case R.id.track_detail_save:
|
||||
FileTypeDialogFragment.newInstance(R.id.track_detail_save, R.string.save_selection_title,
|
||||
R.string.save_selection_option, 4)
|
||||
.show(getSupportFragmentManager(), FileTypeDialogFragment.FILE_TYPE_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:
|
||||
DeleteTrackDialogFragment.newInstance(false, new long[] { trackId })
|
||||
.show(getSupportFragmentManager(), DeleteTrackDialogFragment.DELETE_TRACK_DIALOG_TAG);
|
||||
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_feedback:
|
||||
GoogleFeedbackUtils.bindFeedback(this);
|
||||
return true;
|
||||
case R.id.track_detail_help:
|
||||
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 onChooseUploadServiceDone(boolean sendDrive, boolean sendMaps,
|
||||
boolean sendFusionTables, boolean sendSpreadsheets, boolean mapsExistingMap) {
|
||||
SendRequest sendRequest = new SendRequest(trackId);
|
||||
sendRequest.setSendDrive(sendDrive);
|
||||
sendRequest.setSendMaps(sendMaps);
|
||||
sendRequest.setSendFusionTables(sendFusionTables);
|
||||
sendRequest.setSendSpreadsheets(sendSpreadsheets);
|
||||
sendRequest.setMapsExistingMap(mapsExistingMap);
|
||||
if (sendDrive) {
|
||||
AnalyticsUtils.sendPageViews(this, "/send/drive");
|
||||
}
|
||||
if (sendMaps) {
|
||||
AnalyticsUtils.sendPageViews(this, "/send/maps");
|
||||
}
|
||||
if (sendFusionTables) {
|
||||
AnalyticsUtils.sendPageViews(this, "/send/fusion_tables");
|
||||
}
|
||||
if (sendSpreadsheets) {
|
||||
AnalyticsUtils.sendPageViews(this, "/send/spreadsheets");
|
||||
}
|
||||
sendToGoogle(sendRequest);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TrackRecordingServiceConnection getTrackRecordingServiceConnection() {
|
||||
return trackRecordingServiceConnection;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDeleteTrackDone() {
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
finish();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFileTypeDone(int menuId, TrackFileFormat trackFileFormat) {
|
||||
switch (menuId) {
|
||||
case R.id.track_detail_save:
|
||||
AnalyticsUtils.sendPageViews(
|
||||
this, "/action/save_" + trackFileFormat.name().toLowerCase(Locale.US));
|
||||
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);
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link TrackDataHub}.
|
||||
*/
|
||||
public TrackDataHub getTrackDataHub() {
|
||||
return trackDataHub;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows marker.
|
||||
*/
|
||||
private void showMarker() {
|
||||
if (markerId != -1L) {
|
||||
MyTracksMapFragment mapFragmet = (MyTracksMapFragment) getSupportFragmentManager()
|
||||
.findFragmentByTag(MyTracksMapFragment.MAP_FRAGMENT_TAG);
|
||||
if (mapFragmet != null) {
|
||||
tabHost.setCurrentTabByTag(MyTracksMapFragment.MAP_FRAGMENT_TAG);
|
||||
mapFragmet.showMarker(trackId, markerId);
|
||||
} else {
|
||||
Log.e(TAG, "MapFragment is null");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the menu items.
|
||||
*
|
||||
* @param isRecording true if recording
|
||||
*/
|
||||
private void updateMenuItems(boolean isRecording, boolean isPaused) {
|
||||
if (insertMarkerMenuItem != null) {
|
||||
insertMarkerMenuItem.setVisible(isRecording && !isPaused);
|
||||
}
|
||||
if (playMenuItem != null) {
|
||||
playMenuItem.setVisible(!isRecording);
|
||||
}
|
||||
if (shareMenuItem != null && shareMenuItem.isEnabled()) {
|
||||
shareMenuItem.setVisible(!isRecording);
|
||||
}
|
||||
if (sendGoogleMenuItem != null) {
|
||||
sendGoogleMenuItem.setVisible(!isRecording);
|
||||
}
|
||||
if (saveMenuItem != null) {
|
||||
saveMenuItem.setVisible(!isRecording);
|
||||
}
|
||||
if (voiceFrequencyMenuItem != null) {
|
||||
voiceFrequencyMenuItem.setVisible(isRecording);
|
||||
}
|
||||
if (splitFrequencyMenuItem != null) {
|
||||
splitFrequencyMenuItem.setVisible(isRecording);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
/*
|
||||
* 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.ChooseActivityTypeDialogFragment;
|
||||
import com.google.android.apps.mytracks.fragments.ChooseActivityTypeDialogFragment.ChooseActivityTypeCaller;
|
||||
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
|
||||
import com.google.android.apps.mytracks.util.TrackIconUtils;
|
||||
import com.google.android.apps.mytracks.util.TrackNameUtils;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.BitmapFactory.Options;
|
||||
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.ImageView;
|
||||
import android.widget.Spinner;
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
|
||||
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 Long trackId;
|
||||
private MyTracksProviderUtils myTracksProviderUtils;
|
||||
private Track track;
|
||||
private String iconValue;
|
||||
|
||||
private EditText name;
|
||||
private AutoCompleteTextView activityType;
|
||||
private Spinner activityTypeIcon;
|
||||
private EditText description;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle bundle) {
|
||||
super.onCreate(bundle);
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
// Add a border around the icon
|
||||
Options options = new BitmapFactory.Options();
|
||||
options.inJustDecodeBounds = true;
|
||||
BitmapFactory.decodeResource(getResources(), R.drawable.track_airplane, options);
|
||||
final int padding = 4;
|
||||
final int width = options.outWidth + 2 * padding;
|
||||
final int height = options.outHeight + 2 * padding;
|
||||
|
||||
activityTypeIcon = (Spinner) findViewById(R.id.track_edit_activity_type_icon);
|
||||
ArrayAdapter<StringBuilder> activityTypeIconAdapter = new ArrayAdapter<StringBuilder>(
|
||||
this, android.R.layout.simple_spinner_item,
|
||||
new StringBuilder[] {new StringBuilder(iconValue)}) {
|
||||
@Override
|
||||
public View getView(int position, View convertView, android.view.ViewGroup parent) {
|
||||
ImageView imageView =
|
||||
convertView != null ? (ImageView) convertView : new ImageView(getContext());
|
||||
Bitmap source = BitmapFactory.decodeResource(
|
||||
getResources(), TrackIconUtils.getIconDrawable(getItem(position).toString()));
|
||||
if (ApiAdapterFactory.getApiAdapter().isSpinnerBackgroundLight()) {
|
||||
source = TrackIconUtils.invertBitmap(source);
|
||||
}
|
||||
imageView.setImageBitmap(source);
|
||||
imageView.setMinimumHeight(height);
|
||||
imageView.setMinimumWidth(width);
|
||||
imageView.setPadding(padding, padding, padding, padding);
|
||||
return imageView;
|
||||
}
|
||||
};
|
||||
activityTypeIcon.setAdapter(activityTypeIconAdapter);
|
||||
activityTypeIcon.setOnTouchListener(new View.OnTouchListener() {
|
||||
@Override
|
||||
public boolean onTouch(View v, MotionEvent event) {
|
||||
if (event.getAction() == MotionEvent.ACTION_UP) {
|
||||
new ChooseActivityTypeDialogFragment().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) {
|
||||
new ChooseActivityTypeDialogFragment().show(getSupportFragmentManager(),
|
||||
ChooseActivityTypeDialogFragment.CHOOSE_ACTIVITY_TYPE_DIALOG_TAG);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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) {
|
||||
track.setName(name.getText().toString());
|
||||
String category = activityType.getText().toString();
|
||||
track.setCategory(category);
|
||||
track.setIcon(TrackIconUtils.getIconValue(TrackEditActivity.this, category));
|
||||
track.setDescription(description.getText().toString());
|
||||
track.setModifiedTime(System.currentTimeMillis());
|
||||
myTracksProviderUtils.updateTrack(track);
|
||||
finish();
|
||||
}
|
||||
});
|
||||
|
||||
Button cancel = (Button) findViewById(R.id.track_edit_cancel);
|
||||
if (getIntent().getBooleanExtra(EXTRA_NEW_TRACK, false)) {
|
||||
String trackName = TrackNameUtils.getTrackName(
|
||||
this, -1L, -1L, myTracksProviderUtils.getFirstValidTrackPoint(trackId));
|
||||
if (trackName != null) {
|
||||
name.setText(trackName);
|
||||
}
|
||||
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 void onSaveInstanceState(Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putString(ICON_VALUE_KEY, iconValue);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getLayoutResId() {
|
||||
return R.layout.track_edit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the activity type icon.
|
||||
*
|
||||
* @param value the icon value
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private void setActivityTypeIcon(String value) {
|
||||
iconValue = value;
|
||||
ArrayAdapter<StringBuilder> adapter =
|
||||
(ArrayAdapter<StringBuilder>) activityTypeIcon.getAdapter();
|
||||
StringBuilder stringBuilder = adapter.getItem(0);
|
||||
stringBuilder.delete(0, stringBuilder.length());
|
||||
stringBuilder.append(value);
|
||||
adapter.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChooseActivityTypeDone(String value) {
|
||||
activityType.setText(getString(TrackIconUtils.getIconActivityType(value)));
|
||||
setActivityTypeIcon(value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,776 @@
|
||||
/*
|
||||
* 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.DeleteTrackDialogFragment;
|
||||
import com.google.android.apps.mytracks.fragments.DeleteTrackDialogFragment.DeleteTrackCaller;
|
||||
import com.google.android.apps.mytracks.fragments.EnableSyncDialogFragment;
|
||||
import com.google.android.apps.mytracks.fragments.EnableSyncDialogFragment.EnableSyncCaller;
|
||||
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.io.file.ImportActivity;
|
||||
import com.google.android.apps.mytracks.io.file.SaveActivity;
|
||||
import com.google.android.apps.mytracks.io.file.TrackFileFormat;
|
||||
import com.google.android.apps.mytracks.io.sendtogoogle.SendRequest;
|
||||
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.RemoveTempFilesService;
|
||||
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.GoogleFeedbackUtils;
|
||||
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.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.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.provider.Settings;
|
||||
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, EnableSyncCaller, DeleteTrackCaller, FileTypeCaller {
|
||||
|
||||
private static final String TAG = TrackListActivity.class.getSimpleName();
|
||||
private static final int GOOGLE_PLAY_SERVICES_REQUEST_CODE = 0;
|
||||
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.metric_units_key))) {
|
||||
metricUnits = PreferencesUtils.getBoolean(TrackListActivity.this,
|
||||
R.string.metric_units_key, PreferencesUtils.METRIC_UNITS_DEFAULT);
|
||||
}
|
||||
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() {
|
||||
boolean isGpsStarted = TrackRecordingServiceConnectionUtils
|
||||
.isRecordingServiceRunning(TrackListActivity.this);
|
||||
boolean isRecording = recordingTrackId
|
||||
!= PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
|
||||
updateMenuItems(isGpsStarted, isRecording);
|
||||
sectionResourceCursorAdapter.notifyDataSetChanged();
|
||||
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 shareWithMe = true;
|
||||
if (ids.length == 1) {
|
||||
Track track = myTracksProviderUtils.getTrack(ids[0]);
|
||||
shareWithMe = track.isSharedWithMe();
|
||||
}
|
||||
menu.findItem(R.id.list_context_menu_share).setVisible(!shareWithMe);
|
||||
menu.findItem(R.id.list_context_menu_show_on_map).setVisible(false);
|
||||
menu.findItem(R.id.list_context_menu_edit).setVisible(!shareWithMe);
|
||||
menu.findItem(R.id.list_context_menu_delete).setVisible(true);
|
||||
}
|
||||
|
||||
@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, "/action/record_track");
|
||||
updateMenuItems(false, true);
|
||||
startRecording();
|
||||
} else {
|
||||
if (recordingTrackPaused) {
|
||||
// Paused -> Resume
|
||||
AnalyticsUtils.sendPageViews(TrackListActivity.this, "/action/resume_track");
|
||||
updateMenuItems(false, true);
|
||||
TrackRecordingServiceConnectionUtils.resumeTrack(trackRecordingServiceConnection);
|
||||
trackController.update(true, false);
|
||||
} else {
|
||||
// Recording -> Paused
|
||||
AnalyticsUtils.sendPageViews(TrackListActivity.this, "/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, "/action/stop_recording");
|
||||
updateMenuItems(false, false);
|
||||
TrackRecordingServiceConnectionUtils.stopRecording(
|
||||
TrackListActivity.this, trackRecordingServiceConnection, true);
|
||||
}
|
||||
};
|
||||
|
||||
// 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 = PreferencesUtils.METRIC_UNITS_DEFAULT;
|
||||
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 importAllMenuItem;
|
||||
private MenuItem saveAllMenuItem;
|
||||
private MenuItem deleteAllMenuItem;
|
||||
private MenuItem syncNowMenuItem;
|
||||
private MenuItem feedbackMenuItem;
|
||||
|
||||
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();
|
||||
}
|
||||
Intent intent = new Intent(this, RemoveTempFilesService.class);
|
||||
startService(intent);
|
||||
|
||||
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 categoryIndex = cursor.getColumnIndex(TracksColumns.CATEGORY);
|
||||
int totalTimeIndex = cursor.getColumnIndexOrThrow(TracksColumns.TOTALTIME);
|
||||
int totalDistanceIndex = cursor.getColumnIndexOrThrow(TracksColumns.TOTALDISTANCE);
|
||||
int startTimeIndex = cursor.getColumnIndexOrThrow(TracksColumns.STARTTIME);
|
||||
int descriptionIndex = cursor.getColumnIndex(TracksColumns.DESCRIPTION);
|
||||
int sharedOwnerIndex = cursor.getColumnIndex(TracksColumns.SHAREDOWNER);
|
||||
|
||||
boolean isRecording = cursor.getLong(idIndex) == recordingTrackId;
|
||||
String icon = cursor.getString(iconIndex);
|
||||
int iconId = TrackIconUtils.getIconDrawable(icon);
|
||||
String name = cursor.getString(nameIndex);
|
||||
String category = icon != null && !icon.equals("") ? null : cursor.getString(categoryIndex);
|
||||
String totalTime = StringUtils.formatElapsedTime(cursor.getLong(totalTimeIndex));
|
||||
String totalDistance = StringUtils.formatDistance(
|
||||
TrackListActivity.this, cursor.getDouble(totalDistanceIndex), metricUnits);
|
||||
long startTime = cursor.getLong(startTimeIndex);
|
||||
String description = cursor.getString(descriptionIndex);
|
||||
String sharedOwner = cursor.getString(sharedOwnerIndex);
|
||||
|
||||
ListItemUtils.setListItem(TrackListActivity.this, view, isRecording, recordingTrackPaused,
|
||||
iconId, R.string.icon_track, name, category, totalTime, totalDistance, startTime,
|
||||
description, sharedOwner);
|
||||
}
|
||||
};
|
||||
listView.setAdapter(sectionResourceCursorAdapter);
|
||||
ApiAdapterFactory.getApiAdapter()
|
||||
.configureListViewContextualMenu(this, listView, contextualActionModeCallback);
|
||||
|
||||
getSupportLoaderManager().initLoader(0, null, new LoaderCallbacks<Cursor>() {
|
||||
@Override
|
||||
public Loader<Cursor> onCreateLoader(int arg0, Bundle arg1) {
|
||||
return new CursorLoader(TrackListActivity.this, TracksColumns.CONTENT_URI, PROJECTION, null,
|
||||
null, TracksColumns.SHAREDWITHME + " ASC, " + TracksColumns.STARTTIME + " DESC");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
|
||||
sectionResourceCursorAdapter.swapCursor(cursor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(Loader<Cursor> loader) {
|
||||
sectionResourceCursorAdapter.swapCursor(null);
|
||||
}
|
||||
});
|
||||
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, "/page/track_list");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
|
||||
// Update UI
|
||||
boolean isGpsStarted = TrackRecordingServiceConnectionUtils.isRecordingServiceRunning(this);
|
||||
boolean isRecording = recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
|
||||
updateMenuItems(isGpsStarted, isRecording);
|
||||
sectionResourceCursorAdapter.notifyDataSetChanged();
|
||||
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);
|
||||
|
||||
searchMenuItem = menu.findItem(R.id.track_list_search);
|
||||
startGpsMenuItem = menu.findItem(R.id.track_list_start_gps);
|
||||
importAllMenuItem = menu.findItem(R.id.track_list_import_all);
|
||||
saveAllMenuItem = menu.findItem(R.id.track_list_save_all);
|
||||
deleteAllMenuItem = menu.findItem(R.id.track_list_delete_all);
|
||||
syncNowMenuItem = menu.findItem(R.id.track_list_sync_now);
|
||||
feedbackMenuItem = menu.findItem(R.id.track_list_feedback);
|
||||
feedbackMenuItem.setVisible(GoogleFeedbackUtils.isAvailable(this));
|
||||
|
||||
ApiAdapterFactory.getApiAdapter().configureSearchWidget(this, searchMenuItem, trackController);
|
||||
boolean isGpsStarted = TrackRecordingServiceConnectionUtils.isRecordingServiceRunning(this);
|
||||
boolean isRecording = recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
|
||||
updateMenuItems(isGpsStarted, isRecording);
|
||||
return true;
|
||||
}
|
||||
|
||||
@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());
|
||||
if (!myTracksLocationManager.isGpsProviderEnabled()) {
|
||||
intent = GoogleLocationUtils.isAvailable(TrackListActivity.this) ? new Intent(
|
||||
GoogleLocationUtils.ACTION_GOOGLE_LOCATION_SETTINGS)
|
||||
: new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS);
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
startActivity(intent);
|
||||
} else {
|
||||
boolean isGpsStarted = TrackRecordingServiceConnectionUtils.isRecordingServiceRunning(
|
||||
this);
|
||||
boolean isRecording = recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
|
||||
|
||||
startGps = !isGpsStarted;
|
||||
|
||||
// Update menu
|
||||
updateMenuItems(startGps, isRecording);
|
||||
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
myTracksLocationManager.close();
|
||||
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_save_all:
|
||||
FileTypeDialogFragment.newInstance(R.id.track_list_save_all, R.string.save_selection_title,
|
||||
R.string.save_all_selection_option, 4)
|
||||
.show(getSupportFragmentManager(), FileTypeDialogFragment.FILE_TYPE_DIALOG_TAG);
|
||||
return true;
|
||||
case R.id.track_list_delete_all:
|
||||
DeleteTrackDialogFragment.newInstance(true, new long[] {})
|
||||
.show(getSupportFragmentManager(), DeleteTrackDialogFragment.DELETE_TRACK_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_sync_now:
|
||||
SyncUtils.syncNow(this);
|
||||
return true;
|
||||
case R.id.track_list_settings:
|
||||
intent = IntentUtils.newIntent(this, SettingsActivity.class);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
case R.id.track_list_feedback:
|
||||
GoogleFeedbackUtils.bindFeedback(this);
|
||||
return true;
|
||||
case R.id.track_list_help:
|
||||
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 });
|
||||
}
|
||||
|
||||
@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
|
||||
public TrackRecordingServiceConnection getTrackRecordingServiceConnection() {
|
||||
return trackRecordingServiceConnection;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDeleteTrackDone() {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFileTypeDone(int menuId, TrackFileFormat trackFileFormat) {
|
||||
Intent intent;
|
||||
switch (menuId) {
|
||||
case R.id.track_list_save_all:
|
||||
AnalyticsUtils.sendPageViews(
|
||||
this, "/action/save_all_" + trackFileFormat.name().toLowerCase(Locale.US));
|
||||
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, "/action/import_all_" + trackFileFormat.name().toLowerCase(Locale.US));
|
||||
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.getAcceptEula(this)) {
|
||||
Fragment fragment = getSupportFragmentManager()
|
||||
.findFragmentByTag(EulaDialogFragment.EULA_DIALOG_TAG);
|
||||
if (fragment == null) {
|
||||
EulaDialogFragment.newInstance(false)
|
||||
.show(getSupportFragmentManager(), EulaDialogFragment.EULA_DIALOG_TAG);
|
||||
}
|
||||
} else {
|
||||
/*
|
||||
* Before the welcome sequence, the empty view is not visible so that it
|
||||
* doesn't show through.
|
||||
*/
|
||||
findViewById(R.id.track_list_empty_view).setVisibility(View.VISIBLE);
|
||||
|
||||
checkGooglePlayServices();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEulaDone() {
|
||||
if (EulaUtils.getAcceptEula(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;
|
||||
}
|
||||
}
|
||||
showEnableSync();
|
||||
}
|
||||
|
||||
private void showEnableSync() {
|
||||
if (EulaUtils.getShowEnableSync(this)) {
|
||||
Fragment fragment = getSupportFragmentManager()
|
||||
.findFragmentByTag(EnableSyncDialogFragment.ENABLE_SYNC_DIALOG_TAG);
|
||||
if (fragment == null) {
|
||||
new EnableSyncDialogFragment().show(
|
||||
getSupportFragmentManager(), EnableSyncDialogFragment.ENABLE_SYNC_DIALOG_TAG);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnableSyncDone(boolean enable) {
|
||||
EulaUtils.setShowEnableSync(this);
|
||||
if (enable) {
|
||||
SendRequest sendRequest = new SendRequest(-1L);
|
||||
sendRequest.setSendDrive(true);
|
||||
sendRequest.setDriveEnableSync(true);
|
||||
sendToGoogle(sendRequest);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the menu items.
|
||||
*
|
||||
* @param isGpsStarted true if gps is started
|
||||
* @param isRecording true if recording
|
||||
*/
|
||||
private void updateMenuItems(boolean isGpsStarted, boolean isRecording) {
|
||||
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.menu_stop_gps : R.drawable.menu_start_gps);
|
||||
}
|
||||
}
|
||||
if (importAllMenuItem != null) {
|
||||
importAllMenuItem.setVisible(!isRecording);
|
||||
}
|
||||
if (saveAllMenuItem != null) {
|
||||
saveAllMenuItem.setVisible(!isRecording);
|
||||
}
|
||||
if (deleteAllMenuItem != null) {
|
||||
deleteAllMenuItem.setVisible(!isRecording);
|
||||
}
|
||||
if (syncNowMenuItem != null) {
|
||||
syncNowMenuItem.setVisible(driveSync);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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:
|
||||
confirmPlay(trackIds);
|
||||
return true;
|
||||
case R.id.list_context_menu_share:
|
||||
confirmShare(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:
|
||||
boolean deleteAll = listView.getCount() == trackIds.length;
|
||||
DeleteTrackDialogFragment.newInstance(deleteAll, trackIds)
|
||||
.show(getSupportFragmentManager(), DeleteTrackDialogFragment.DELETE_TRACK_DIALOG_TAG);
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* 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 com.google.android.apps.mytracks.services.MyTracksLocationManager;
|
||||
import com.google.android.gms.location.LocationListener;
|
||||
|
||||
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;
|
||||
import android.os.Looper;
|
||||
|
||||
/**
|
||||
* Data source on the phone.
|
||||
*
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
public class DataSource {
|
||||
|
||||
private final ContentResolver contentResolver;
|
||||
private final MyTracksLocationManager myTracksLocationManager;
|
||||
private final SharedPreferences sharedPreferences;
|
||||
|
||||
public DataSource(Context context) {
|
||||
contentResolver = context.getContentResolver();
|
||||
myTracksLocationManager = new MyTracksLocationManager(context, Looper.myLooper());
|
||||
sharedPreferences = context.getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
|
||||
}
|
||||
|
||||
public void close() {
|
||||
myTracksLocationManager.close();
|
||||
}
|
||||
|
||||
public boolean isAllowed() {
|
||||
return myTracksLocationManager.isAllowed();
|
||||
}
|
||||
|
||||
public boolean isGpsProviderEnabled() {
|
||||
return myTracksLocationManager.isGpsProviderEnabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 location listener.
|
||||
*
|
||||
* @param listener the listener
|
||||
*/
|
||||
public void registerLocationListener(LocationListener listener) {
|
||||
myTracksLocationManager.requestLocationUpdates(0, 0, listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters a location listener.
|
||||
*
|
||||
* @param listener the listener
|
||||
*/
|
||||
public void unregisterLocationListener(LocationListener listener) {
|
||||
myTracksLocationManager.removeLocationUpdates(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Request last location.
|
||||
*/
|
||||
public void requestLastLocation(LocationListener locationListener) {
|
||||
myTracksLocationManager.requestLastLocation(locationListener);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,63 @@
|
||||
/*
|
||||
* 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 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 the current location changes.
|
||||
*
|
||||
* @param location the location
|
||||
*/
|
||||
public void notifyLocationChanged(Location location);
|
||||
|
||||
/**
|
||||
* Notifies when the location provider enabled status changes.
|
||||
*
|
||||
* @param enabled true if enabled, false if not
|
||||
*/
|
||||
public void notifyLocationProviderEnabled(boolean enabled);
|
||||
|
||||
/**
|
||||
* Notifies when a preference changes.
|
||||
*
|
||||
* @param key the preference key
|
||||
*/
|
||||
public void notifyPreferenceChanged(String key);
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
/*
|
||||
* 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 static com.google.android.apps.mytracks.Constants.TAG;
|
||||
|
||||
import com.google.android.gms.location.LocationListener;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
|
||||
import android.database.ContentObserver;
|
||||
import android.location.Location;
|
||||
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 {
|
||||
|
||||
/**
|
||||
* 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 location changes.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
@VisibleForTesting
|
||||
class CurrentLocationListener implements LocationListener {
|
||||
@Override
|
||||
public void onLocationChanged(Location location) {
|
||||
if (!dataSource.isAllowed()) {
|
||||
return;
|
||||
}
|
||||
dataSourceListener.notifyLocationChanged(location);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 CurrentLocationListener currentLocationListener;
|
||||
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();
|
||||
currentLocationListener = new CurrentLocationListener();
|
||||
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 SELECTED_TRACK:
|
||||
// Do nothing
|
||||
break;
|
||||
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 LOCATION:
|
||||
if (dataSource.isGpsProviderEnabled()) {
|
||||
dataSourceListener.notifyLocationProviderEnabled(true);
|
||||
} else {
|
||||
dataSourceListener.notifyLocationProviderEnabled(false);
|
||||
}
|
||||
if (dataSource.isAllowed()) {
|
||||
dataSource.registerLocationListener(currentLocationListener);
|
||||
} else {
|
||||
dataSource.unregisterLocationListener(currentLocationListener);
|
||||
}
|
||||
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 SELECTED_TRACK:
|
||||
// Do nothing
|
||||
break;
|
||||
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 LOCATION:
|
||||
dataSource.unregisterLocationListener(currentLocationListener);
|
||||
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,275 @@
|
||||
/*
|
||||
* 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 android.util.Pair;
|
||||
|
||||
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
|
||||
Pair<Double, Double> averageSpeed = writeSpeed(
|
||||
stats.getAverageSpeed(), builder, R.string.description_average_speed, lineBreak);
|
||||
|
||||
// Average moving speed
|
||||
Pair<Double, Double> averageMovingSpeed = writeSpeed(stats.getAverageMovingSpeed(), builder,
|
||||
R.string.description_average_moving_speed, lineBreak);
|
||||
|
||||
// Max speed
|
||||
Pair<Double, Double> maxSpeed = writeSpeed(
|
||||
stats.getMaxSpeed(), builder, R.string.description_max_speed, lineBreak);
|
||||
|
||||
// Average pace
|
||||
writePace(averageSpeed, builder, R.string.description_average_pace, lineBreak);
|
||||
|
||||
// Average moving pace
|
||||
writePace(averageMovingSpeed, builder, R.string.description_average_moving_pace, lineBreak);
|
||||
|
||||
// Fastest pace
|
||||
writePace(maxSpeed, builder, R.string.description_fastest_pace, 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
|
||||
* @return a pair of speed, first in kilometers per hour, second in miles per
|
||||
* hour.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
Pair<Double, Double> 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);
|
||||
return Pair.create(speedInKmHr, speedInMiHr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes pace.
|
||||
*
|
||||
* @param speed a pair of speed, first in kilometers per hour, second in miles
|
||||
* per hour
|
||||
* @param builder StringBuilder to append pace
|
||||
* @param resId resource id of pace string
|
||||
* @param lineBreak line break string
|
||||
*/
|
||||
@VisibleForTesting
|
||||
void writePace(
|
||||
Pair<Double, Double> speed, StringBuilder builder, int resId, String lineBreak) {
|
||||
double paceInMinKm = getPace(speed.first);
|
||||
double paceInMinMi = getPace(speed.second);
|
||||
builder.append(context.getString(resId, paceInMinKm, paceInMinMi));
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets pace (in minutes) from speed.
|
||||
*
|
||||
* @param speed speed in hours
|
||||
*/
|
||||
@VisibleForTesting
|
||||
double getPace(double speed) {
|
||||
return speed == 0 ? 0.0 : 60.0 / speed; // convert from hours to minutes
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,538 @@
|
||||
/*
|
||||
* 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.io.sync.SyncUtils;
|
||||
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.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.Process;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* 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();
|
||||
private static final int DATABASE_VERSION = 21;
|
||||
|
||||
@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 points SENSOR column
|
||||
if (oldVersion <= 17) {
|
||||
Log.w(TAG, "Upgrade DB: Adding sensor column.");
|
||||
db.execSQL("ALTER TABLE " + TrackPointsColumns.TABLE_NAME + " ADD "
|
||||
+ TrackPointsColumns.SENSOR + " BLOB");
|
||||
}
|
||||
|
||||
// Add tracks TABLEID column
|
||||
if (oldVersion <= 18) {
|
||||
Log.w(TAG, "Upgrade DB: Adding tableid column.");
|
||||
db.execSQL("ALTER TABLE " + TracksColumns.TABLE_NAME + " ADD " + TracksColumns.TABLEID
|
||||
+ " STRING");
|
||||
}
|
||||
|
||||
// Add tracks ICON column
|
||||
if (oldVersion <= 19) {
|
||||
Log.w(TAG, "Upgrade DB: Adding 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 driveid column.");
|
||||
db.execSQL("ALTER TABLE " + TracksColumns.TABLE_NAME + " ADD " + TracksColumns.DRIVEID
|
||||
+ " STRING");
|
||||
Log.w(TAG, "Upgrade DB: Adding modifiedtime column.");
|
||||
db.execSQL("ALTER TABLE " + TracksColumns.TABLE_NAME + " ADD "
|
||||
+ TracksColumns.MODIFIEDTIME + " INTEGER");
|
||||
Log.w(TAG, "Upgrade DB: Adding sharedwithme column.");
|
||||
db.execSQL("ALTER TABLE " + TracksColumns.TABLE_NAME + " ADD "
|
||||
+ TracksColumns.SHAREDWITHME + " INTEGER");
|
||||
Log.w(TAG, "Upgrade DB: Adding sharedowner column.");
|
||||
db.execSQL("ALTER TABLE " + TracksColumns.TABLE_NAME + " ADD " + TracksColumns.SHAREDOWNER
|
||||
+ " STRING");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
boolean driveSync = false;
|
||||
String driveIds = "";
|
||||
if (table.equals(TracksColumns.TABLE_NAME)) {
|
||||
driveSync = PreferencesUtils.getBoolean(
|
||||
getContext(), R.string.drive_sync_key, PreferencesUtils.DRIVE_SYNC_DEFAULT);
|
||||
if (driveSync) {
|
||||
driveIds = where != null ? getDriveIds(null, where, selectionArgs)
|
||||
: getDriveIds(
|
||||
new String[] { TracksColumns.DRIVEID }, SyncUtils.DRIVE_ID_TRACKS_QUERY, null);
|
||||
}
|
||||
}
|
||||
|
||||
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 (driveSync && table.equals(TracksColumns.TABLE_NAME)) {
|
||||
String driveDeletedList = PreferencesUtils.getString(getContext(),
|
||||
R.string.drive_deleted_list_key, PreferencesUtils.DRIVE_DELETED_LIST_DEFAULT);
|
||||
if (driveDeletedList.equals(PreferencesUtils.DRIVE_DELETED_LIST_DEFAULT)) {
|
||||
driveDeletedList = driveIds;
|
||||
} else {
|
||||
driveDeletedList += ";" + driveIds;
|
||||
}
|
||||
PreferencesUtils.setString(getContext(), R.string.drive_deleted_list_key, driveDeletedList);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of dirve ids.
|
||||
*
|
||||
* @param projection the projection
|
||||
* @param where where
|
||||
* @param selectionArgs selection args
|
||||
*/
|
||||
private String getDriveIds(String[] projection, String where, String[] selectionArgs) {
|
||||
ArrayList<String> driveIds = new ArrayList<String>();
|
||||
Cursor cursor = null;
|
||||
try {
|
||||
cursor = query(TracksColumns.CONTENT_URI, projection, where, selectionArgs, null);
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
int index = cursor.getColumnIndex(TracksColumns.DRIVEID);
|
||||
do {
|
||||
String driveId = cursor.getString(index);
|
||||
if (driveId != null && !driveId.equals("")) {
|
||||
driveIds.add(driveId);
|
||||
}
|
||||
} while (cursor.moveToNext());
|
||||
}
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
return TextUtils.join(";", driveIds);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,409 @@
|
||||
/*
|
||||
* 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 = (query.currentTimestamp - timestamp) / (60L * 60L * 1000L);
|
||||
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,786 @@
|
||||
/*
|
||||
* 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 static com.google.android.apps.mytracks.Constants.MAX_DISPLAYED_WAYPOINTS_POINTS;
|
||||
import static com.google.android.apps.mytracks.Constants.TARGET_DISPLAYED_TRACK_POINTS;
|
||||
|
||||
import com.google.android.apps.mytracks.Constants;
|
||||
import com.google.android.apps.mytracks.content.MyTracksProviderUtils.LocationIterator;
|
||||
import com.google.android.apps.mytracks.content.TrackDataListener.LocationState;
|
||||
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.hardware.GeomagneticField;
|
||||
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 age of a location to be considered current.
|
||||
*/
|
||||
private static final long MAX_LOCATION_AGE_MS = 60 * 1000; // 1 minute
|
||||
|
||||
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 minRequiredAccuracy;
|
||||
private int minRecordingDistance;
|
||||
|
||||
// Location values
|
||||
private Location lastSeenLocation = null;
|
||||
private boolean hasProviderEnabled = true;
|
||||
private boolean hasFix = false;
|
||||
private boolean hasGoodFix = false;
|
||||
|
||||
// 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() {
|
||||
dataSourceManager.updateListeners(trackDataManager.getRegisteredTrackDataTypes());
|
||||
loadDataForAll();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops.
|
||||
*/
|
||||
public void stop() {
|
||||
if (!started) {
|
||||
Log.i(TAG, "TrackDataHub not started, ignoring stop.");
|
||||
return;
|
||||
}
|
||||
started = false;
|
||||
|
||||
dataSourceManager.unregisterAllListeners();
|
||||
handlerThread.getLooper().quit();
|
||||
|
||||
handlerThread = null;
|
||||
handler = null;
|
||||
dataSource.close();
|
||||
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;
|
||||
PreferencesUtils.setLong(context, R.string.selected_track_id_key, selectedTrackId);
|
||||
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);
|
||||
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);
|
||||
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.selected_track_id_key))) {
|
||||
long trackId = PreferencesUtils.getLong(context, R.string.selected_track_id_key);
|
||||
boolean hasChanged = trackId != selectedTrackId;
|
||||
selectedTrackId = trackId;
|
||||
if (key != null) {
|
||||
if (hasChanged) {
|
||||
loadDataForAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
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.metric_units_key))) {
|
||||
metricUnits = PreferencesUtils.getBoolean(
|
||||
context, R.string.metric_units_key, PreferencesUtils.METRIC_UNITS_DEFAULT);
|
||||
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.report_speed_key))) {
|
||||
reportSpeed = PreferencesUtils.getBoolean(
|
||||
context, R.string.report_speed_key, PreferencesUtils.REPORT_SPEED_DEFAULT);
|
||||
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.min_required_accuracy_key))) {
|
||||
minRequiredAccuracy = PreferencesUtils.getInt(context, R.string.min_required_accuracy_key,
|
||||
PreferencesUtils.MIN_REQUIRED_ACCURACY_DEFAULT);
|
||||
if (key != null) {
|
||||
for (TrackDataListener trackDataListener :
|
||||
trackDataManager.getListeners(TrackDataType.PREFERENCE)) {
|
||||
if (trackDataListener.onMinRequiredAccuracy(minRequiredAccuracy)) {
|
||||
loadDataForListener(trackDataListener);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (key == null
|
||||
|| key.equals(PreferencesUtils.getKey(context, R.string.min_recording_distance_key))) {
|
||||
minRecordingDistance = PreferencesUtils.getInt(
|
||||
context, R.string.min_recording_distance_key,
|
||||
PreferencesUtils.MIN_RECORDING_DISTANCE_DEFAULT);
|
||||
if (key != null) {
|
||||
for (TrackDataListener trackDataListener :
|
||||
trackDataManager.getListeners(TrackDataType.PREFERENCE)) {
|
||||
if (trackDataListener.onMinRecordingDistanceChanged(minRecordingDistance)) {
|
||||
loadDataForListener(trackDataListener);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void notifyLocationProviderEnabled(final boolean enabled) {
|
||||
runInHanderThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
hasProviderEnabled = enabled;
|
||||
notifyLocationStateChanged(trackDataManager.getListeners(TrackDataType.LOCATION));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void notifyLocationChanged(final Location location) {
|
||||
runInHanderThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
notifyLocationChanged(
|
||||
location, false, trackDataManager.getListeners(TrackDataType.LOCATION));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.onMinRequiredAccuracy(minRequiredAccuracy);
|
||||
trackDataListener.onMinRecordingDistanceChanged(minRecordingDistance);
|
||||
}
|
||||
|
||||
notifySelectedTrackChanged(trackDataManager.getListeners(TrackDataType.SELECTED_TRACK));
|
||||
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));
|
||||
|
||||
if (lastSeenLocation != null) {
|
||||
notifyLocationChanged(
|
||||
lastSeenLocation, true, trackDataManager.getListeners(TrackDataType.LOCATION));
|
||||
} else {
|
||||
notifyLocationStateChanged(trackDataManager.getListeners(TrackDataType.LOCATION));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.onMinRequiredAccuracy(minRequiredAccuracy);
|
||||
trackDataListener.onMinRecordingDistanceChanged(minRecordingDistance);
|
||||
}
|
||||
|
||||
if (trackDataTypes.contains(TrackDataType.SELECTED_TRACK)) {
|
||||
notifySelectedTrackChanged(trackDataListeners);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
if (trackDataTypes.contains(TrackDataType.LOCATION)) {
|
||||
if (lastSeenLocation != null) {
|
||||
notifyLocationChanged(lastSeenLocation, true, trackDataListeners);
|
||||
} else {
|
||||
notifyLocationStateChanged(trackDataListeners);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies selected track changed. To be run in the {@link #handler} thread.
|
||||
*
|
||||
* @param trackDataListeners the track data listeners to notify
|
||||
*/
|
||||
private void notifySelectedTrackChanged(Set<TrackDataListener> trackDataListeners) {
|
||||
if (trackDataListeners.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Track track = myTracksProviderUtils.getTrack(selectedTrackId);
|
||||
for (TrackDataListener trackDataListener : trackDataListeners) {
|
||||
trackDataListener.onSelectedTrackChanged(track);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 Constants#MAX_DISPLAYED_WAYPOINTS_POINTS}. 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_POINTS);
|
||||
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;
|
||||
LocationIterator iterator = myTracksProviderUtils.getTrackPointLocationIterator(selectedTrackId,
|
||||
localLastSeenLocationId + 1, false, MyTracksProviderUtils.DEFAULT_LOCATION_FACTORY);
|
||||
boolean includeNextPoint = false;
|
||||
while (iterator.hasNext()) {
|
||||
Location location = iterator.next();
|
||||
long locationId = iterator.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;
|
||||
}
|
||||
iterator.close();
|
||||
|
||||
if (updateSamplingState) {
|
||||
numLoadedPoints = localNumLoadedPoints;
|
||||
firstSeenLocationId = localFirstSeenLocationId;
|
||||
lastSeenLocationId = localLastSeenLocationId;
|
||||
}
|
||||
|
||||
for (TrackDataListener listener : sampledInListeners) {
|
||||
listener.onNewTrackPointsDone();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies location state changed. To be run in the {@link #handler} thread.
|
||||
*
|
||||
* @param trackDataListeners the track data listeners to notify
|
||||
*/
|
||||
private void notifyLocationStateChanged(Set<TrackDataListener> trackDataListeners) {
|
||||
if (trackDataListeners.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
TrackDataListener.LocationState locationState;
|
||||
if (!hasProviderEnabled) {
|
||||
locationState = LocationState.DISABLED;
|
||||
lastSeenLocation = null;
|
||||
} else if (!hasFix) {
|
||||
locationState = LocationState.NO_FIX;
|
||||
} else if (!hasGoodFix) {
|
||||
locationState = LocationState.BAD_FIX;
|
||||
} else {
|
||||
locationState = LocationState.GOOD_FIX;
|
||||
}
|
||||
for (TrackDataListener trackDataListener : trackDataListeners) {
|
||||
trackDataListener.onLocationStateChanged(locationState);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies location changed. To be run in the {@link #handler} thread.
|
||||
*
|
||||
* @param location the location
|
||||
* @param notifyLocationStateChange true to always notify location state
|
||||
* change
|
||||
* @param trackDataListeners the track data listeners to notify
|
||||
*/
|
||||
private void notifyLocationChanged(Location location, boolean notifyLocationStateChange,
|
||||
Set<TrackDataListener> trackDataListeners) {
|
||||
if (location == null) {
|
||||
return;
|
||||
}
|
||||
boolean oldHasFix = hasFix;
|
||||
boolean oldHasGoodFix = hasGoodFix;
|
||||
|
||||
hasFix = !isLocationOld(location, System.currentTimeMillis(), MAX_LOCATION_AGE_MS);
|
||||
hasGoodFix = location.getAccuracy() <= minRequiredAccuracy;
|
||||
lastSeenLocation = location;
|
||||
|
||||
if (trackDataListeners.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (notifyLocationStateChange || hasFix != oldHasFix || hasGoodFix != oldHasGoodFix) {
|
||||
notifyLocationStateChanged(trackDataListeners);
|
||||
}
|
||||
|
||||
for (TrackDataListener trackDataListener : trackDataListeners) {
|
||||
trackDataListener.onLocationChanged(lastSeenLocation);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the track points sampling states.
|
||||
*/
|
||||
private void resetSamplingState() {
|
||||
numLoadedPoints = 0;
|
||||
firstSeenLocationId = -1L;
|
||||
lastSeenLocationId = -1L;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if a location is invalid or too old.
|
||||
*
|
||||
* @param location the location
|
||||
* @param now the current time
|
||||
* @param maxAge the maximum age
|
||||
*/
|
||||
private boolean isLocationOld(Location location, long now, long maxAge) {
|
||||
return !LocationUtils.isValidLocation(location) || (now - location.getTime() > maxAge);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link DataSource}.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
protected DataSource newDataSource() {
|
||||
return new DataSource(context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a declination.
|
||||
*
|
||||
* @param location the location
|
||||
* @param time the time
|
||||
*/
|
||||
@VisibleForTesting
|
||||
protected float getDeclination(Location location, long time) {
|
||||
GeomagneticField field = new GeomagneticField((float) location.getLatitude(), (float) location
|
||||
.getLongitude(), (float) location.getAltitude(), time);
|
||||
return field.getDeclination();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
@VisibleForTesting
|
||||
long getSelectedTrackId() {
|
||||
return selectedTrackId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the minRequiredAccuracy.
|
||||
*
|
||||
* @return the minRequiredAccuracy
|
||||
*/
|
||||
@VisibleForTesting
|
||||
int getMinRequiredAccuracy() {
|
||||
return minRequiredAccuracy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the metricUnits.
|
||||
*
|
||||
* @return the metricUnits
|
||||
*/
|
||||
@VisibleForTesting
|
||||
boolean isMetricUnits() {
|
||||
return metricUnits;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the reportSpeed.
|
||||
*
|
||||
* @return the reportSpeed
|
||||
*/
|
||||
@VisibleForTesting
|
||||
boolean isReportSpeed() {
|
||||
return reportSpeed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the value of lastSeenLocation.
|
||||
*
|
||||
* @param lastSeenLocation value of lastSeenLocation
|
||||
*/
|
||||
@VisibleForTesting
|
||||
void setLastSeenLocation(Location lastSeenLocation) {
|
||||
this.lastSeenLocation = lastSeenLocation;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
* 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 location state changes.
|
||||
*/
|
||||
public void onLocationStateChanged(LocationState locationState);
|
||||
|
||||
/**
|
||||
* Called when the location changes. This is meant for location display only,
|
||||
* track point data is reported with other methods like
|
||||
* {@link #onSampledInTrackPoint(Location)} and
|
||||
* {@link #onSampledOutTrackPoint(Location)}.
|
||||
*
|
||||
* @param location the location
|
||||
*/
|
||||
public void onLocationChanged(Location location);
|
||||
|
||||
/**
|
||||
* Called when the selected track changes. This will be followed by calls to
|
||||
* data methods such as {@link #onTrackUpdated(Track)},
|
||||
* {@link #clearTrackPoints()}, {@link #onSampledInTrackPoint(Location)},
|
||||
* etc., even if no track is currently selected (in which case you'll only get
|
||||
* calls to clear the current data).
|
||||
*
|
||||
* @param track the selected track or null if no track is selected
|
||||
*/
|
||||
public void onSelectedTrackChanged(Track track);
|
||||
|
||||
/**
|
||||
* 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 min required accuracy preference value is changed.
|
||||
*
|
||||
* @param minRequiredAccuracy the new value
|
||||
* @return true to reload all the data, false otherwise
|
||||
*/
|
||||
public boolean onMinRequiredAccuracy(int minRequiredAccuracy);
|
||||
|
||||
/**
|
||||
* Called when the min recording distance preference value is changed.
|
||||
*
|
||||
* @param minRecordingDistance the new value
|
||||
* @return true to reload all the data, false otherwise.
|
||||
*/
|
||||
public boolean onMinRecordingDistanceChanged(int minRecordingDistance);
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
* 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 static com.google.android.apps.mytracks.Constants.TAG;
|
||||
|
||||
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 {
|
||||
|
||||
// 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);
|
||||
}
|
||||
// Always include preference
|
||||
types.add(TrackDataType.PREFERENCE);
|
||||
return types;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* 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 {
|
||||
SELECTED_TRACK, // selected track changes
|
||||
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
|
||||
LOCATION, // location changes
|
||||
PREFERENCE // preference changes
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* 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.SystemUtils;
|
||||
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.support.v4.app.DialogFragment;
|
||||
import android.support.v4.app.FragmentActivity;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
/**
|
||||
* A DialogFragment to show information about My Tracks.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class AboutDialogFragment extends DialogFragment {
|
||||
|
||||
/**
|
||||
* Interface for caller of this dialog fragment.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public interface AboutCaller {
|
||||
|
||||
/**
|
||||
* Called when about license is invoked.
|
||||
*/
|
||||
public void onAboutLicense();
|
||||
}
|
||||
|
||||
public static final String ABOUT_DIALOG_TAG = "aboutDialog";
|
||||
|
||||
private AboutCaller caller;
|
||||
private FragmentActivity fragmentActivity;
|
||||
|
||||
@Override
|
||||
public void onAttach(Activity activity) {
|
||||
super.onAttach(activity);
|
||||
try {
|
||||
caller = (AboutCaller) activity;
|
||||
} catch (ClassCastException e) {
|
||||
throw new ClassCastException(
|
||||
activity.toString() + " must implement " + AboutCaller.class.getSimpleName());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
fragmentActivity = getActivity();
|
||||
View view = fragmentActivity.getLayoutInflater().inflate(R.layout.about, null);
|
||||
TextView aboutVersion = (TextView) view.findViewById(R.id.about_version);
|
||||
aboutVersion.setText(SystemUtils.getMyTracksVersion(fragmentActivity));
|
||||
return new AlertDialog.Builder(fragmentActivity).setNegativeButton(
|
||||
R.string.about_license, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
caller.onAboutLicense();
|
||||
}
|
||||
}).setPositiveButton(R.string.generic_ok, null).setTitle(R.string.help_about).setView(view)
|
||||
.create();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
* 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.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.provider.ContactsContract;
|
||||
import android.support.v4.app.DialogFragment;
|
||||
import android.support.v4.app.FragmentActivity;
|
||||
import android.support.v4.widget.SimpleCursorAdapter;
|
||||
import android.view.View;
|
||||
import android.widget.FilterQueryProvider;
|
||||
import android.widget.MultiAutoCompleteTextView;
|
||||
|
||||
/**
|
||||
* A DialogFragment to add emails.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class AddEmailsDialogFragment extends DialogFragment {
|
||||
|
||||
/**
|
||||
* Interface for caller of this dialog fragment.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public interface AddEmailsCaller {
|
||||
|
||||
/**
|
||||
* Called when add emails is done.
|
||||
*
|
||||
* @param emails the added emails
|
||||
*/
|
||||
public void onAddEmailsDone(String emails);
|
||||
}
|
||||
|
||||
public static final String ADD_EMAILS_DIALOG_TAG = "addEmailsDialog";
|
||||
|
||||
private static final String KEY_TRACK_ID = "trackId";
|
||||
|
||||
public static AddEmailsDialogFragment newInstance(long trackId) {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putLong(KEY_TRACK_ID, trackId);
|
||||
|
||||
AddEmailsDialogFragment addPeopleDialogFragment = new AddEmailsDialogFragment();
|
||||
addPeopleDialogFragment.setArguments(bundle);
|
||||
return addPeopleDialogFragment;
|
||||
}
|
||||
|
||||
private AddEmailsCaller caller;
|
||||
private FragmentActivity fragmentActivity;
|
||||
private MultiAutoCompleteTextView multiAutoCompleteTextView;
|
||||
|
||||
@Override
|
||||
public void onAttach(Activity activity) {
|
||||
super.onAttach(activity);
|
||||
try {
|
||||
caller = (AddEmailsCaller) activity;
|
||||
} catch (ClassCastException e) {
|
||||
throw new ClassCastException(
|
||||
activity.toString() + " must implement " + AddEmailsCaller.class.getSimpleName());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
fragmentActivity = getActivity();
|
||||
View view = fragmentActivity.getLayoutInflater().inflate(R.layout.add_emails, null);
|
||||
multiAutoCompleteTextView = (MultiAutoCompleteTextView) view.findViewById(R.id.add_emails);
|
||||
multiAutoCompleteTextView.setTokenizer(new MultiAutoCompleteTextView.CommaTokenizer());
|
||||
|
||||
SimpleCursorAdapter adapter = new SimpleCursorAdapter(fragmentActivity,
|
||||
R.layout.add_emails_item, getCursor(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 getCursor(fragmentActivity, constraint);
|
||||
}
|
||||
});
|
||||
multiAutoCompleteTextView.setAdapter(adapter);
|
||||
|
||||
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) {
|
||||
String acl = multiAutoCompleteTextView.getText().toString();
|
||||
caller.onAddEmailsDone(acl);
|
||||
}
|
||||
}).setTitle(R.string.share_track_add_emails_title).setView(view).create();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the cursor
|
||||
*
|
||||
* @param activity the activity
|
||||
* @param constraint the constraint
|
||||
*/
|
||||
private Cursor getCursor(Activity activity, 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 activity.getContentResolver().query(uri, projection, selection, null, order);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,540 @@
|
||||
/*
|
||||
* 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.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.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 = PreferencesUtils.METRIC_UNITS_DEFAULT;
|
||||
private boolean reportSpeed = PreferencesUtils.REPORT_SPEED_DEFAULT;
|
||||
private int minRecordingDistance = PreferencesUtils.MIN_RECORDING_DISTANCE_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 onLocationStateChanged(LocationState state) {
|
||||
// We don't care.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLocationChanged(Location loc) {
|
||||
// We don't care.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSelectedTrackChanged(Track track) {
|
||||
// We don't care.
|
||||
}
|
||||
|
||||
@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();
|
||||
getActivity().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();
|
||||
getActivity().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()) {
|
||||
getActivity().runOnUiThread(updateChart);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMetricUnitsChanged(boolean metric) {
|
||||
if (isResumed()) {
|
||||
if (metricUnits == metric) {
|
||||
return false;
|
||||
}
|
||||
metricUnits = metric;
|
||||
chartView.setMetricUnits(metricUnits);
|
||||
getActivity().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);
|
||||
getActivity().runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (isResumed()) {
|
||||
chartView.requestLayout();
|
||||
}
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMinRequiredAccuracy(int minRequiredAccuracy) {
|
||||
// We don't care.
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMinRecordingDistanceChanged(int value) {
|
||||
if (isResumed()) {
|
||||
if (minRecordingDistance == value) {
|
||||
return false;
|
||||
}
|
||||
minRecordingDistance = value;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the chart settings.
|
||||
*/
|
||||
private void checkChartSettings() {
|
||||
boolean needUpdate = false;
|
||||
if (chartByDistance != PreferencesUtils.getBoolean(getActivity(),
|
||||
R.string.chart_by_distance_key, PreferencesUtils.CHART_BY_DISTANCE_DEFAULT)) {
|
||||
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());
|
||||
}
|
||||
|
||||
/**
|
||||
* 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, minRecordingDistance);
|
||||
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,126 @@
|
||||
/*
|
||||
* 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.sendtogoogle.SendToGoogleUtils;
|
||||
import com.google.android.gms.auth.GoogleAuthException;
|
||||
import com.google.android.gms.auth.UserRecoverableAuthException;
|
||||
import com.google.api.client.googleapis.extensions.android.gms.auth.UserRecoverableAuthIOException;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* A non UI fragment to check permission.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class CheckPermissionFragment extends Fragment {
|
||||
|
||||
/**
|
||||
* Interface for caller of this fragment.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public interface CheckPermissionCaller {
|
||||
|
||||
/**
|
||||
* Called when check permission is done.
|
||||
*
|
||||
* @param scope the permission scope
|
||||
* @param success true if success
|
||||
* @param intent if not success, intent to prompt for permission
|
||||
*/
|
||||
public void onCheckPermissionDone(String scope, boolean success, Intent intent);
|
||||
}
|
||||
|
||||
public static final String CHECK_PERMISSION_TAG = "checkPermission";
|
||||
|
||||
private static final String TAG = CheckPermissionFragment.class.getSimpleName();
|
||||
private static final String KEY_ACCOUNT_NAME = "accountName";
|
||||
private static final String KEY_SCOPE = "scope";
|
||||
|
||||
private CheckPermissionCaller caller;
|
||||
|
||||
public static CheckPermissionFragment newInstance(String accountName, String scope) {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString(KEY_ACCOUNT_NAME, accountName);
|
||||
bundle.putString(KEY_SCOPE, scope);
|
||||
|
||||
CheckPermissionFragment checkPermissionFragment = new CheckPermissionFragment();
|
||||
checkPermissionFragment.setArguments(bundle);
|
||||
return checkPermissionFragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Activity activity) {
|
||||
super.onAttach(activity);
|
||||
try {
|
||||
caller = (CheckPermissionCaller) activity;
|
||||
} catch (ClassCastException e) {
|
||||
throw new ClassCastException(
|
||||
activity.toString() + " must implement " + CheckPermissionCaller.class.getSimpleName());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
// Only want one instance
|
||||
setRetainInstance(true);
|
||||
|
||||
Thread thread = new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
String accountName = getArguments().getString(KEY_ACCOUNT_NAME);
|
||||
String scope = getArguments().getString(KEY_SCOPE);
|
||||
try {
|
||||
SendToGoogleUtils.getGoogleAccountCredential(getActivity(), accountName, scope);
|
||||
finish(scope, true, null);
|
||||
} catch (UserRecoverableAuthException e) {
|
||||
finish(scope, false, e.getIntent());
|
||||
} catch (GoogleAuthException e) {
|
||||
Log.e(TAG, "GoogleAuthException", e);
|
||||
finish(scope, false, null);
|
||||
} catch (UserRecoverableAuthIOException e) {
|
||||
finish(scope, false, e.getIntent());
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "IOException", e);
|
||||
finish(scope, false, null);
|
||||
}
|
||||
}
|
||||
});
|
||||
thread.start();
|
||||
}
|
||||
|
||||
private void finish(final String scope, final boolean success, final Intent intent) {
|
||||
getFragmentManager().popBackStack();
|
||||
getActivity().runOnUiThread(new Runnable() {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
caller.onCheckPermissionDone(scope, success, intent);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* 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.DialogFragment;
|
||||
import android.support.v4.app.FragmentActivity;
|
||||
|
||||
/**
|
||||
* A DialogFragment to choose an account.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class ChooseAccountDialogFragment extends DialogFragment {
|
||||
|
||||
/**
|
||||
* Interface for caller of this dialog fragment.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public interface ChooseAccountCaller {
|
||||
|
||||
/**
|
||||
* Called when choose account is done.
|
||||
*/
|
||||
public void onChooseAccountDone();
|
||||
}
|
||||
|
||||
public static final String CHOOSE_ACCOUNT_DIALOG_TAG = "chooseAccount";
|
||||
|
||||
private ChooseAccountCaller caller;
|
||||
private FragmentActivity fragmentActivity;
|
||||
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 = getActivity();
|
||||
accounts = AccountManager.get(fragmentActivity).getAccountsByType(Constants.ACCOUNT_TYPE);
|
||||
|
||||
if (accounts.length == 1) {
|
||||
PreferencesUtils.setString(fragmentActivity, R.string.google_account_key, accounts[0].name);
|
||||
dismiss();
|
||||
caller.onChooseAccountDone();
|
||||
return;
|
||||
}
|
||||
|
||||
String googleAccount = PreferencesUtils.getString(
|
||||
fragmentActivity, R.string.google_account_key, PreferencesUtils.GOOGLE_ACCOUNT_DEFAULT);
|
||||
for (int i = 0; i < accounts.length; i++) {
|
||||
if (accounts[i].name.equals(googleAccount)) {
|
||||
dismiss();
|
||||
caller.onChooseAccountDone();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
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();
|
||||
}
|
||||
String[] choices = new String[accounts.length];
|
||||
for (int i = 0; i < accounts.length; i++) {
|
||||
choices[i] = accounts[i].name;
|
||||
}
|
||||
return new AlertDialog.Builder(fragmentActivity).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.setString(
|
||||
fragmentActivity, R.string.google_account_key, accounts[position].name);
|
||||
caller.onChooseAccountDone();
|
||||
}
|
||||
}).setSingleChoiceItems(choices, 0, null)
|
||||
.setTitle(R.string.send_google_choose_account_title).create();
|
||||
}
|
||||
}
|
||||
+260
@@ -0,0 +1,260 @@
|
||||
/*
|
||||
* 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.os.Bundle;
|
||||
import android.support.v4.app.DialogFragment;
|
||||
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 DialogFragment {
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
public static final String CHOOSE_ACTIVITY_DIALOG_TAG = "chooseActivityDialog";
|
||||
|
||||
private ChooseActivityCaller caller;
|
||||
private FragmentActivity fragmentActivity;
|
||||
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
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
fragmentActivity = getActivity();
|
||||
packageManager = fragmentActivity.getPackageManager();
|
||||
List<DisplayInfo> displayInfos = getDisplayInfos();
|
||||
|
||||
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;
|
||||
if (convertView == null) {
|
||||
view = fragmentActivity.getLayoutInflater()
|
||||
.inflate(R.layout.choose_activity_list_item, parent, false);
|
||||
} else {
|
||||
view = convertView;
|
||||
}
|
||||
DisplayInfo displayInfo = getItem(position);
|
||||
TextView text1 = (TextView) view.findViewById(R.id.choose_activity_list_item_text1);
|
||||
TextView text2 = (TextView) view.findViewById(R.id.choose_activity_list_item_text2);
|
||||
ImageView icon = (ImageView) view.findViewById(R.id.choose_activity_list_item_icon);
|
||||
text1.setText(displayInfo.primaryLabel);
|
||||
if (displayInfo.secondaryLabel != null) {
|
||||
text2.setVisibility(View.VISIBLE);
|
||||
text2.setText(displayInfo.secondaryLabel);
|
||||
} else {
|
||||
text2.setVisibility(View.GONE);
|
||||
}
|
||||
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_picker_title).create();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCancel(DialogInterface dialog) {
|
||||
super.onCancel(dialog);
|
||||
caller.onChooseActivityDone(null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the display info.
|
||||
*/
|
||||
private List<DisplayInfo> getDisplayInfos() {
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+105
@@ -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.util.TrackIconUtils;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.app.Dialog;
|
||||
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 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);
|
||||
}
|
||||
|
||||
public static final String CHOOSE_ACTIVITY_TYPE_DIALOG_TAG = "chooseActivityType";
|
||||
|
||||
private ChooseActivityTypeCaller caller;
|
||||
|
||||
@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) {
|
||||
GridView gridView =
|
||||
(GridView) getActivity().getLayoutInflater().inflate(R.layout.choose_activity_type, null);
|
||||
|
||||
final List<String> iconValues = TrackIconUtils.getAllIconValues();
|
||||
List<Integer> imageIds = new ArrayList<Integer>();
|
||||
for (String iconValue : iconValues) {
|
||||
imageIds.add(TrackIconUtils.getIconDrawable(iconValue));
|
||||
}
|
||||
|
||||
Options options = new BitmapFactory.Options();
|
||||
options.inJustDecodeBounds = true;
|
||||
BitmapFactory.decodeResource(getResources(), R.drawable.track_airplane, options);
|
||||
int padding = 32;
|
||||
int width = options.outWidth + 2 * padding;
|
||||
int height = options.outHeight + 2 * padding;
|
||||
gridView.setColumnWidth(width);
|
||||
|
||||
ChooseActivityTypeImageAdapter imageAdapter =
|
||||
new ChooseActivityTypeImageAdapter(getActivity(), imageIds, width, height, padding);
|
||||
gridView.setAdapter(imageAdapter);
|
||||
gridView.setOnItemClickListener(new OnItemClickListener() {
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
|
||||
dismiss();
|
||||
caller.onChooseActivityTypeDone(iconValues.get(position));
|
||||
}
|
||||
});
|
||||
|
||||
return new AlertDialog.Builder(getActivity()).setNegativeButton(R.string.generic_cancel, null)
|
||||
.setTitle(R.string.track_edit_activity_type_hint).setView(gridView).create();
|
||||
}
|
||||
}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* 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 android.content.Context;
|
||||
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;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getView(int position, View convertView, ViewGroup parent) {
|
||||
ImageView imageView;
|
||||
if (convertView == null) {
|
||||
imageView = new ImageView(context);
|
||||
} else {
|
||||
imageView = (ImageView) convertView;
|
||||
}
|
||||
imageView.setImageResource(imageIds.get(position));
|
||||
imageView.setMinimumHeight(height);
|
||||
imageView.setMinimumWidth(width);
|
||||
imageView.setPadding(padding, padding, padding, padding);
|
||||
return imageView;
|
||||
}
|
||||
}
|
||||
+195
@@ -0,0 +1,195 @@
|
||||
/*
|
||||
* 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.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.support.v4.app.DialogFragment;
|
||||
import android.support.v4.app.FragmentActivity;
|
||||
import android.view.View;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.CompoundButton;
|
||||
import android.widget.CompoundButton.OnCheckedChangeListener;
|
||||
import android.widget.RadioButton;
|
||||
import android.widget.TableRow;
|
||||
import android.widget.Toast;
|
||||
|
||||
/**
|
||||
* A DialogFragment to choose upload service.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class ChooseUploadServiceDialogFragment extends DialogFragment {
|
||||
|
||||
/**
|
||||
* Interface for caller of this dialog fragment.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public interface ChooseUploadServiceCaller {
|
||||
|
||||
/**
|
||||
* Called when choose upload service is done.
|
||||
*/
|
||||
public void onChooseUploadServiceDone(boolean sendDrive, boolean sendMaps,
|
||||
boolean sendFusionTables, boolean sendSpreadsheets, boolean mapsExistingMap);
|
||||
}
|
||||
|
||||
public static final String CHOOSE_UPLOAD_SERVICE_DIALOG_TAG = "chooseUploadService";
|
||||
|
||||
private static final String KEY_HIDE_DRIVE = "hideDrive";
|
||||
|
||||
public static ChooseUploadServiceDialogFragment newInstance(boolean hideDrive) {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putBoolean(KEY_HIDE_DRIVE, hideDrive);
|
||||
|
||||
ChooseUploadServiceDialogFragment chooseUploadServiceDialogFragment = new ChooseUploadServiceDialogFragment();
|
||||
chooseUploadServiceDialogFragment.setArguments(bundle);
|
||||
return chooseUploadServiceDialogFragment;
|
||||
}
|
||||
|
||||
private ChooseUploadServiceCaller caller;
|
||||
private FragmentActivity fragmentActivity;
|
||||
private boolean hideDrive;
|
||||
|
||||
private CheckBox driveCheckBox;
|
||||
private CheckBox mapsCheckBox;
|
||||
private CheckBox fusionTablesCheckBox;
|
||||
private CheckBox spreadsheetsCheckBox;
|
||||
|
||||
private TableRow mapsOptionTableRow;
|
||||
private RadioButton existingMapRadioButton;
|
||||
|
||||
@Override
|
||||
public void onAttach(Activity activity) {
|
||||
super.onAttach(activity);
|
||||
try {
|
||||
caller = (ChooseUploadServiceCaller) activity;
|
||||
} catch (ClassCastException e) {
|
||||
throw new ClassCastException(activity.toString() + " must implement "
|
||||
+ ChooseUploadServiceCaller.class.getSimpleName());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
fragmentActivity = getActivity();
|
||||
hideDrive = getArguments().getBoolean(KEY_HIDE_DRIVE);
|
||||
|
||||
View view = fragmentActivity.getLayoutInflater().inflate(R.layout.choose_upload_service, null);
|
||||
|
||||
view.findViewById(R.id.choose_upload_service_drive_options)
|
||||
.setVisibility(hideDrive ? View.GONE : View.VISIBLE);
|
||||
|
||||
driveCheckBox = (CheckBox) view.findViewById(R.id.choose_upload_service_drive);
|
||||
mapsCheckBox = (CheckBox) view.findViewById(R.id.choose_upload_service_maps);
|
||||
fusionTablesCheckBox = (CheckBox) view.findViewById(R.id.choose_upload_service_fusion_tables);
|
||||
spreadsheetsCheckBox = (CheckBox) view.findViewById(R.id.choose_upload_service_spreadsheets);
|
||||
|
||||
driveCheckBox.setChecked(PreferencesUtils.getBoolean(
|
||||
fragmentActivity, R.string.send_to_drive_key, PreferencesUtils.SEND_TO_DRIVE_DEFAULT));
|
||||
mapsCheckBox.setChecked(PreferencesUtils.getBoolean(
|
||||
fragmentActivity, R.string.send_to_maps_key, PreferencesUtils.SEND_TO_MAPS_DEFAULT));
|
||||
fusionTablesCheckBox.setChecked(PreferencesUtils.getBoolean(
|
||||
fragmentActivity, R.string.send_to_fusion_tables_key,
|
||||
PreferencesUtils.SEND_TO_FUSION_TABLES_DEFAULT));
|
||||
|
||||
spreadsheetsCheckBox.setChecked(PreferencesUtils.getBoolean(
|
||||
fragmentActivity, R.string.send_to_spreadsheets_key,
|
||||
PreferencesUtils.SEND_TO_SPREADSHEETS_DEFAULT));
|
||||
|
||||
mapsCheckBox.setOnCheckedChangeListener(new OnCheckedChangeListener() {
|
||||
public void onCheckedChanged(CompoundButton button, boolean checked) {
|
||||
updateMapsOption();
|
||||
}
|
||||
});
|
||||
|
||||
mapsOptionTableRow = (TableRow) view.findViewById(R.id.choose_upload_service_maps_options);
|
||||
|
||||
RadioButton newMapRadioButton = (RadioButton) view.findViewById(
|
||||
R.id.choose_upload_service_new_map);
|
||||
boolean defaultMapPublic = PreferencesUtils.getBoolean(
|
||||
fragmentActivity, R.string.default_map_public_key, PreferencesUtils.DEFAULT_MAP_PUBLIC_DEFAULT);
|
||||
newMapRadioButton.setText(defaultMapPublic ? R.string.send_google_new_public_map
|
||||
: R.string.send_google_new_unlisted_map);
|
||||
|
||||
existingMapRadioButton = (RadioButton) view.findViewById(
|
||||
R.id.choose_upload_service_existing_map);
|
||||
|
||||
updateMapsOption();
|
||||
if (PreferencesUtils.getBoolean(
|
||||
fragmentActivity, R.string.pick_existing_map_key, PreferencesUtils.PICK_EXISTING_MAP_DEFAULT)) {
|
||||
existingMapRadioButton.setChecked(true);
|
||||
} else {
|
||||
newMapRadioButton.setChecked(true);
|
||||
}
|
||||
|
||||
// Update fusion table
|
||||
boolean defaultTablePublic = PreferencesUtils.getBoolean(
|
||||
fragmentActivity, R.string.default_table_public_key, PreferencesUtils.DEFAULT_TABLE_PUBLIC_DEFAULT);
|
||||
String text = getString(R.string.send_google_fusion_tables) + "\n"
|
||||
+ getString(defaultTablePublic ? R.string.send_google_fusion_tables_public
|
||||
: R.string.send_google_fusion_tables_private);
|
||||
fusionTablesCheckBox.setText(text);
|
||||
|
||||
return new AlertDialog.Builder(fragmentActivity).setNegativeButton(R.string.generic_cancel, null)
|
||||
.setPositiveButton(R.string.send_google_send_now, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
PreferencesUtils.setBoolean(
|
||||
fragmentActivity, R.string.send_to_drive_key, driveCheckBox.isChecked());
|
||||
PreferencesUtils.setBoolean(
|
||||
fragmentActivity, R.string.pick_existing_map_key, existingMapRadioButton.isChecked());
|
||||
PreferencesUtils.setBoolean(
|
||||
fragmentActivity, R.string.send_to_maps_key, mapsCheckBox.isChecked());
|
||||
PreferencesUtils.setBoolean(
|
||||
fragmentActivity, R.string.send_to_fusion_tables_key, fusionTablesCheckBox.isChecked());
|
||||
PreferencesUtils.setBoolean(
|
||||
fragmentActivity, R.string.send_to_spreadsheets_key, spreadsheetsCheckBox.isChecked());
|
||||
if (sendDrive() || mapsCheckBox.isChecked() || fusionTablesCheckBox.isChecked()
|
||||
|| spreadsheetsCheckBox.isChecked()) {
|
||||
caller.onChooseUploadServiceDone(sendDrive(), mapsCheckBox.isChecked(),
|
||||
fusionTablesCheckBox.isChecked(), spreadsheetsCheckBox.isChecked(),
|
||||
existingMapRadioButton.isChecked());
|
||||
} else {
|
||||
Toast.makeText(fragmentActivity, R.string.send_google_no_service_selected, Toast.LENGTH_LONG)
|
||||
.show();
|
||||
}
|
||||
}
|
||||
}).setTitle(R.string.send_google_title).setView(view).create();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates map option.
|
||||
*/
|
||||
private void updateMapsOption() {
|
||||
mapsOptionTableRow.setVisibility(mapsCheckBox.isChecked() ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
/**
|
||||
* True to send to Google Drive.
|
||||
*/
|
||||
private boolean sendDrive() {
|
||||
return !hideDrive && driveCheckBox.isChecked();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* 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.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.support.v4.app.DialogFragment;
|
||||
import android.support.v4.app.FragmentActivity;
|
||||
import android.view.View;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.TextView;
|
||||
|
||||
/**
|
||||
* A DialogFragment to confirm an action.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class ConfirmDialogFragment extends DialogFragment {
|
||||
|
||||
/**
|
||||
* Interface for caller of this dialog fragment.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public interface ConfirmCaller {
|
||||
|
||||
/**
|
||||
* Called when confirm is done.
|
||||
*/
|
||||
public void onConfirmDone(int confirmId, long[] trackIds);
|
||||
}
|
||||
|
||||
public static final String CONFIRM_DIALOG_TAG = "confirmDialog";
|
||||
|
||||
private static final String KEY_CONFIRM_ID = "confirmId";
|
||||
private static final String KEY_DEFAULT_VALUE = "defaultValue";
|
||||
private static final String KEY_MESSAGE = "message";
|
||||
private static final String KEY_TRACK_IDS = "trackIds";
|
||||
|
||||
private CheckBox checkBox;
|
||||
|
||||
public static ConfirmDialogFragment newInstance(
|
||||
int confirmId, boolean defaultValue, CharSequence message, long[] trackIds) {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putInt(KEY_CONFIRM_ID, confirmId);
|
||||
bundle.putBoolean(KEY_DEFAULT_VALUE, defaultValue);
|
||||
bundle.putCharSequence(KEY_MESSAGE, message);
|
||||
bundle.putLongArray(KEY_TRACK_IDS, trackIds);
|
||||
|
||||
ConfirmDialogFragment confirmDialogFragment = new ConfirmDialogFragment();
|
||||
confirmDialogFragment.setArguments(bundle);
|
||||
return confirmDialogFragment;
|
||||
}
|
||||
|
||||
private ConfirmCaller caller;
|
||||
private FragmentActivity fragmentActivity;
|
||||
|
||||
@Override
|
||||
public void onAttach(Activity activity) {
|
||||
super.onAttach(activity);
|
||||
try {
|
||||
caller = (ConfirmCaller) activity;
|
||||
} catch (ClassCastException e) {
|
||||
throw new ClassCastException(
|
||||
activity.toString() + " must implement " + ConfirmCaller.class.getSimpleName());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
int confirmId = getArguments().getInt(KEY_CONFIRM_ID);
|
||||
boolean defaultValue = getArguments().getBoolean(KEY_DEFAULT_VALUE);
|
||||
fragmentActivity = getActivity();
|
||||
if (!PreferencesUtils.getBoolean(fragmentActivity, confirmId, defaultValue)) {
|
||||
long[] trackIds = getArguments().getLongArray(KEY_TRACK_IDS);
|
||||
dismiss();
|
||||
caller.onConfirmDone(confirmId, trackIds);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
View view = fragmentActivity.getLayoutInflater().inflate(R.layout.confirm_dialog, null);
|
||||
TextView textView = (TextView) view.findViewById(R.id.confirm_dialog_message);
|
||||
textView.setText(getArguments().getCharSequence(KEY_MESSAGE));
|
||||
checkBox = (CheckBox) view.findViewById(R.id.confirm_dialog_check_box);
|
||||
|
||||
return new AlertDialog.Builder(fragmentActivity).setNegativeButton(R.string.generic_no, null)
|
||||
.setPositiveButton(R.string.generic_yes, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
int confirmId = getArguments().getInt(KEY_CONFIRM_ID);
|
||||
long[] trackIds = getArguments().getLongArray(KEY_TRACK_IDS);
|
||||
PreferencesUtils.setBoolean(fragmentActivity, confirmId, !checkBox.isChecked());
|
||||
caller.onConfirmDone(confirmId, trackIds);
|
||||
}
|
||||
}).setTitle(R.string.generic_confirm_title).setView(view).create();
|
||||
}
|
||||
}
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* 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.DialogFragment;
|
||||
import android.support.v4.app.FragmentActivity;
|
||||
|
||||
/**
|
||||
* A DialogFragment to delete marker.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class DeleteMarkerDialogFragment extends DialogFragment {
|
||||
|
||||
/**
|
||||
* 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
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
final FragmentActivity fragmentActivity = getActivity();
|
||||
final long[] markerIds = getArguments().getLongArray(KEY_MARKER_IDS);
|
||||
int messageId = markerIds.length > 1 ? R.string.marker_delete_multiple_confirm_message
|
||||
: R.string.marker_delete_one_confirm_message;
|
||||
return DialogUtils.createConfirmationDialog(
|
||||
fragmentActivity, messageId, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
for (long markerId : markerIds) {
|
||||
MyTracksProviderUtils.Factory.get(fragmentActivity)
|
||||
.deleteWaypoint(markerId, new DescriptionGeneratorImpl(fragmentActivity));
|
||||
}
|
||||
caller.onDeleteMarkerDone();
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
* 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.MyTracksProviderUtils;
|
||||
import com.google.android.apps.mytracks.io.sync.SyncUtils;
|
||||
import com.google.android.apps.mytracks.services.TrackRecordingServiceConnection;
|
||||
import com.google.android.apps.mytracks.util.DialogUtils;
|
||||
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.app.Activity;
|
||||
import android.app.Dialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.DialogFragment;
|
||||
import android.support.v4.app.FragmentActivity;
|
||||
|
||||
/**
|
||||
* A DialogFragment to track.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class DeleteTrackDialogFragment extends DialogFragment {
|
||||
|
||||
/**
|
||||
* Interface for caller of this dialog fragment.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public interface DeleteTrackCaller {
|
||||
|
||||
/**
|
||||
* Gets the track recording service connection.
|
||||
*/
|
||||
public TrackRecordingServiceConnection getTrackRecordingServiceConnection();
|
||||
|
||||
/**
|
||||
* Called when delete track is done.
|
||||
*/
|
||||
public void onDeleteTrackDone();
|
||||
}
|
||||
|
||||
public static final String DELETE_TRACK_DIALOG_TAG = "deleteTrackDialog";
|
||||
private static final String KEY_DELETE_ALL = "deleteAll";
|
||||
private static final String KEY_TRACK_IDS = "trackIds";
|
||||
|
||||
/**
|
||||
* Create a new instance.
|
||||
*
|
||||
* @param deleteAll true to delete all tracks
|
||||
* @param trackIds list of track ids to delete when deleteAll if false
|
||||
*/
|
||||
public static DeleteTrackDialogFragment newInstance(boolean deleteAll, long[] trackIds) {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putBoolean(KEY_DELETE_ALL, deleteAll);
|
||||
bundle.putLongArray(KEY_TRACK_IDS, trackIds);
|
||||
|
||||
DeleteTrackDialogFragment deleteTrackDialogFragment = new DeleteTrackDialogFragment();
|
||||
deleteTrackDialogFragment.setArguments(bundle);
|
||||
return deleteTrackDialogFragment;
|
||||
}
|
||||
|
||||
private DeleteTrackCaller caller;
|
||||
|
||||
@Override
|
||||
public void onAttach(Activity activity) {
|
||||
super.onAttach(activity);
|
||||
try {
|
||||
caller = (DeleteTrackCaller) activity;
|
||||
} catch (ClassCastException e) {
|
||||
throw new ClassCastException(
|
||||
activity.toString() + " must implement " + DeleteTrackCaller.class.getSimpleName());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
boolean deleteAll = getArguments().getBoolean(KEY_DELETE_ALL);
|
||||
final FragmentActivity fragmentActivity = getActivity();
|
||||
if (deleteAll) {
|
||||
return DialogUtils.createConfirmationDialog(fragmentActivity,
|
||||
R.string.track_delete_all_confirm_message, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
PreferencesUtils.setBoolean(fragmentActivity, R.string.drive_sync_key,
|
||||
PreferencesUtils.DRIVE_SYNC_DEFAULT);
|
||||
SyncUtils.disableSync(fragmentActivity);
|
||||
SyncUtils.clearSyncState(fragmentActivity);
|
||||
MyTracksProviderUtils.Factory.get(fragmentActivity).deleteAllTracks();
|
||||
caller.onDeleteTrackDone();
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
final long[] trackIds = getArguments().getLongArray(KEY_TRACK_IDS);
|
||||
int messageId = trackIds.length > 1 ? R.string.track_delete_multiple_confirm_message
|
||||
: R.string.track_delete_one_confirm_message;
|
||||
return DialogUtils.createConfirmationDialog(
|
||||
fragmentActivity, messageId, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
for (long trackId : trackIds) {
|
||||
if (trackId == PreferencesUtils.getLong(
|
||||
fragmentActivity, R.string.recording_track_id_key)) {
|
||||
TrackRecordingServiceConnectionUtils.stopRecording(
|
||||
fragmentActivity, caller.getTrackRecordingServiceConnection(), false);
|
||||
}
|
||||
}
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
for (long id : trackIds) {
|
||||
MyTracksProviderUtils.Factory.get(fragmentActivity).deleteTrack(id);
|
||||
}
|
||||
caller.onDeleteTrackDone();
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* 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.os.Bundle;
|
||||
import android.support.v4.app.DialogFragment;
|
||||
import android.support.v4.app.FragmentActivity;
|
||||
|
||||
/**
|
||||
* A DialogFrament to enable sync.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class EnableSyncDialogFragment extends DialogFragment {
|
||||
|
||||
/**
|
||||
* 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;
|
||||
private FragmentActivity fragmentActivity;
|
||||
|
||||
@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
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
fragmentActivity = getActivity();
|
||||
if (PreferencesUtils.getBoolean(
|
||||
fragmentActivity, R.string.drive_sync_key, PreferencesUtils.DRIVE_SYNC_DEFAULT)) {
|
||||
dismiss();
|
||||
caller.onEnableSyncDone(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
return new AlertDialog.Builder(fragmentActivity).setMessage(R.string.enable_sync_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) {
|
||||
/*
|
||||
* google_account_key can be set from the android backup service. If
|
||||
* the user wants to enable sync, make sure google_account_key is
|
||||
* cleared so the user can choose an account.
|
||||
*/
|
||||
PreferencesUtils.setString(fragmentActivity, R.string.google_account_key,
|
||||
PreferencesUtils.GOOGLE_ACCOUNT_DEFAULT);
|
||||
caller.onEnableSyncDone(true);
|
||||
}
|
||||
}).setTitle(R.string.enable_sync_title).create();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCancel(DialogInterface arg0) {
|
||||
caller.onEnableSyncDone(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
* 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.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.os.Bundle;
|
||||
import android.support.v4.app.DialogFragment;
|
||||
import android.support.v4.app.FragmentActivity;
|
||||
import android.view.KeyEvent;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* A DialogFragment to show EULA.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class EulaDialogFragment extends DialogFragment {
|
||||
|
||||
/**
|
||||
* 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;
|
||||
private FragmentActivity fragmentActivity;
|
||||
|
||||
@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
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
fragmentActivity = getActivity();
|
||||
|
||||
boolean hasAccepted = getArguments().getBoolean(KEY_HAS_ACCEPTED);
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(fragmentActivity).setMessage(
|
||||
getEulaText()).setTitle(R.string.eula_title);
|
||||
|
||||
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.setAcceptEula(fragmentActivity);
|
||||
PreferencesUtils.setBoolean(
|
||||
fragmentActivity, R.string.metric_units_key, !Locale.US.equals(Locale.getDefault()));
|
||||
caller.onEulaDone();
|
||||
}
|
||||
});
|
||||
}
|
||||
return builder.create();
|
||||
}
|
||||
|
||||
@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,103 @@
|
||||
/*
|
||||
* 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.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;
|
||||
import android.support.v4.app.DialogFragment;
|
||||
|
||||
/**
|
||||
* A DialogFragment to select a file type, gpx, kml, etc.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class FileTypeDialogFragment extends DialogFragment {
|
||||
|
||||
/**
|
||||
* 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
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
String fileTypes[] = getResources().getStringArray(R.array.file_types);
|
||||
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];
|
||||
for (int i = 0; i < choices.length; i++) {
|
||||
choices[i] = getString(optionId, fileTypes[i]);
|
||||
}
|
||||
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,111 @@
|
||||
/*
|
||||
* 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.DialogFragment;
|
||||
import android.support.v4.app.FragmentActivity;
|
||||
|
||||
/**
|
||||
* A DialogFragment to configure frequency.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class FrequencyDialogFragment extends DialogFragment {
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
private FragmentActivity fragmentActivity;
|
||||
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
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(fragmentActivity, preferenceId, getFrequencyValue(listIndex));
|
||||
}
|
||||
})
|
||||
.setSingleChoiceItems(getFrequencyDisplayOptions(), getListIndex(frequencyValue), null)
|
||||
.setTitle(titleId)
|
||||
.create();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the frequency display options.
|
||||
*/
|
||||
private String[] getFrequencyDisplayOptions() {
|
||||
boolean metricUnits = PreferencesUtils.getBoolean(
|
||||
fragmentActivity, R.string.metric_units_key, PreferencesUtils.METRIC_UNITS_DEFAULT);
|
||||
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]);
|
||||
}
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* 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.os.Bundle;
|
||||
import android.support.v4.app.DialogFragment;
|
||||
import android.support.v4.app.FragmentActivity;
|
||||
import android.widget.Toast;
|
||||
|
||||
/**
|
||||
* A DialogFragment to install Google Earth.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class InstallEarthDialogFragment extends DialogFragment {
|
||||
|
||||
public static final String INSTALL_EARTH_DIALOG_TAG = "installEarthDialog";
|
||||
|
||||
private FragmentActivity fragmentActivity;
|
||||
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
fragmentActivity = getActivity();
|
||||
return new AlertDialog.Builder(fragmentActivity).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(
|
||||
fragmentActivity, R.string.track_detail_install_earth_error, Toast.LENGTH_LONG)
|
||||
.show();
|
||||
}
|
||||
}
|
||||
}).setTitle(R.string.generic_confirm_title).create();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,753 @@
|
||||
/*
|
||||
* 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.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.provider.Settings;
|
||||
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.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 String ZOOM_TO_CURRENT_LOCATION_KEY = "zoom_to_current_location_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;
|
||||
|
||||
private TrackDataHub trackDataHub;
|
||||
|
||||
// Current location
|
||||
private Location currentLocation;
|
||||
|
||||
/**
|
||||
* 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>
|
||||
* Set to false when <br>
|
||||
* 1. showing a marker <br>
|
||||
* 2. user manually zooms/pans
|
||||
*/
|
||||
private boolean keepCurrentLocationVisible;
|
||||
|
||||
/**
|
||||
* True to zoom/center the current location when it is available.
|
||||
* <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 <br>
|
||||
* 3. after zooming to the current location <br>
|
||||
* <p>
|
||||
* The last one is to support the use case of zooming only once. After zoom,
|
||||
* set it to false. E.g., only zoom to the first location during a recording.
|
||||
*/
|
||||
private boolean zoomToCurrentLocation;
|
||||
|
||||
private OnLocationChangedListener onLocationChangedListener;
|
||||
|
||||
// For showing a marker
|
||||
private long markerTrackId = -1L;
|
||||
private long markerId = -1L;
|
||||
|
||||
// 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);
|
||||
|
||||
myLocationImageButton = (ImageButton) layout.findViewById(R.id.map_my_location);
|
||||
myLocationImageButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
final MyTracksLocationManager myTracksLocationManager = new MyTracksLocationManager(
|
||||
getActivity(), Looper.myLooper());
|
||||
if (!myTracksLocationManager.isAllowed()) {
|
||||
String setting = getString(
|
||||
GoogleLocationUtils.isAvailable(getActivity()) ? R.string.gps_google_location_settings
|
||||
: R.string.gps_location_access);
|
||||
Toast.makeText(
|
||||
getActivity(), getString(R.string.my_location_no_gps, setting), Toast.LENGTH_LONG)
|
||||
.show();
|
||||
myTracksLocationManager.close();
|
||||
} else {
|
||||
myTracksLocationManager.requestLastLocation(new LocationListener() {
|
||||
@Override
|
||||
public void onLocationChanged(Location location) {
|
||||
myTracksLocationManager.close();
|
||||
keepCurrentLocationVisible = true;
|
||||
zoomToCurrentLocation = true;
|
||||
setCurrentLocation(location);
|
||||
updateCurrentLocation();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
messageTextView = (TextView) layout.findViewById(R.id.map_message);
|
||||
|
||||
/*
|
||||
* At this point, after super.onCreateView, getMap will not return null and
|
||||
* we can initialize googleMap. However, onCreateView 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;
|
||||
zoomToCurrentLocation = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
googleMap.moveCamera(
|
||||
CameraUpdateFactory.newLatLngZoom(getDefaultLatLng(), googleMap.getMinZoomLevel()));
|
||||
}
|
||||
return layout;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(Bundle savedInstanceState) {
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
if (savedInstanceState != null) {
|
||||
keepCurrentLocationVisible = savedInstanceState.getBoolean(
|
||||
KEEP_CURRENT_LOCATION_VISIBLE_KEY, false);
|
||||
zoomToCurrentLocation = savedInstanceState.getBoolean(ZOOM_TO_CURRENT_LOCATION_KEY, false);
|
||||
Location location = (Location) savedInstanceState.getParcelable(CURRENT_LOCATION_KEY);
|
||||
setCurrentLocation(location);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
if (googleMap != null) {
|
||||
int mapType = PreferencesUtils.getInt(
|
||||
getActivity(), R.string.map_type_key, PreferencesUtils.MAP_TYPE_DEFAUlT);
|
||||
googleMap.setMapType(mapType);
|
||||
ApiAdapterFactory.getApiAdapter().invalidMenu(getActivity());
|
||||
}
|
||||
resumeTrackDataHub();
|
||||
updateCurrentLocation();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
if (currentLocation != null) {
|
||||
outState.putParcelable(CURRENT_LOCATION_KEY, currentLocation);
|
||||
}
|
||||
outState.putBoolean(KEEP_CURRENT_LOCATION_VISIBLE_KEY, keepCurrentLocationVisible);
|
||||
outState.putBoolean(ZOOM_TO_CURRENT_LOCATION_KEY, zoomToCurrentLocation);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
pauseTrackDataHub();
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the marker on the map.
|
||||
*
|
||||
* @param trackId the track id
|
||||
* @param id the marker id
|
||||
*/
|
||||
public void showMarker(long trackId, long id) {
|
||||
/*
|
||||
* Synchronize to prevent race condition in changing markerTrackId and
|
||||
* markerId variables.
|
||||
*/
|
||||
synchronized (this) {
|
||||
if (currentTrack != null && currentTrack.getId() == trackId) {
|
||||
showMarker(id);
|
||||
markerTrackId = -1L;
|
||||
markerId = -1L;
|
||||
return;
|
||||
}
|
||||
markerTrackId = trackId;
|
||||
markerId = id;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflator) {
|
||||
menuInflator.inflate(R.menu.map, menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPrepareOptionsMenu(Menu menu) {
|
||||
if (googleMap != null) {
|
||||
int id;
|
||||
switch (googleMap.getMapType()) {
|
||||
case GoogleMap.MAP_TYPE_NORMAL:
|
||||
id = R.id.menu_map;
|
||||
break;
|
||||
case GoogleMap.MAP_TYPE_SATELLITE:
|
||||
id = R.id.menu_satellite;
|
||||
break;
|
||||
case GoogleMap.MAP_TYPE_HYBRID:
|
||||
id = R.id.menu_satellite_with_streets;
|
||||
break;
|
||||
case GoogleMap.MAP_TYPE_TERRAIN:
|
||||
id = R.id.menu_terrain;
|
||||
break;
|
||||
default:
|
||||
id = R.id.menu_map;
|
||||
}
|
||||
MenuItem menuItem = menu.findItem(id);
|
||||
if (menuItem != null) {
|
||||
menuItem.setChecked(true);
|
||||
}
|
||||
}
|
||||
super.onPrepareOptionsMenu(menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem menuItem) {
|
||||
int type = GoogleMap.MAP_TYPE_NORMAL;
|
||||
switch (menuItem.getItemId()) {
|
||||
case R.id.menu_map:
|
||||
type = GoogleMap.MAP_TYPE_NORMAL;
|
||||
break;
|
||||
case R.id.menu_satellite:
|
||||
type = GoogleMap.MAP_TYPE_SATELLITE;
|
||||
break;
|
||||
case R.id.menu_satellite_with_streets:
|
||||
type = GoogleMap.MAP_TYPE_HYBRID;
|
||||
break;
|
||||
case R.id.menu_terrain:
|
||||
type = GoogleMap.MAP_TYPE_TERRAIN;
|
||||
break;
|
||||
default:
|
||||
return super.onOptionsItemSelected(menuItem);
|
||||
}
|
||||
if (googleMap != null) {
|
||||
googleMap.setMapType(type);
|
||||
menuItem.setChecked(true);
|
||||
PreferencesUtils.setInt(getActivity(), R.string.map_type_key, type);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLocationStateChanged(final LocationState state) {
|
||||
if (isResumed()) {
|
||||
getActivity().runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (!isResumed() || googleMap == null) {
|
||||
return;
|
||||
}
|
||||
boolean myLocationEnabled = true;
|
||||
if (state == LocationState.DISABLED) {
|
||||
setCurrentLocation(null);
|
||||
myLocationEnabled = false;
|
||||
}
|
||||
googleMap.setMyLocationEnabled(myLocationEnabled);
|
||||
|
||||
String message;
|
||||
boolean isGpsDisabled;
|
||||
if (!isSelectedTrackRecording()) {
|
||||
message = null;
|
||||
isGpsDisabled = false;
|
||||
} else {
|
||||
switch (state) {
|
||||
case DISABLED:
|
||||
String setting = getString(GoogleLocationUtils.isAvailable(getActivity())
|
||||
? R.string.gps_google_location_settings
|
||||
: R.string.gps_location_access);
|
||||
message = getString(R.string.gps_disabled, setting);
|
||||
isGpsDisabled = true;
|
||||
break;
|
||||
case NO_FIX:
|
||||
message = getString(R.string.gps_wait_for_signal);
|
||||
isGpsDisabled = false;
|
||||
break;
|
||||
case BAD_FIX:
|
||||
message = getString(R.string.gps_wait_for_better_signal);
|
||||
isGpsDisabled = false;
|
||||
break;
|
||||
case GOOD_FIX:
|
||||
message = null;
|
||||
isGpsDisabled = false;
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unexpected state: " + state);
|
||||
}
|
||||
}
|
||||
if (message == null) {
|
||||
messageTextView.setVisibility(View.GONE);
|
||||
return;
|
||||
}
|
||||
messageTextView.setText(message);
|
||||
messageTextView.setVisibility(View.VISIBLE);
|
||||
if (isGpsDisabled) {
|
||||
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) {
|
||||
Intent intent = GoogleLocationUtils.isAvailable(getActivity()) ? new Intent(
|
||||
GoogleLocationUtils.ACTION_GOOGLE_LOCATION_SETTINGS)
|
||||
: new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS);
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
startActivity(intent);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
messageTextView.setOnClickListener(null);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLocationChanged(final Location location) {
|
||||
if (isResumed()) {
|
||||
setCurrentLocation(location);
|
||||
updateCurrentLocation();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSelectedTrackChanged(final Track track) {
|
||||
if (isResumed()) {
|
||||
currentTrack = track;
|
||||
boolean hasTrack = track != null;
|
||||
if (hasTrack) {
|
||||
mapOverlay.setShowEndMarker(!isSelectedTrackRecording());
|
||||
synchronized (this) {
|
||||
/*
|
||||
* Synchronize to prevent race condition in changing markerTrackId and
|
||||
* markerId variables.
|
||||
*/
|
||||
if (track.getId() == markerTrackId) {
|
||||
// Show the marker
|
||||
showMarker(markerId);
|
||||
|
||||
markerTrackId = -1L;
|
||||
markerId = -1L;
|
||||
} else {
|
||||
// Show the track
|
||||
showTrack();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTrackUpdated(Track track) {
|
||||
// We don't care.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearTrackPoints() {
|
||||
if (isResumed()) {
|
||||
mapOverlay.clearPoints();
|
||||
reloadPaths = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSampledInTrackPoint(final Location location) {
|
||||
if (isResumed()) {
|
||||
mapOverlay.addLocation(location);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSampledOutTrackPoint(Location location) {
|
||||
// We don't care.
|
||||
}
|
||||
|
||||
@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) {
|
||||
mapOverlay.update(googleMap, paths, reloadPaths);
|
||||
reloadPaths = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@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) {
|
||||
mapOverlay.update(googleMap, paths, 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 onMinRequiredAccuracy(int minRequiredAccuracy) {
|
||||
// We don't care.
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMinRecordingDistanceChanged(int minRecordingDistance) {
|
||||
// We don't care.
|
||||
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.SELECTED_TRACK,
|
||||
TrackDataType.WAYPOINTS_TABLE, TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE,
|
||||
TrackDataType.LOCATION));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
private void setCurrentLocation(Location location) {
|
||||
// If recording, zoom to the first location
|
||||
if (isSelectedTrackRecording() && currentLocation == null && location != null) {
|
||||
zoomToCurrentLocation = true;
|
||||
}
|
||||
currentLocation = location;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the current location and zoom to it if necessary.
|
||||
*/
|
||||
private void updateCurrentLocation() {
|
||||
getActivity().runOnUiThread(new Runnable() {
|
||||
public void run() {
|
||||
if (!isResumed() || googleMap == null || onLocationChangedListener == null
|
||||
|| currentLocation == null) {
|
||||
return;
|
||||
}
|
||||
onLocationChangedListener.onLocationChanged(currentLocation);
|
||||
if (zoomToCurrentLocation
|
||||
|| (keepCurrentLocationVisible && !isLocationVisible(currentLocation))) {
|
||||
LatLng latLng = new LatLng(currentLocation.getLatitude(), currentLocation.getLongitude());
|
||||
googleMap.animateCamera(CameraUpdateFactory.newLatLngZoom(latLng, DEFAULT_ZOOM_LEVEL));
|
||||
zoomToCurrentLocation = false;
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the camera over a track.
|
||||
*/
|
||||
private void showTrack() {
|
||||
getActivity().runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (!isResumed() || googleMap == null || currentTrack == null
|
||||
|| currentTrack.getNumberOfPoints() < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that mapView is valid.
|
||||
*/
|
||||
if (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 CameraUpdate.newLatLngBounds(LatLngBounds bounds,
|
||||
* int padding) if the map view has not undergone layout. Thus calling
|
||||
* CameraUpdate.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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the camera over a 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 && waypoint.getLocation() != null) {
|
||||
Location location = waypoint.getLocation();
|
||||
LatLng latLng = new LatLng(location.getLatitude(), location.getLongitude());
|
||||
keepCurrentLocationVisible = false;
|
||||
zoomToCurrentLocation = 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());
|
||||
Track track = myTracksProviderUtils.getLastTrack();
|
||||
if (track != null) {
|
||||
Location location = myTracksProviderUtils.getLastValidTrackPoint(track.getId());
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
/*
|
||||
* 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.LocationUtils;
|
||||
import com.google.android.apps.mytracks.util.PreferencesUtils;
|
||||
import com.google.android.apps.mytracks.util.StatsUtils;
|
||||
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.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
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";
|
||||
|
||||
private static final long MAX_LOCATION_AGE_MS = 60 * 1000; // 1 minute
|
||||
private static final int ONE_SECOND = 1000;
|
||||
|
||||
private TrackDataHub trackDataHub;
|
||||
private Handler handler;
|
||||
|
||||
private Location lastLocation = null;
|
||||
private TripStatistics lastTripStatistics = null;
|
||||
private int minRequiredAccuracy = PreferencesUtils.MIN_REQUIRED_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();
|
||||
updateUi(getActivity());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
resumeTrackDataHub();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
pauseTrackDataHub();
|
||||
handler.removeCallbacks(updateTotalTime);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLocationStateChanged(LocationState state) {
|
||||
// We don't care.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLocationChanged(final Location location) {
|
||||
// We don't care.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSelectedTrackChanged(Track track) {
|
||||
if (isResumed()) {
|
||||
handler.removeCallbacks(updateTotalTime);
|
||||
if (isSelectedTrackRecording()) {
|
||||
handler.post(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;
|
||||
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 = !isLocationOld(lastLocation);
|
||||
boolean hasGoodFix = lastLocation.getAccuracy() <= minRequiredAccuracy;
|
||||
|
||||
if (!hasFix || !hasGoodFix) {
|
||||
lastLocation = null;
|
||||
}
|
||||
}
|
||||
StatsUtils.setLocationValues(getActivity(), 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 onMinRequiredAccuracy(int newValue) {
|
||||
minRequiredAccuracy = newValue;
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMinRecordingDistanceChanged(int minRecordingDistance) {
|
||||
// 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.SELECTED_TRACK,
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if a location is old.
|
||||
*
|
||||
* @param location the location
|
||||
*/
|
||||
private boolean isLocationOld(Location location) {
|
||||
return !LocationUtils.isValidLocation(location)
|
||||
|| (System.currentTimeMillis() - location.getTime() > MAX_LOCATION_AGE_MS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the UI.
|
||||
*/
|
||||
private void updateUi(FragmentActivity activity) {
|
||||
StatsUtils.setTripStatisticsValues(activity, lastTripStatistics);
|
||||
StatsUtils.setLocationValues(activity, lastLocation, isSelectedTrackRecording());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* 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.backup;
|
||||
|
||||
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.widget.Toast;
|
||||
|
||||
/**
|
||||
* Activity to backup data to the SD card.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class BackupActivity extends Activity {
|
||||
|
||||
private static final int DIALOG_PROGRESS_ID = 0;
|
||||
|
||||
private BackupAsyncTask backupAsyncTask;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
Object retained = getLastNonConfigurationInstance();
|
||||
if (retained instanceof BackupAsyncTask) {
|
||||
backupAsyncTask = (BackupAsyncTask) retained;
|
||||
backupAsyncTask.setActivity(this);
|
||||
} else {
|
||||
backupAsyncTask = new BackupAsyncTask(this);
|
||||
backupAsyncTask.execute();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object onRetainNonConfigurationInstance() {
|
||||
backupAsyncTask.setActivity(null);
|
||||
return backupAsyncTask;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Dialog onCreateDialog(int id) {
|
||||
if (id != DIALOG_PROGRESS_ID) {
|
||||
return null;
|
||||
}
|
||||
return DialogUtils.createSpinnerProgressDialog(
|
||||
this, R.string.settings_backup_now_progress_message, new DialogInterface.OnCancelListener() {
|
||||
@Override
|
||||
public void onCancel(DialogInterface dialog) {
|
||||
finish();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes when the associated AsyncTask completes.
|
||||
*
|
||||
* @param success true if the AsyncTask is successful
|
||||
* @param messageId message id to display to user
|
||||
*/
|
||||
public void onAsyncTaskCompleted(boolean success, int messageId) {
|
||||
removeDialog(DIALOG_PROGRESS_ID);
|
||||
Toast.makeText(this, messageId, success ? Toast.LENGTH_SHORT : Toast.LENGTH_LONG).show();
|
||||
finish();
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the progress dialog.
|
||||
*/
|
||||
public void showProgressDialog() {
|
||||
showDialog(DIALOG_PROGRESS_ID);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* 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.backup;
|
||||
|
||||
import com.google.android.apps.mytracks.util.FileUtils;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.os.AsyncTask;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* AsyncTask to backup data to the SD card.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class BackupAsyncTask extends AsyncTask<Void, Integer, Boolean> {
|
||||
|
||||
private static final String TAG = BackupAsyncTask.class.getSimpleName();
|
||||
|
||||
private BackupActivity backupActivity;
|
||||
private final ExternalFileBackup externalFileBackup;
|
||||
|
||||
// true if the AsyncTask result is success
|
||||
private boolean success;
|
||||
|
||||
// true if the AsyncTask has completed
|
||||
private boolean completed;
|
||||
|
||||
// message id to return to the activity
|
||||
private int messageId;
|
||||
|
||||
/**
|
||||
* Creates an AsyncTask.
|
||||
*
|
||||
* @param backupActivity the activity currently associated with this
|
||||
* AsyncTask
|
||||
*/
|
||||
public BackupAsyncTask(BackupActivity backupActivity) {
|
||||
this.backupActivity = backupActivity;
|
||||
this.externalFileBackup = new ExternalFileBackup(backupActivity);
|
||||
success = false;
|
||||
completed = false;
|
||||
messageId = R.string.settings_backup_now_error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current {@link BackupActivity} associated with this AyncTask.
|
||||
*
|
||||
* @param activity the current {@link BackupActivity}, can be null
|
||||
*/
|
||||
public void setActivity(BackupActivity activity) {
|
||||
this.backupActivity = activity;
|
||||
if (completed && backupActivity != null) {
|
||||
backupActivity.onAsyncTaskCompleted(success, messageId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
if (backupActivity != null) {
|
||||
backupActivity.showProgressDialog();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Boolean doInBackground(Void... params) {
|
||||
if (!FileUtils.isExternalStorageWriteable()) {
|
||||
messageId = R.string.external_storage_not_writable;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!externalFileBackup.isBackupsDirectoryAvailable(true)) {
|
||||
messageId = R.string.external_storage_not_writable;
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
externalFileBackup.writeToDefaultFile();
|
||||
messageId = R.string.settings_backup_now_success;
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
Log.d(TAG, "IO exception", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Boolean result) {
|
||||
success = result;
|
||||
completed = true;
|
||||
if (backupActivity != null) {
|
||||
backupActivity.onAsyncTaskCompleted(success, messageId);
|
||||
}
|
||||
}
|
||||
}
|
||||
+40
@@ -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,248 @@
|
||||
/*
|
||||
* Copyright 2010 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package com.google.android.apps.mytracks.io.backup;
|
||||
|
||||
import com.google.android.apps.mytracks.content.ContentTypeIds;
|
||||
|
||||
import android.database.Cursor;
|
||||
import android.database.MergeCursor;
|
||||
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Database dumper which is able to write only part of the database
|
||||
* according to some query.
|
||||
*
|
||||
* This dumper is symmetrical to {@link DatabaseImporter}.
|
||||
*
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
class DatabaseDumper {
|
||||
|
||||
/** The names of the columns being dumped. */
|
||||
private final String[] columnNames;
|
||||
/** The types of the columns being dumped. */
|
||||
private final byte[] columnTypes;
|
||||
/** Whether to output null fields. */
|
||||
private final boolean outputNullFields;
|
||||
|
||||
// Temporary state
|
||||
private int[] columnIndices;
|
||||
private boolean[] hasFields;
|
||||
|
||||
public DatabaseDumper(String[] columnNames, byte[] columnTypes,
|
||||
boolean outputNullFields) {
|
||||
if (columnNames.length != columnTypes.length) {
|
||||
throw new IllegalArgumentException("Names don't match types");
|
||||
}
|
||||
|
||||
this.columnNames = columnNames;
|
||||
this.columnTypes = columnTypes;
|
||||
this.outputNullFields = outputNullFields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the header plus all rows that can be read from the given cursor.
|
||||
* This assumes the cursor will have the same column and column indices on
|
||||
* every row (and thus may not work with a {@link MergeCursor}).
|
||||
*/
|
||||
public void writeAllRows(Cursor cursor, DataOutputStream writer)
|
||||
throws IOException {
|
||||
writeHeaders(cursor, cursor.getCount(), writer);
|
||||
|
||||
if (!cursor.moveToFirst()) {
|
||||
return;
|
||||
}
|
||||
|
||||
do {
|
||||
writeOneRow(cursor, writer);
|
||||
} while (cursor.moveToNext());
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes just the headers for the data that will come from the given cursor.
|
||||
* The headers include column information and the number of rows that will be
|
||||
* written.
|
||||
*
|
||||
* @param cursor the cursor to get columns from
|
||||
* @param numRows the number of rows that will be later written
|
||||
* @param writer the output to write to
|
||||
* @throws IOException if there are errors while writing
|
||||
*/
|
||||
public void writeHeaders(Cursor cursor, int numRows, DataOutputStream writer)
|
||||
throws IOException {
|
||||
initializeCachedValues(cursor);
|
||||
writeQueryMetadata(numRows, writer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the current row from the cursor. The cursor is not advanced.
|
||||
* This must be called after {@link #writeHeaders}.
|
||||
*
|
||||
* @param cursor the cursor to write data from
|
||||
* @param writer the output to write to
|
||||
* @throws IOException if there are any errors while writing
|
||||
*/
|
||||
public void writeOneRow(Cursor cursor, DataOutputStream writer)
|
||||
throws IOException {
|
||||
if (columnIndices == null) {
|
||||
throw new IllegalStateException(
|
||||
"Cannot write rows before writing the header");
|
||||
}
|
||||
|
||||
if (columnIndices.length > Long.SIZE) {
|
||||
throw new IllegalArgumentException("Too many fields");
|
||||
}
|
||||
|
||||
// Build a bitmap of which fields are present
|
||||
long fields = 0;
|
||||
for (int i = 0; i < columnIndices.length; i++) {
|
||||
hasFields[i] = !cursor.isNull(columnIndices[i]);
|
||||
fields |= (hasFields[i] ? 1 : 0) << i;
|
||||
}
|
||||
writer.writeLong(fields);
|
||||
|
||||
// Actually write the present fields
|
||||
for (int i = 0; i < columnIndices.length; i++) {
|
||||
if (hasFields[i]) {
|
||||
writeCell(columnIndices[i], columnTypes[i], cursor, writer);
|
||||
} else if (outputNullFields) {
|
||||
writeDummyCell(columnTypes[i], writer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the column indices and other temporary state for reading from
|
||||
* the given cursor.
|
||||
*/
|
||||
private void initializeCachedValues(Cursor cursor) {
|
||||
// These indices are constant for every row (unless we're fed a MergeCursor)
|
||||
if (cursor instanceof MergeCursor) {
|
||||
throw new IllegalArgumentException("Cannot use a MergeCursor");
|
||||
}
|
||||
|
||||
columnIndices = new int[columnNames.length];
|
||||
for (int i = 0; i < columnNames.length; i++) {
|
||||
String columnName = columnNames[i];
|
||||
columnIndices[i] = cursor.getColumnIndexOrThrow(columnName);
|
||||
}
|
||||
|
||||
hasFields = new boolean[columnIndices.length];
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes metadata about the query to be dumped.
|
||||
*
|
||||
* @param numRows the number of rows that will be dumped
|
||||
* @param writer the output to write to
|
||||
* @throws IOException if there are any errors while writing
|
||||
*/
|
||||
private void writeQueryMetadata(
|
||||
int numRows, DataOutputStream writer) throws IOException {
|
||||
// Write column data
|
||||
writer.writeInt(columnNames.length);
|
||||
for (int i = 0; i < columnNames.length; i++) {
|
||||
String columnName = columnNames[i];
|
||||
byte columnType = columnTypes[i];
|
||||
writer.writeUTF(columnName);
|
||||
writer.writeByte(columnType);
|
||||
}
|
||||
|
||||
// Write the number of rows
|
||||
writer.writeInt(numRows);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a single cell of the database to the output.
|
||||
*
|
||||
* @param columnIdx the column index to read from
|
||||
* @param columnTypeId the type of the column to be read
|
||||
* @param cursor the cursor to read from
|
||||
* @param writer the output to write to
|
||||
* @throws IOException if there are any errors while writing
|
||||
*/
|
||||
private void writeCell(
|
||||
int columnIdx, byte columnTypeId, Cursor cursor, DataOutputStream writer)
|
||||
throws IOException {
|
||||
switch (columnTypeId) {
|
||||
case ContentTypeIds.LONG_TYPE_ID:
|
||||
writer.writeLong(cursor.getLong(columnIdx));
|
||||
return;
|
||||
case ContentTypeIds.DOUBLE_TYPE_ID:
|
||||
writer.writeDouble(cursor.getDouble(columnIdx));
|
||||
return;
|
||||
case ContentTypeIds.FLOAT_TYPE_ID:
|
||||
writer.writeFloat(cursor.getFloat(columnIdx));
|
||||
return;
|
||||
case ContentTypeIds.BOOLEAN_TYPE_ID:
|
||||
writer.writeBoolean(cursor.getInt(columnIdx) != 0);
|
||||
return;
|
||||
case ContentTypeIds.INT_TYPE_ID:
|
||||
writer.writeInt(cursor.getInt(columnIdx));
|
||||
return;
|
||||
case ContentTypeIds.STRING_TYPE_ID:
|
||||
writer.writeUTF(cursor.getString(columnIdx));
|
||||
return;
|
||||
case ContentTypeIds.BLOB_TYPE_ID: {
|
||||
byte[] blob = cursor.getBlob(columnIdx);
|
||||
writer.writeInt(blob.length);
|
||||
writer.write(blob);
|
||||
return;
|
||||
}
|
||||
default:
|
||||
throw new IllegalArgumentException(
|
||||
"Type " + columnTypeId + " not supported");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a dummy cell value to the output.
|
||||
*
|
||||
* @param columnTypeId the type of the value to write
|
||||
* @throws IOException if there are any errors while writing
|
||||
*/
|
||||
private void writeDummyCell(byte columnTypeId, DataOutputStream writer)
|
||||
throws IOException {
|
||||
switch (columnTypeId) {
|
||||
case ContentTypeIds.LONG_TYPE_ID:
|
||||
writer.writeLong(0L);
|
||||
return;
|
||||
case ContentTypeIds.DOUBLE_TYPE_ID:
|
||||
writer.writeDouble(0.0);
|
||||
return;
|
||||
case ContentTypeIds.FLOAT_TYPE_ID:
|
||||
writer.writeFloat(0.0f);
|
||||
return;
|
||||
case ContentTypeIds.BOOLEAN_TYPE_ID:
|
||||
writer.writeBoolean(false);
|
||||
return;
|
||||
case ContentTypeIds.INT_TYPE_ID:
|
||||
writer.writeInt(0);
|
||||
return;
|
||||
case ContentTypeIds.STRING_TYPE_ID:
|
||||
writer.writeUTF("");
|
||||
return;
|
||||
case ContentTypeIds.BLOB_TYPE_ID:
|
||||
writer.writeInt(0);
|
||||
return;
|
||||
default:
|
||||
throw new IllegalArgumentException(
|
||||
"Type " + columnTypeId + " not supported");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
/*
|
||||
* Copyright 2010 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package com.google.android.apps.mytracks.io.backup;
|
||||
|
||||
import com.google.android.apps.mytracks.content.ContentTypeIds;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ContentValues;
|
||||
import android.net.Uri;
|
||||
|
||||
import java.io.DataInputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Database importer which reads values written by {@link DatabaseDumper}.
|
||||
*
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
public class DatabaseImporter {
|
||||
|
||||
/** Maximum number of entries in a bulk insertion */
|
||||
private static final int DEFAULT_BULK_SIZE = 1024;
|
||||
|
||||
private final Uri destinationUri;
|
||||
private final ContentResolver resolver;
|
||||
private final boolean readNullFields;
|
||||
private final int bulkSize;
|
||||
|
||||
// Metadata read from the reader
|
||||
private String[] columnNames;
|
||||
private byte[] columnTypes;
|
||||
|
||||
public DatabaseImporter(Uri destinationUri, ContentResolver resolver,
|
||||
boolean readNullFields) {
|
||||
this(destinationUri, resolver, readNullFields, DEFAULT_BULK_SIZE);
|
||||
}
|
||||
|
||||
protected DatabaseImporter(Uri destinationUri, ContentResolver resolver,
|
||||
boolean readNullFields, int bulkSize) {
|
||||
this.destinationUri = destinationUri;
|
||||
this.resolver = resolver;
|
||||
this.readNullFields = readNullFields;
|
||||
this.bulkSize = bulkSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the header which includes metadata about the table being imported.
|
||||
*
|
||||
* @throws IOException if there are any problems while reading
|
||||
*/
|
||||
private void readHeaders(DataInputStream reader) throws IOException {
|
||||
int numColumns = reader.readInt();
|
||||
columnNames = new String[numColumns];
|
||||
columnTypes = new byte[numColumns];
|
||||
for (int i = 0; i < numColumns; i++) {
|
||||
columnNames[i] = reader.readUTF();
|
||||
columnTypes[i] = reader.readByte();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports all rows from the reader into the database.
|
||||
* Insertion is done in bulks for efficiency.
|
||||
*
|
||||
* @throws IOException if there are any errors while reading
|
||||
*/
|
||||
public void importAllRows(DataInputStream reader) throws IOException {
|
||||
readHeaders(reader);
|
||||
|
||||
ContentValues[] valueBulk = new ContentValues[bulkSize];
|
||||
int numValues = 0;
|
||||
|
||||
int numRows = reader.readInt();
|
||||
int numColumns = columnNames.length;
|
||||
|
||||
// For each row
|
||||
for (int r = 0; r < numRows; r++) {
|
||||
if (valueBulk[numValues] == null) {
|
||||
valueBulk[numValues] = new ContentValues(numColumns);
|
||||
} else {
|
||||
// Reuse values objects
|
||||
valueBulk[numValues].clear();
|
||||
}
|
||||
|
||||
// Read the fields bitmap
|
||||
long fields = reader.readLong();
|
||||
for (int c = 0; c < numColumns; c++) {
|
||||
if ((fields & 1) == 1) {
|
||||
// Field is present, read into values
|
||||
readOneCell(columnNames[c], columnTypes[c], valueBulk[numValues],
|
||||
reader);
|
||||
} else if (readNullFields) {
|
||||
// Field not present but still written, read and discard
|
||||
readOneCell(columnNames[c], columnTypes[c], null, reader);
|
||||
}
|
||||
|
||||
fields >>= 1;
|
||||
}
|
||||
|
||||
numValues++;
|
||||
|
||||
// If we have enough values, flush them as a bulk insertion
|
||||
if (numValues >= bulkSize) {
|
||||
doBulkInsert(valueBulk);
|
||||
numValues = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Do a final bulk insert with the leftovers
|
||||
if (numValues > 0) {
|
||||
ContentValues[] leftovers = new ContentValues[numValues];
|
||||
System.arraycopy(valueBulk, 0, leftovers, 0, numValues);
|
||||
doBulkInsert(leftovers);
|
||||
}
|
||||
}
|
||||
|
||||
protected void doBulkInsert(ContentValues[] values) {
|
||||
resolver.bulkInsert(destinationUri, values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a single cell from the reader.
|
||||
*
|
||||
* @param name the name of the column to be read
|
||||
* @param typeId the type ID of the column to be read
|
||||
* @param values the {@link ContentValues} object to put the read cell value
|
||||
* in - if null, the value is just discarded
|
||||
* @throws IOException if there are any problems while reading
|
||||
*/
|
||||
private void readOneCell(String name, byte typeId, ContentValues values,
|
||||
DataInputStream reader) throws IOException {
|
||||
switch (typeId) {
|
||||
case ContentTypeIds.BOOLEAN_TYPE_ID: {
|
||||
boolean value = reader.readBoolean();
|
||||
if (values != null) { values.put(name, value); }
|
||||
return;
|
||||
}
|
||||
case ContentTypeIds.LONG_TYPE_ID: {
|
||||
long value = reader.readLong();
|
||||
if (values != null) { values.put(name, value); }
|
||||
return;
|
||||
}
|
||||
case ContentTypeIds.DOUBLE_TYPE_ID: {
|
||||
double value = reader.readDouble();
|
||||
if (values != null) { values.put(name, value); }
|
||||
return;
|
||||
}
|
||||
case ContentTypeIds.FLOAT_TYPE_ID: {
|
||||
Float value = reader.readFloat();
|
||||
if (values != null) { values.put(name, value); }
|
||||
return;
|
||||
}
|
||||
case ContentTypeIds.INT_TYPE_ID: {
|
||||
int value = reader.readInt();
|
||||
if (values != null) { values.put(name, value); }
|
||||
return;
|
||||
}
|
||||
case ContentTypeIds.STRING_TYPE_ID: {
|
||||
String value = reader.readUTF();
|
||||
if (values != null) { values.put(name, value); }
|
||||
return;
|
||||
}
|
||||
case ContentTypeIds.BLOB_TYPE_ID: {
|
||||
int blobLength = reader.readInt();
|
||||
if (blobLength != 0) {
|
||||
byte[] blob = new byte[blobLength];
|
||||
int readBytes = reader.read(blob, 0, blobLength);
|
||||
if (readBytes != blobLength) {
|
||||
throw new IOException(String.format(Locale.US,
|
||||
"Short read on column %s; expected %d bytes, read %d",
|
||||
name, blobLength, readBytes));
|
||||
}
|
||||
|
||||
if (values != null) {
|
||||
values.put(name, blob);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
default:
|
||||
throw new IOException("Read unknown type " + typeId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
/*
|
||||
* 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 static com.google.android.apps.mytracks.Constants.TAG;
|
||||
|
||||
import com.google.android.apps.mytracks.Constants;
|
||||
import com.google.android.apps.mytracks.content.TrackPointsColumns;
|
||||
import com.google.android.apps.mytracks.content.TracksColumns;
|
||||
import com.google.android.apps.mytracks.content.WaypointsColumns;
|
||||
import com.google.android.apps.mytracks.util.FileUtils;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.database.Cursor;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.TimeZone;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipFile;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
/**
|
||||
* Handler for writing or reading single-file backups.
|
||||
*
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
class ExternalFileBackup {
|
||||
// Filename format - in UTC
|
||||
private static final SimpleDateFormat BACKUP_FILENAME_FORMAT = new SimpleDateFormat(
|
||||
"'backup-'yyyy-MM-dd_HH-mm-ss'.zip'", Locale.US);
|
||||
static {
|
||||
BACKUP_FILENAME_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC"));
|
||||
}
|
||||
|
||||
private static final String BACKUPS_SUBDIR = "backups";
|
||||
private static final int BACKUP_FORMAT_VERSION = 1;
|
||||
private static final String ZIP_ENTRY_NAME =
|
||||
"backup.mytracks.v" + BACKUP_FORMAT_VERSION;
|
||||
private static final int COMPRESSION_LEVEL = 8;
|
||||
|
||||
private final Context context;
|
||||
|
||||
public ExternalFileBackup(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the backups directory is (or can be made) available.
|
||||
*
|
||||
* @param create whether to try creating the directory if it doesn't exist
|
||||
*/
|
||||
public boolean isBackupsDirectoryAvailable(boolean create) {
|
||||
return getBackupsDirectory(create) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the backup directory, or null if not available.
|
||||
*
|
||||
* @param create whether to try creating the directory if it doesn't exist
|
||||
*/
|
||||
private File getBackupsDirectory(boolean create) {
|
||||
String dirName = FileUtils.buildExternalDirectoryPath(BACKUPS_SUBDIR);
|
||||
final File dir = new File(dirName);
|
||||
Log.d(Constants.TAG, "Dir: " + dir.getAbsolutePath());
|
||||
if (create) {
|
||||
// Try to create - if that fails, return null
|
||||
return FileUtils.ensureDirectoryExists(dir) ? dir : null;
|
||||
} else {
|
||||
// Return it if it already exists, otherwise return null
|
||||
return FileUtils.isDirectory(dir) ? dir : null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of available backups to be restored.
|
||||
*/
|
||||
public Date[] getAvailableBackups() {
|
||||
File dir = getBackupsDirectory(false);
|
||||
if (dir == null) { return null; }
|
||||
String[] fileNames = dir.list();
|
||||
|
||||
List<Date> backupDates = new ArrayList<Date>(fileNames.length);
|
||||
for (int i = 0; i < fileNames.length; i++) {
|
||||
String fileName = fileNames[i];
|
||||
try {
|
||||
backupDates.add(BACKUP_FILENAME_FORMAT.parse(fileName));
|
||||
} catch (ParseException e) {
|
||||
// Not a backup file, ignore
|
||||
}
|
||||
}
|
||||
|
||||
return backupDates.toArray(new Date[backupDates.size()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the backup to the default file.
|
||||
*/
|
||||
public void writeToDefaultFile() throws IOException {
|
||||
writeToFile(getFileForDate(new Date()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores the backup from the given date.
|
||||
*/
|
||||
public void restoreFromDate(Date when) throws IOException {
|
||||
restoreFromFile(getFileForDate(when));
|
||||
}
|
||||
|
||||
/**
|
||||
* Produces the proper file descriptor for the given backup date.
|
||||
*/
|
||||
private File getFileForDate(Date when) {
|
||||
File dir = getBackupsDirectory(false);
|
||||
String fileName = BACKUP_FILENAME_FORMAT.format(when);
|
||||
File file = new File(dir, fileName);
|
||||
return file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronously writes a backup to the given file.
|
||||
*/
|
||||
private void writeToFile(File outputFile) throws IOException {
|
||||
Log.d(Constants.TAG,
|
||||
"Writing backup to file " + outputFile.getAbsolutePath());
|
||||
|
||||
// Create all the auxiliary classes that will do the writing
|
||||
PreferenceBackupHelper preferencesHelper = new PreferenceBackupHelper(context);
|
||||
DatabaseDumper trackDumper = new DatabaseDumper(
|
||||
TracksColumns.COLUMNS,
|
||||
TracksColumns.COLUMN_TYPES,
|
||||
false);
|
||||
DatabaseDumper waypointDumper = new DatabaseDumper(
|
||||
WaypointsColumns.COLUMNS,
|
||||
WaypointsColumns.COLUMN_TYPES,
|
||||
false);
|
||||
DatabaseDumper pointDumper = new DatabaseDumper(
|
||||
TrackPointsColumns.COLUMNS,
|
||||
TrackPointsColumns.COLUMN_TYPES,
|
||||
false);
|
||||
|
||||
// Open the target for writing
|
||||
FileOutputStream outputStream = new FileOutputStream(outputFile);
|
||||
ZipOutputStream compressedStream = new ZipOutputStream(outputStream);
|
||||
compressedStream.setLevel(COMPRESSION_LEVEL);
|
||||
compressedStream.putNextEntry(new ZipEntry(ZIP_ENTRY_NAME));
|
||||
DataOutputStream outWriter = new DataOutputStream(compressedStream);
|
||||
|
||||
try {
|
||||
// Dump the entire contents of each table
|
||||
ContentResolver contentResolver = context.getContentResolver();
|
||||
Cursor tracksCursor = null;
|
||||
try {
|
||||
tracksCursor = contentResolver.query(TracksColumns.CONTENT_URI, null, null, null, null);
|
||||
if (tracksCursor != null) {
|
||||
trackDumper.writeAllRows(tracksCursor, outWriter);
|
||||
}
|
||||
} finally {
|
||||
if (tracksCursor != null) {
|
||||
tracksCursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
Cursor waypointsCursor = null;
|
||||
try {
|
||||
waypointsCursor = contentResolver.query(
|
||||
WaypointsColumns.CONTENT_URI, null, null, null, null);
|
||||
if (waypointsCursor != null) {
|
||||
waypointDumper.writeAllRows(waypointsCursor, outWriter);
|
||||
}
|
||||
} finally {
|
||||
if (waypointsCursor != null) {
|
||||
waypointsCursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
Cursor pointsCursor = null;
|
||||
try {
|
||||
pointsCursor = contentResolver.query(
|
||||
TrackPointsColumns.CONTENT_URI, null, null, null, null);
|
||||
if (pointsCursor != null) {
|
||||
pointDumper.writeAllRows(pointsCursor, outWriter);
|
||||
}
|
||||
} finally {
|
||||
if (pointsCursor != null) {
|
||||
pointsCursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
// Dump preferences
|
||||
SharedPreferences preferences = context.getSharedPreferences(
|
||||
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
|
||||
preferencesHelper.exportPreferences(preferences, outWriter);
|
||||
} catch (IOException e) {
|
||||
// We tried to delete the partially created file, but do nothing
|
||||
// if that also fails.
|
||||
if (!outputFile.delete()) {
|
||||
Log.w(TAG, "Failed to delete file " + outputFile.getAbsolutePath());
|
||||
}
|
||||
|
||||
throw e;
|
||||
} finally {
|
||||
compressedStream.closeEntry();
|
||||
compressedStream.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronously restores the backup from the given file.
|
||||
*/
|
||||
private void restoreFromFile(File inputFile) throws IOException {
|
||||
Log.d(Constants.TAG,
|
||||
"Restoring from file " + inputFile.getAbsolutePath());
|
||||
|
||||
PreferenceBackupHelper preferencesHelper = new PreferenceBackupHelper(context);
|
||||
ContentResolver resolver = context.getContentResolver();
|
||||
DatabaseImporter trackImporter =
|
||||
new DatabaseImporter(TracksColumns.CONTENT_URI, resolver, false);
|
||||
DatabaseImporter waypointImporter =
|
||||
new DatabaseImporter(WaypointsColumns.CONTENT_URI, resolver, false);
|
||||
DatabaseImporter pointImporter =
|
||||
new DatabaseImporter(TrackPointsColumns.CONTENT_URI, resolver, false);
|
||||
|
||||
ZipFile zipFile = new ZipFile(inputFile, ZipFile.OPEN_READ);
|
||||
ZipEntry zipEntry = zipFile.getEntry(ZIP_ENTRY_NAME);
|
||||
if (zipEntry == null) {
|
||||
throw new IOException("Invalid backup ZIP file");
|
||||
}
|
||||
|
||||
InputStream compressedStream = zipFile.getInputStream(zipEntry);
|
||||
DataInputStream reader = new DataInputStream(compressedStream);
|
||||
|
||||
try {
|
||||
// Delete all previous contents of the tables and preferences.
|
||||
resolver.delete(TracksColumns.CONTENT_URI, null, null);
|
||||
resolver.delete(TrackPointsColumns.CONTENT_URI, null, null);
|
||||
resolver.delete(WaypointsColumns.CONTENT_URI, null, null);
|
||||
|
||||
// Import the new contents of each table
|
||||
trackImporter.importAllRows(reader);
|
||||
waypointImporter.importAllRows(reader);
|
||||
pointImporter.importAllRows(reader);
|
||||
|
||||
// Restore preferences
|
||||
SharedPreferences preferences = context.getSharedPreferences(
|
||||
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
|
||||
preferencesHelper.importPreferences(reader, preferences);
|
||||
} finally {
|
||||
compressedStream.close();
|
||||
zipFile.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* Copyright 2010 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package com.google.android.apps.mytracks.io.backup;
|
||||
|
||||
import com.google.android.apps.mytracks.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 PREFERENCES_ENTITY = "prefs";
|
||||
|
||||
@Override
|
||||
public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
|
||||
ParcelFileDescriptor newState) throws IOException {
|
||||
Log.i(Constants.TAG, "Performing backup");
|
||||
SharedPreferences preferences = this.getSharedPreferences(
|
||||
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
|
||||
|
||||
backupPreferences(data, preferences);
|
||||
Log.i(Constants.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(Constants.TAG, "Restoring from backup");
|
||||
while (data.readNextHeader()) {
|
||||
String key = data.getKey();
|
||||
Log.d(Constants.TAG, "Restoring entity " + key);
|
||||
if (key.equals(PREFERENCES_ENTITY)) {
|
||||
restorePreferences(data);
|
||||
} else {
|
||||
Log.e(Constants.TAG, "Found unknown backup entity: " + key);
|
||||
data.skipEntityData();
|
||||
}
|
||||
}
|
||||
Log.i(Constants.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,209 @@
|
||||
/*
|
||||
* 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.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.auto_resume_track_current_retry_key));
|
||||
doNotBackup.add(context.getString(R.string.drive_deleted_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.recording_track_id_key));
|
||||
doNotBackup.add(context.getString(R.string.recording_track_paused_key));
|
||||
doNotBackup.add(context.getString(R.string.selected_track_id_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
|
||||
*/
|
||||
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,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.io.backup;
|
||||
|
||||
import com.google.android.apps.mytracks.TrackListActivity;
|
||||
import com.google.android.apps.mytracks.util.DialogUtils;
|
||||
import com.google.android.apps.mytracks.util.IntentUtils;
|
||||
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;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* An activity to restore data from the SD card.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class RestoreActivity extends Activity {
|
||||
|
||||
public static final String EXTRA_DATE = "date";
|
||||
|
||||
private static final String TAG = RestoreActivity.class.getSimpleName();
|
||||
private static final int DIALOG_PROGRESS_ID = 0;
|
||||
|
||||
private RestoreAsyncTask restoreAsyncTask;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
Object retained = getLastNonConfigurationInstance();
|
||||
if (retained instanceof RestoreAsyncTask) {
|
||||
restoreAsyncTask = (RestoreAsyncTask) retained;
|
||||
restoreAsyncTask.setActivity(this);
|
||||
} else {
|
||||
long date = getIntent().getLongExtra(EXTRA_DATE, -1L);
|
||||
if (date == -1L) {
|
||||
Log.d(TAG, "Invalid date");
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
restoreAsyncTask = new RestoreAsyncTask(this, new Date(date));
|
||||
restoreAsyncTask.execute();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object onRetainNonConfigurationInstance() {
|
||||
restoreAsyncTask.setActivity(null);
|
||||
return restoreAsyncTask;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Dialog onCreateDialog(int id) {
|
||||
if (id != DIALOG_PROGRESS_ID) {
|
||||
return null;
|
||||
}
|
||||
return DialogUtils.createSpinnerProgressDialog(this,
|
||||
R.string.settings_backup_restore_progress_message, new DialogInterface.OnCancelListener() {
|
||||
@Override
|
||||
public void onCancel(DialogInterface dialog) {
|
||||
finish();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes when the associated AsyncTask completes.
|
||||
*
|
||||
* @param success true if the AsyncTask is successful
|
||||
* @param messageId message id to display to user
|
||||
*/
|
||||
public void onAsyncTaskCompleted(boolean success, int messageId) {
|
||||
removeDialog(DIALOG_PROGRESS_ID);
|
||||
Toast.makeText(this, messageId, success ? Toast.LENGTH_SHORT : Toast.LENGTH_LONG).show();
|
||||
Intent intent = IntentUtils.newIntent(this, TrackListActivity.class);
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the progress dialog.
|
||||
*/
|
||||
public void showProgressDialog() {
|
||||
showDialog(DIALOG_PROGRESS_ID);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* 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.backup;
|
||||
|
||||
import com.google.android.apps.mytracks.io.sync.SyncUtils;
|
||||
import com.google.android.apps.mytracks.util.PreferencesUtils;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.os.AsyncTask;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* AsyncTask to restore data from the SD card.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class RestoreAsyncTask extends AsyncTask<Void, Integer, Boolean> {
|
||||
|
||||
private static final String TAG = RestoreAsyncTask.class.getSimpleName();
|
||||
|
||||
private RestoreActivity restoreActivity;
|
||||
private final Date date;
|
||||
private final ExternalFileBackup externalFileBackup;
|
||||
|
||||
// true if the AsyncTask result is success
|
||||
private boolean success;
|
||||
|
||||
// true if the AsyncTask has completed
|
||||
private boolean completed;
|
||||
|
||||
// message id to return to the activity
|
||||
private int messageId;
|
||||
|
||||
/**
|
||||
* Creates an AsyncTask.
|
||||
*
|
||||
* @param restoreActivity the activity currently associated with this
|
||||
* AsyncTask
|
||||
* @param date the date to retore from
|
||||
*/
|
||||
public RestoreAsyncTask(RestoreActivity restoreActivity, Date date) {
|
||||
this.restoreActivity = restoreActivity;
|
||||
this.date = date;
|
||||
this.externalFileBackup = new ExternalFileBackup(restoreActivity);
|
||||
success = false;
|
||||
completed = false;
|
||||
messageId = R.string.settings_backup_restore_error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current {@link RestoreActivity} associated with this AyncTask.
|
||||
*
|
||||
* @param activity the current {@link RestoreActivity}, can be null
|
||||
*/
|
||||
public void setActivity(RestoreActivity activity) {
|
||||
this.restoreActivity = activity;
|
||||
if (completed && restoreActivity != null) {
|
||||
restoreActivity.onAsyncTaskCompleted(success, messageId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
if (restoreActivity != null) {
|
||||
restoreActivity.showProgressDialog();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Boolean doInBackground(Void... params) {
|
||||
try {
|
||||
PreferencesUtils.setBoolean(
|
||||
restoreActivity, R.string.drive_sync_key, PreferencesUtils.DRIVE_SYNC_DEFAULT);
|
||||
SyncUtils.disableSync(restoreActivity);
|
||||
externalFileBackup.restoreFromDate(date);
|
||||
SyncUtils.clearSyncState(restoreActivity);
|
||||
messageId = R.string.settings_backup_restore_success;
|
||||
return true;
|
||||
} catch (IOException e) {
|
||||
Log.d(TAG, "IO exception", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Boolean result) {
|
||||
success = result;
|
||||
completed = true;
|
||||
if (restoreActivity != null) {
|
||||
restoreActivity.onAsyncTaskCompleted(success, messageId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
* 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.backup;
|
||||
|
||||
import com.google.android.apps.mytracks.util.FileUtils;
|
||||
import com.google.android.apps.mytracks.util.IntentUtils;
|
||||
import com.google.android.apps.mytracks.util.StringUtils;
|
||||
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.os.Bundle;
|
||||
import android.widget.Toast;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* An activity to choose a date to restore from.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class RestoreChooserActivity extends Activity {
|
||||
|
||||
private static final Comparator<Date> REVERSE_DATE_ORDER = new Comparator<Date>() {
|
||||
@Override
|
||||
public int compare(Date s1, Date s2) {
|
||||
return s2.compareTo(s1);
|
||||
}
|
||||
};
|
||||
private static final int DIALOG_CHOOSER_ID = 0;
|
||||
|
||||
private Date[] backupDates;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
ExternalFileBackup externalFileBackup = new ExternalFileBackup(this);
|
||||
|
||||
// Get the list of existing backups
|
||||
if (!FileUtils.isExternalStorageAvailable()) {
|
||||
Toast.makeText(this, R.string.external_storage_not_available, Toast.LENGTH_LONG).show();
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!externalFileBackup.isBackupsDirectoryAvailable(false)) {
|
||||
Toast.makeText(this, R.string.settings_backup_restore_no_backup, Toast.LENGTH_LONG).show();
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
backupDates = externalFileBackup.getAvailableBackups();
|
||||
if (backupDates == null || backupDates.length == 0) {
|
||||
Toast.makeText(this, R.string.settings_backup_restore_no_backup, Toast.LENGTH_LONG).show();
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
if (backupDates.length == 1) {
|
||||
Intent intent = IntentUtils.newIntent(this, RestoreActivity.class)
|
||||
.putExtra(RestoreActivity.EXTRA_DATE, backupDates[0].getTime());
|
||||
startActivity(intent);
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
Arrays.sort(backupDates, REVERSE_DATE_ORDER);
|
||||
showDialog(DIALOG_CHOOSER_ID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Dialog onCreateDialog(int id) {
|
||||
if (id != DIALOG_CHOOSER_ID) {
|
||||
return null;
|
||||
}
|
||||
String items[] = new String[backupDates.length];
|
||||
for (int i = 0; i < backupDates.length; i++) {
|
||||
items[i] = StringUtils.formatDateTime(this, backupDates[i].getTime());
|
||||
}
|
||||
return new AlertDialog.Builder(this)
|
||||
.setCancelable(true)
|
||||
.setItems(items, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
Intent intent = IntentUtils.newIntent(
|
||||
RestoreChooserActivity.this, RestoreActivity.class)
|
||||
.putExtra(RestoreActivity.EXTRA_DATE, backupDates[which].getTime());
|
||||
startActivity(intent);
|
||||
finish();
|
||||
}
|
||||
})
|
||||
.setOnCancelListener(new DialogInterface.OnCancelListener() {
|
||||
@Override
|
||||
public void onCancel(DialogInterface dialog) {
|
||||
finish();
|
||||
}
|
||||
})
|
||||
.setTitle(R.string.settings_backup_restore_select_title)
|
||||
.create();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* 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.ChooseMapActivity;
|
||||
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());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getServiceName() {
|
||||
return getString(R.string.send_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 sendRequest.isMapsExistingMap() ? ChooseMapActivity.class : SendMapsActivity.class;
|
||||
} else if (sendRequest.isSendFusionTables()) {
|
||||
return SendFusionTablesActivity.class;
|
||||
} else if (sendRequest.isSendSpreadsheets()) {
|
||||
return SendSpreadsheetsActivity.class;
|
||||
} else {
|
||||
return UploadResultActivity.class;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
/*
|
||||
* 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.gms.auth.GoogleAuthException;
|
||||
import com.google.android.gms.auth.UserRecoverableAuthException;
|
||||
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 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 Context context;
|
||||
private final MyTracksProviderUtils myTracksProviderUtils;
|
||||
|
||||
public SendDriveAsyncTask(SendDriveActivity activity, long trackId, Account account, String acl) {
|
||||
super(activity);
|
||||
this.trackId = trackId;
|
||||
this.account = account;
|
||||
if (acl != null) {
|
||||
this.acl = acl.split(",");
|
||||
} else {
|
||||
this.acl = null;
|
||||
}
|
||||
|
||||
context = activity.getApplicationContext();
|
||||
myTracksProviderUtils = MyTracksProviderUtils.Factory.get(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void closeConnection() {}
|
||||
|
||||
@Override
|
||||
protected void saveResult() {}
|
||||
|
||||
@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();
|
||||
if (driveId != null && !driveId.equals("")) {
|
||||
File driveFile = drive.files().get(driveId).execute();
|
||||
if (SyncUtils.isValid(driveFile, folderId) && SyncUtils.updateDriveFile(
|
||||
drive, driveFile, context, myTracksProviderUtils, track, false)) {
|
||||
addPermission(drive, driveId);
|
||||
return true;
|
||||
}
|
||||
SyncUtils.updateTrackWithDriveFileInfo(myTracksProviderUtils, track, null);
|
||||
}
|
||||
|
||||
String id = SyncUtils.insertDriveFile(
|
||||
drive, folderId, context, myTracksProviderUtils, track, false);
|
||||
if (id == null) {
|
||||
return false;
|
||||
}
|
||||
addPermission(drive, id);
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void invalidateToken() {}
|
||||
|
||||
/**
|
||||
* Adds permission.
|
||||
*
|
||||
* @param drive the drive
|
||||
* @param driveId the drive id
|
||||
*/
|
||||
private void addPermission(Drive drive, String driveId) throws IOException {
|
||||
if (acl != null) {
|
||||
for (String email : acl) {
|
||||
email = email.trim();
|
||||
if (!email.equals("")) {
|
||||
Permission permission = new Permission();
|
||||
permission.setValue(email);
|
||||
permission.setType("user");
|
||||
permission.setRole("reader");
|
||||
drive.permissions().insert(driveId, permission).execute();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,591 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import com.google.android.apps.mytracks.Constants;
|
||||
import com.google.android.apps.mytracks.content.DescriptionGeneratorImpl;
|
||||
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.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.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.maps.mytracks.R;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.location.Location;
|
||||
import android.location.LocationManager;
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.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 importers like {@link GpxImporter} and
|
||||
* {@link KmlImporter}.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
abstract class AbstractImporter extends DefaultHandler {
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
protected static final String DEFAULT_ALTITUDE = "0";
|
||||
|
||||
// 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 minRecordingDistance;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param context the context
|
||||
* @param importTrackId the track id to import to. -1L to import to a new
|
||||
* track.
|
||||
*/
|
||||
protected AbstractImporter(Context context, long importTrackId) {
|
||||
this(context, importTrackId, MyTracksProviderUtils.Factory.get(context));
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
protected AbstractImporter(
|
||||
Context context, long importTrackId, MyTracksProviderUtils myTracksProviderUtils) {
|
||||
this.context = context;
|
||||
this.importTrackId = importTrackId;
|
||||
this.myTracksProviderUtils = myTracksProviderUtils;
|
||||
this.minRecordingDistance = PreferencesUtils.getInt(context,
|
||||
R.string.min_recording_distance_key, PreferencesUtils.MIN_RECORDING_DISTANCE_DEFAULT);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports a file.
|
||||
*
|
||||
* @param inputStream the input stream.
|
||||
* @return an array of imported track ids.
|
||||
*/
|
||||
public long[] importFile(InputStream inputStream)
|
||||
throws SAXException, IOException, ParserConfigurationException {
|
||||
SAXParserFactory saxParserFactory = SAXParserFactory.newInstance();
|
||||
SAXParser saxParser = saxParserFactory.newSAXParser();
|
||||
|
||||
try {
|
||||
long start = System.currentTimeMillis();
|
||||
|
||||
saxParser.parse(inputStream, this);
|
||||
|
||||
long end = System.currentTimeMillis();
|
||||
Log.d(Constants.TAG, "Total import time: " + (end - start) + "ms");
|
||||
} catch (SAXException e) {
|
||||
handleException();
|
||||
throw e;
|
||||
} catch (IOException e) {
|
||||
handleException();
|
||||
throw e;
|
||||
}
|
||||
long[] result = new long[trackIds.size()];
|
||||
for (int i = 0; i < result.length; i++) {
|
||||
result[i] = trackIds.get(i);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
Cursor trackPointCursor = null;
|
||||
try {
|
||||
trackPointCursor = myTracksProviderUtils.getTrackPointCursor(track.getId(), -1L, -1, false);
|
||||
if (trackPointCursor == null) {
|
||||
return;
|
||||
}
|
||||
int waypointPosition = -1;
|
||||
Waypoint waypoint = null;
|
||||
int trackPointPosition = -1;
|
||||
Location trackPoint = null;
|
||||
TripStatisticsUpdater trackTripStatisticstrackUpdater = new TripStatisticsUpdater(
|
||||
track.getTripStatistics().getStartTime());
|
||||
TripStatisticsUpdater markerTripStatisticsUpdater = new TripStatisticsUpdater(
|
||||
track.getTripStatistics().getStartTime());
|
||||
|
||||
while (true) {
|
||||
if (waypoint == null) {
|
||||
waypointPosition++;
|
||||
waypoint = waypointPosition < waypoints.size() ? waypoints.get(waypointPosition) : null;
|
||||
if (waypoint == null) {
|
||||
// No more waypoints
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (trackPoint == null) {
|
||||
trackPointPosition++;
|
||||
trackPoint = trackPointCursor.moveToPosition(trackPointPosition) ? myTracksProviderUtils
|
||||
.createTrackPoint(trackPointCursor)
|
||||
: null;
|
||||
if (trackPoint == null) {
|
||||
// No more track points. Ignore the rest of the waypoints.
|
||||
return;
|
||||
}
|
||||
trackTripStatisticstrackUpdater.addLocation(trackPoint, minRecordingDistance);
|
||||
markerTripStatisticsUpdater.addLocation(trackPoint, minRecordingDistance);
|
||||
}
|
||||
if (waypoint.getLocation().getTime() > trackPoint.getTime()) {
|
||||
trackPoint = null;
|
||||
} else if (waypoint.getLocation().getTime() < trackPoint.getTime()) {
|
||||
waypoint = null;
|
||||
} else {
|
||||
// The waypoint location time matches the track point time
|
||||
if (trackPoint.getLatitude() == waypoint.getLocation().getLatitude()
|
||||
&& trackPoint.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(trackPoint.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, trackPoint, tripStatistics);
|
||||
myTracksProviderUtils.insertWaypoint(newWaypoint);
|
||||
}
|
||||
waypoint = null;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (trackPointCursor != null) {
|
||||
trackPointCursor.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(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);
|
||||
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(Constants.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.
|
||||
*/
|
||||
float speed = trackData.lastLocationInCurrentSegment.distanceTo(location) * 1000.0f
|
||||
/ timeDifference;
|
||||
location.setSpeed(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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a location.
|
||||
*/
|
||||
private Location createLocation() throws SAXException {
|
||||
if (latitude == null || longitude == null || altitude == null) {
|
||||
return null;
|
||||
}
|
||||
double longitudeValue;
|
||||
double latitudeValue;
|
||||
double altitudeValue;
|
||||
|
||||
try {
|
||||
longitudeValue = Double.parseDouble(longitude);
|
||||
latitudeValue = Double.parseDouble(latitude);
|
||||
altitudeValue = Double.parseDouble(altitude);
|
||||
} catch (NumberFormatException e) {
|
||||
throw new SAXException(createErrorMessage(String.format(
|
||||
"Unable to parse longitude latitude altitude: %s %s %s", longitude, latitude, 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("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);
|
||||
location.setAltitude(altitudeValue);
|
||||
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);
|
||||
}
|
||||
trackData.tripStatisticsUpdater.addLocation(location, minRecordingDistance);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles an exception.
|
||||
*/
|
||||
private void handleException() {
|
||||
for (long trackId : trackIds) {
|
||||
myTracksProviderUtils.deleteTrack(trackId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
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.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;
|
||||
|
||||
/**
|
||||
* 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 TrackFormatWriter {
|
||||
|
||||
private static final NumberFormat SHORT_FORMAT = NumberFormat.getInstance();
|
||||
|
||||
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 out) {
|
||||
printWriter = new PrintWriter(out);
|
||||
segmentIndex = 0;
|
||||
pointIndex = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
printWriter.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeHeader(Track track) {
|
||||
writeCommaSeparatedLine(context.getString(R.string.generic_name),
|
||||
context.getString(R.string.track_edit_activity_type_hint),
|
||||
context.getString(R.string.generic_description));
|
||||
writeCommaSeparatedLine(track.getName(), track.getCategory(), track.getDescription());
|
||||
writeCommaSeparatedLine();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeFooter() {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeBeginWaypoints() {
|
||||
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()),
|
||||
Double.toString(location.getAltitude()),
|
||||
Double.toString(location.getBearing()),
|
||||
SHORT_FORMAT.format(location.getAccuracy()),
|
||||
SHORT_FORMAT.format(location.getSpeed()),
|
||||
StringUtils.formatDateTimeIso8601(location.getTime()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeBeginTrack(Track track, Location firstPoint) {
|
||||
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 lastPoint) {
|
||||
// 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()),
|
||||
Double.toString(location.getAltitude()),
|
||||
Double.toString(location.getBearing()),
|
||||
SHORT_FORMAT.format(location.getAccuracy()),
|
||||
SHORT_FORMAT.format(location.getSpeed()),
|
||||
StringUtils.formatDateTimeIso8601(location.getTime()),
|
||||
power,
|
||||
cadence,
|
||||
heartRate);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,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;
|
||||
|
||||
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 GpxImporter extends AbstractImporter {
|
||||
|
||||
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
|
||||
* @param importTrackId track id to import to. -1L to import to a new track.
|
||||
*/
|
||||
public GpxImporter(Context context, long importTrackId) {
|
||||
super(context, importTrackId);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public GpxImporter(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 = DEFAULT_ALTITUDE;
|
||||
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;
|
||||
latitude = attributes.getValue(ATTRIBUTE_LAT);
|
||||
longitude = attributes.getValue(ATTRIBUTE_LON);
|
||||
altitude = DEFAULT_ALTITUDE;
|
||||
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,186 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import com.google.android.apps.mytracks.content.Track;
|
||||
import com.google.android.apps.mytracks.content.Waypoint;
|
||||
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 TrackFormatWriter {
|
||||
|
||||
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.close();
|
||||
printWriter = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeHeader(Track track) {
|
||||
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>");
|
||||
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 writeBeginTrack(Track track, Location firstLocation) {
|
||||
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 lastLocation) {
|
||||
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) + ">");
|
||||
printWriter.println("<ele>" + ELEVATION_FORMAT.format(location.getAltitude()) + "</ele>");
|
||||
printWriter.println(
|
||||
"<time>" + StringUtils.formatDateTimeIso8601(location.getTime()) + "</time>");
|
||||
printWriter.println("</trkpt>");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeBeginWaypoints() {
|
||||
// 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) + ">");
|
||||
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>");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,221 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import com.google.android.apps.mytracks.TrackDetailActivity;
|
||||
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 path;
|
||||
|
||||
// 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;
|
||||
}
|
||||
if (importAll) {
|
||||
path = FileUtils.buildExternalDirectoryPath(
|
||||
trackFileFormat == TrackFileFormat.KML ? "kml" : "gpx");
|
||||
if (!FileUtils.isDirectory(new File(path))) {
|
||||
Toast.makeText(this, getString(R.string.import_no_directory, path), 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;
|
||||
}
|
||||
path = data.getPath();
|
||||
}
|
||||
|
||||
Object retained = getLastNonConfigurationInstance();
|
||||
if (retained instanceof ImportAsyncTask) {
|
||||
importAsyncTask = (ImportAsyncTask) retained;
|
||||
importAsyncTask.setActivity(this);
|
||||
} else {
|
||||
importAsyncTask = new ImportAsyncTask(this, importAll, trackFileFormat, path);
|
||||
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();
|
||||
finish();
|
||||
}
|
||||
}, path);
|
||||
return progressDialog;
|
||||
case DIALOG_RESULT_ID:
|
||||
final boolean success;
|
||||
String message;
|
||||
String totalFiles = getResources()
|
||||
.getQuantityString(R.plurals.files, totalCount, totalCount);
|
||||
if (successCount == totalCount && totalCount > 0) {
|
||||
success = true;
|
||||
message = getString(R.string.import_success, totalFiles, path);
|
||||
} else {
|
||||
success = false;
|
||||
message = getString(R.string.import_error, successCount, totalFiles, path);
|
||||
}
|
||||
return new AlertDialog.Builder(this).setCancelable(true).setIcon(
|
||||
success ? android.R.drawable.ic_dialog_info : android.R.drawable.ic_dialog_alert)
|
||||
.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 which) {
|
||||
if (success && !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();
|
||||
}
|
||||
dialog.dismiss();
|
||||
finish();
|
||||
}
|
||||
}).setTitle(success ? R.string.generic_success_title : R.string.generic_error_title)
|
||||
.create();
|
||||
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,198 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
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.os.AsyncTask;
|
||||
import android.os.PowerManager.WakeLock;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
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();
|
||||
|
||||
// Get the wake lock if not recording or paused
|
||||
if (PreferencesUtils.getLong(importActivity, R.string.recording_track_id_key)
|
||||
== PreferencesUtils.RECORDING_TRACK_ID_DEFAULT || PreferencesUtils.getBoolean(
|
||||
importActivity, R.string.recording_track_paused_key,
|
||||
PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT)) {
|
||||
wakeLock = SystemUtils.acquireWakeLock(importActivity, wakeLock);
|
||||
}
|
||||
|
||||
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 {
|
||||
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 {
|
||||
// Release the wake lock if obtained
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports a file.
|
||||
*
|
||||
* @param file the file
|
||||
*/
|
||||
private boolean importFile(final File file) {
|
||||
try {
|
||||
AbstractImporter importer = trackFileFormat == TrackFileFormat.KML ? new KmlImporter(
|
||||
context, -1L)
|
||||
: new GpxImporter(context, -1L);
|
||||
long trackIds[] = importer.importFile(new FileInputStream(file));
|
||||
int length = trackIds.length;
|
||||
if (length > 0) {
|
||||
trackId = trackIds[length - 1];
|
||||
}
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
Log.d(TAG, "file: " + file.getAbsolutePath(), e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 (!candidate.isDirectory() && candidate.getName()
|
||||
.endsWith(trackFileFormat == TrackFileFormat.KML ? ".kml" : ".gpx")) {
|
||||
files.add(candidate);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
files.add(file);
|
||||
}
|
||||
return files;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
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 java.util.ArrayList;
|
||||
|
||||
import org.xml.sax.Attributes;
|
||||
import org.xml.sax.SAXException;
|
||||
|
||||
/**
|
||||
* Imports a KML file.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class KmlImporter extends AbstractImporter {
|
||||
|
||||
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_KML = "kml";
|
||||
private static final String TAG_NAME = "name";
|
||||
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 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 KmlImporter(Context context, long importTrackId) {
|
||||
super(context, importTrackId);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public KmlImporter(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_PLACEMARK)) {
|
||||
onWaypointStart();
|
||||
} else if (tag.equals(TAG_GX_MULTI_TRACK)) {
|
||||
onTrackStart();
|
||||
} else if (tag.equals(TAG_GX_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)) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
// Reset element content
|
||||
content = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* On waypoint start.
|
||||
*/
|
||||
private void onWaypointStart() {
|
||||
// Reset all Placemark variables
|
||||
name = null;
|
||||
description = null;
|
||||
category = null;
|
||||
latitude = null;
|
||||
longitude = null;
|
||||
altitude = DEFAULT_ALTITUDE;
|
||||
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;
|
||||
}
|
||||
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];
|
||||
if (parts.length == 3) {
|
||||
altitude = parts[2];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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 != 3) {
|
||||
return;
|
||||
}
|
||||
longitude = parts[0];
|
||||
latitude = parts[1];
|
||||
altitude = parts[2];
|
||||
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) {
|
||||
int value;
|
||||
try {
|
||||
value = Integer.parseInt(content.trim());
|
||||
} 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,368 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
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.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.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.location.Location;
|
||||
|
||||
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 TrackFormatWriter {
|
||||
|
||||
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 DescriptionGenerator descriptionGenerator;
|
||||
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) {
|
||||
this(context, new DescriptionGeneratorImpl(context));
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
KmlTrackWriter(Context context, DescriptionGenerator descriptionGenerator) {
|
||||
this.context = context;
|
||||
this.descriptionGenerator = descriptionGenerator;
|
||||
}
|
||||
|
||||
@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.close();
|
||||
printWriter = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeHeader(Track track) {
|
||||
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>");
|
||||
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() {
|
||||
if (printWriter != null) {
|
||||
printWriter.println("<Folder><name>"
|
||||
+ StringUtils.formatCData(context.getString(R.string.menu_markers)) + "</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;
|
||||
writePlacemark(waypoint.getName(), waypoint.getCategory(), waypoint.getDescription(),
|
||||
styleName, waypoint.getLocation());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeBeginTrack(Track track, Location firstLocation) {
|
||||
if (printWriter != null) {
|
||||
String name = context.getString(R.string.marker_label_start, track.getName());
|
||||
writePlacemark(name, "", "", START_STYLE, firstLocation);
|
||||
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 lastLocation) {
|
||||
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, lastLocation);
|
||||
}
|
||||
}
|
||||
|
||||
@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>" + location.getLongitude() + " " + location.getLatitude()
|
||||
+ " " + location.getAltitude() + "</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>" + location.getLongitude() + "," + location.getLatitude()
|
||||
+ "," + location.getAltitude() + "</coordinates>");
|
||||
printWriter.println("</Point>");
|
||||
printWriter.println("</Placemark>");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,203 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
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";
|
||||
public static final String EXTRA_SHARE_TRACK = "share_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 boolean shareTrack;
|
||||
private String directoryName;
|
||||
|
||||
private SaveAsyncTask saveAsyncTask;
|
||||
private ProgressDialog progressDialog;
|
||||
|
||||
// the number of tracks successfully saved
|
||||
private int successCount;
|
||||
|
||||
// the number of tracks to save
|
||||
private int totalCount;
|
||||
|
||||
@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);
|
||||
shareTrack = intent.getBooleanExtra(EXTRA_SHARE_TRACK, false);
|
||||
|
||||
if (!FileUtils.isExternalStorageWriteable()) {
|
||||
Toast.makeText(this, R.string.external_storage_not_writable, Toast.LENGTH_LONG).show();
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
directoryName = playTrack || shareTrack ? FileUtils.buildExternalDirectoryPath(
|
||||
trackFileFormat.getExtension(), "tmp")
|
||||
: FileUtils.buildExternalDirectoryPath(trackFileFormat.getExtension());
|
||||
|
||||
File directory = new File(directoryName);
|
||||
if (!FileUtils.ensureDirectoryExists(directory)) {
|
||||
Toast.makeText(this, R.string.external_storage_not_writable, Toast.LENGTH_LONG).show();
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
Object retained = getLastNonConfigurationInstance();
|
||||
if (retained instanceof SaveAsyncTask) {
|
||||
saveAsyncTask = (SaveAsyncTask) retained;
|
||||
saveAsyncTask.setActivity(this);
|
||||
} else {
|
||||
saveAsyncTask = new SaveAsyncTask(this, trackFileFormat, trackIds, 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.save_progress_message, new DialogInterface.OnCancelListener() {
|
||||
@Override
|
||||
public void onCancel(DialogInterface dialog) {
|
||||
saveAsyncTask.cancel(true);
|
||||
dialog.dismiss();
|
||||
finish();
|
||||
}
|
||||
}, directoryName);
|
||||
return progressDialog;
|
||||
case DIALOG_RESULT_ID:
|
||||
boolean success;
|
||||
String message;
|
||||
String totalTracks = getResources()
|
||||
.getQuantityString(R.plurals.tracks, totalCount, totalCount);
|
||||
if (successCount == totalCount && totalCount > 0) {
|
||||
success = true;
|
||||
message = getString(R.string.save_success, totalTracks, directoryName);
|
||||
} else {
|
||||
success = false;
|
||||
message = getString(R.string.save_error, successCount, totalTracks, directoryName);
|
||||
}
|
||||
return new AlertDialog.Builder(this).setCancelable(true).setIcon(
|
||||
success ? android.R.drawable.ic_dialog_info : android.R.drawable.ic_dialog_alert)
|
||||
.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(success ? R.string.generic_success_title : R.string.generic_error_title)
|
||||
.create();
|
||||
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 savedPath the last successfully saved path
|
||||
*/
|
||||
public void onAsyncTaskCompleted(int aSuccessCount, int aTotalCount, String savedPath) {
|
||||
successCount = aSuccessCount;
|
||||
totalCount = aTotalCount;
|
||||
removeDialog(DIALOG_PROGRESS_ID);
|
||||
if (successCount == 1 && totalCount == 1 && savedPath != null) {
|
||||
if (playTrack) {
|
||||
startActivity(GoogleEarthUtils.getPlayInEarthIntent(savedPath));
|
||||
finish();
|
||||
return;
|
||||
} else if (shareTrack) {
|
||||
Intent intent = IntentUtils.newShareFileIntent(this, trackIds[0], savedPath, trackFileFormat);
|
||||
startActivity(Intent.createChooser(intent, getString(R.string.share_track_picker_title)));
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
}
|
||||
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,251 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
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.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.OutputStream;
|
||||
|
||||
/**
|
||||
* 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 TrackFileFormat trackFileFormat;
|
||||
private final long[] trackIds;
|
||||
private final File directory;
|
||||
private final Context context;
|
||||
private final MyTracksProviderUtils myTracksProviderUtils;
|
||||
|
||||
private WakeLock wakeLock;
|
||||
private TrackWriter trackWriter;
|
||||
|
||||
// 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
|
||||
* @track id the track id to save, -1L to save all tracks
|
||||
* @param trackFileFormat the track file format
|
||||
* @param directory the directory to save to
|
||||
*/
|
||||
public SaveAsyncTask(
|
||||
SaveActivity saveActivity, TrackFileFormat trackFileFormat, long[] trackIds, File directory) {
|
||||
this.saveActivity = saveActivity;
|
||||
this.trackFileFormat = trackFileFormat;
|
||||
this.trackIds = trackIds;
|
||||
this.directory = directory;
|
||||
context = saveActivity.getApplicationContext();
|
||||
myTracksProviderUtils = MyTracksProviderUtils.Factory.get(context);
|
||||
|
||||
// Get the wake lock if not recording or paused
|
||||
if (PreferencesUtils.getLong(saveActivity, R.string.recording_track_id_key)
|
||||
== PreferencesUtils.RECORDING_TRACK_ID_DEFAULT || PreferencesUtils.getBoolean(saveActivity,
|
||||
R.string.recording_track_paused_key, PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT)) {
|
||||
wakeLock = SystemUtils.acquireWakeLock(saveActivity, wakeLock);
|
||||
}
|
||||
|
||||
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 {
|
||||
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 {
|
||||
// Release the wake lock if obtained
|
||||
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() {
|
||||
if (trackWriter != null) {
|
||||
trackWriter.stopWriteTrack();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves tracks to one file.
|
||||
*
|
||||
* @param tracks the tracks
|
||||
*/
|
||||
private Boolean saveTracks(Track[] tracks) {
|
||||
if (tracks.length == 0) {
|
||||
return false;
|
||||
}
|
||||
Track track = tracks[0];
|
||||
// Make sure the file doesn't exist yet (possibly by changing the filename)
|
||||
String fileName = FileUtils.buildUniqueFileName(
|
||||
directory, track.getName(), trackFileFormat.getExtension());
|
||||
if (fileName == null) {
|
||||
Log.d(TAG, "Unable to get a unique filename for " + track.getName());
|
||||
return false;
|
||||
}
|
||||
|
||||
trackWriter = new TrackWriter(
|
||||
context, myTracksProviderUtils, tracks, trackFileFormat, new TrackWriter.OnWriteListener() {
|
||||
@Override
|
||||
public void onWrite(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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
File file = null;
|
||||
try {
|
||||
file = new File(directory, fileName);
|
||||
OutputStream outputStream = new FileOutputStream(file);
|
||||
trackWriter.writeTrack(outputStream);
|
||||
} catch (FileNotFoundException e) {
|
||||
Log.d(TAG, "File not found " + fileName, e);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (trackWriter.wasSuccess()) {
|
||||
savedPath = file.getAbsolutePath();
|
||||
} else {
|
||||
if (!file.delete()) {
|
||||
Log.w(TAG, "Failed to delete file " + file.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
return trackWriter.wasSuccess();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,327 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
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.util.StringUtils;
|
||||
import com.google.android.apps.mytracks.util.SystemUtils;
|
||||
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 Rttsches
|
||||
*/
|
||||
public class TcxTrackWriter implements TrackFormatWriter {
|
||||
|
||||
/**
|
||||
* 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 out) {
|
||||
this.printWriter = new PrintWriter(out);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (printWriter != null) {
|
||||
printWriter.close();
|
||||
printWriter = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getExtension() {
|
||||
return TrackFileFormat.TCX.getExtension();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeHeader(Track track) {
|
||||
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 writeBeginTrack(Track track, Location firstPoint) {
|
||||
sportType = getSportType(track.getCategory());
|
||||
if (printWriter != null) {
|
||||
String startTime = StringUtils.formatDateTimeIso8601(
|
||||
track.getTripStatistics().getStartTime());
|
||||
long totalTimeInSeconds = track.getTripStatistics().getTotalTime() / 1000;
|
||||
|
||||
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 lastPoint) {
|
||||
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>");
|
||||
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>");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeBeginWaypoints() {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeEndWaypoints() {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeWaypoint(Waypoint waypoint) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,78 @@
|
||||
package com.google.android.apps.mytracks.io.file;
|
||||
|
||||
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 {
|
||||
GPX {
|
||||
@Override
|
||||
TrackFormatWriter newFormatWriter(Context context) {
|
||||
return new GpxTrackWriter(context);
|
||||
}
|
||||
},
|
||||
KML {
|
||||
@Override
|
||||
TrackFormatWriter newFormatWriter(Context context) {
|
||||
return new KmlTrackWriter(context);
|
||||
}
|
||||
},
|
||||
CSV {
|
||||
@Override
|
||||
public TrackFormatWriter newFormatWriter(Context context) {
|
||||
return new CsvTrackWriter(context);
|
||||
}
|
||||
},
|
||||
TCX {
|
||||
@Override
|
||||
public TrackFormatWriter newFormatWriter(Context context) {
|
||||
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 and returns a new format writer for each format.
|
||||
*/
|
||||
abstract TrackFormatWriter newFormatWriter(Context context);
|
||||
|
||||
/**
|
||||
* 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,133 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
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:
|
||||
* <ul>
|
||||
* <li>{@link #prepare}</li>
|
||||
* <li>{@link #writeHeader}</li>
|
||||
* <li>For each track:</li>
|
||||
* <ul>
|
||||
* <li>{@link #writeBeginWaypoints}
|
||||
* <li>For each waypoint: {@link #writeWaypoint}
|
||||
* <li>{@link #writeEndWaypoints}
|
||||
* <li>{@link #writeBeginTrack}
|
||||
* <li>For each segment:
|
||||
* <ul>
|
||||
* <li>{@link #writeOpenSegment}</li>
|
||||
* <li>For each location in the segment: {@link #writeLocation}</li>
|
||||
* <li>{@link #writeCloseSegment}</li>
|
||||
* </ul>
|
||||
* </li>
|
||||
* <li>{@link #writeEndTrack}</li>
|
||||
* </ul>
|
||||
* <li>{@link #writeFooter}</li>
|
||||
* <li>{@link #close}</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
public interface TrackFormatWriter {
|
||||
|
||||
/**
|
||||
* Gets the file extension (i.e. gpx, kml, ...)
|
||||
*/
|
||||
public String getExtension();
|
||||
|
||||
/**
|
||||
* Sets up the file handler.
|
||||
*
|
||||
* @param outputStream the output stream for the file handler
|
||||
*/
|
||||
public void prepare(OutputStream outputStream);
|
||||
|
||||
/**
|
||||
* Closes the underlying file handler.
|
||||
*/
|
||||
public void close();
|
||||
|
||||
/**
|
||||
* Writes the header for a file
|
||||
*
|
||||
* @param track the track
|
||||
*/
|
||||
public void writeHeader(Track track);
|
||||
|
||||
/**
|
||||
* Writes the footer.
|
||||
*/
|
||||
public void writeFooter();
|
||||
|
||||
/**
|
||||
* Writes the beginning of the waypoints.
|
||||
*/
|
||||
public void writeBeginWaypoints();
|
||||
|
||||
/**
|
||||
* 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 track.
|
||||
*
|
||||
* @param track the track
|
||||
* @param firstLocation the first location
|
||||
*/
|
||||
public void writeBeginTrack(Track track, Location firstLocation);
|
||||
|
||||
/**
|
||||
* Writes the end of the track.
|
||||
*
|
||||
* @param track the track
|
||||
* @param lastLocation the last location
|
||||
*/
|
||||
public void writeEndTrack(Track track, Location lastLocation);
|
||||
|
||||
/**
|
||||
* Writes the statements necessary to open a new segment.
|
||||
*/
|
||||
public void writeOpenSegment();
|
||||
|
||||
/**
|
||||
* Writes the statements necessary to close a segment.
|
||||
*/
|
||||
public void writeCloseSegment();
|
||||
|
||||
/**
|
||||
* Writes a location.
|
||||
*
|
||||
* @param location the location
|
||||
*/
|
||||
public void writeLocation(Location location);
|
||||
}
|
||||
@@ -0,0 +1,299 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
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 com.google.common.annotations.VisibleForTesting;
|
||||
|
||||
import android.content.Context;
|
||||
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 TrackWriter {
|
||||
|
||||
/**
|
||||
* Listener for when a track location is written.
|
||||
*/
|
||||
public interface OnWriteListener {
|
||||
|
||||
/**
|
||||
* When a track location is written.
|
||||
*
|
||||
* @param number the location number
|
||||
* @param max the maximum number of locations in a track, for calculation of completion
|
||||
* percentage
|
||||
*/
|
||||
public void onWrite(int number, int max);
|
||||
}
|
||||
|
||||
private final static String TAG = TrackWriter.class.getSimpleName();
|
||||
|
||||
private final MyTracksProviderUtils myTracksProviderUtils;
|
||||
private final Track[] tracks;
|
||||
private final TrackFormatWriter trackFormatWriter;
|
||||
private final OnWriteListener onWriteListener;
|
||||
|
||||
private Thread writeThread;
|
||||
private boolean success = false;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param context the context
|
||||
* @param myTracksProviderUtils the my tracks provider utils
|
||||
* @param tracks the tracks
|
||||
* @param trackFileFormat the track file format
|
||||
* @param onWriteListener the on write listener
|
||||
*/
|
||||
public TrackWriter(Context context, MyTracksProviderUtils myTracksProviderUtils, Track[] tracks,
|
||||
TrackFileFormat trackFileFormat, OnWriteListener onWriteListener) {
|
||||
this(myTracksProviderUtils, tracks, trackFileFormat.newFormatWriter(context), onWriteListener);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public TrackWriter(MyTracksProviderUtils myTracksProviderUtils, Track[] tracks,
|
||||
TrackFormatWriter trackFormatWriter, OnWriteListener onWriteListener) {
|
||||
this.myTracksProviderUtils = myTracksProviderUtils;
|
||||
this.tracks = tracks;
|
||||
this.trackFormatWriter = trackFormatWriter;
|
||||
this.onWriteListener = onWriteListener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the write completed successfully.
|
||||
*/
|
||||
public boolean wasSuccess() {
|
||||
return success;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the given track to the output stream.
|
||||
*
|
||||
* @param outputStream the output stream.
|
||||
*/
|
||||
public void writeTrack(final OutputStream outputStream) {
|
||||
writeThread = new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
trackFormatWriter.prepare(outputStream);
|
||||
trackFormatWriter.writeHeader(tracks[0]);
|
||||
long startTime = tracks[0].getTripStatistics().getStartTime();
|
||||
for (int i = 0; i < tracks.length; i++) {
|
||||
writeWaypoints(tracks[i]);
|
||||
long offset = tracks[i].getTripStatistics().getStartTime() - startTime;
|
||||
writeLocations(tracks[i], offset);
|
||||
}
|
||||
trackFormatWriter.writeFooter();
|
||||
trackFormatWriter.close();
|
||||
success = true;
|
||||
} catch (InterruptedException e) {
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
writeThread.start();
|
||||
try {
|
||||
writeThread.join();
|
||||
} catch (InterruptedException e) {
|
||||
Log.e(TAG, "Interrupted while waiting for write to complete", e);
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops any in-progress writes.
|
||||
*/
|
||||
public void stopWriteTrack() {
|
||||
if (writeThread != null && writeThread.isAlive()) {
|
||||
Log.i(TAG, "Attempting to stop track write");
|
||||
writeThread.interrupt();
|
||||
|
||||
try {
|
||||
writeThread.join();
|
||||
Log.i(TAG, "Track write stopped");
|
||||
} catch (InterruptedException e) {
|
||||
Log.e(TAG, "Interrupted while waiting for writer to stop", e);
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the waypoints.
|
||||
*/
|
||||
private void writeWaypoints(Track track) {
|
||||
/*
|
||||
* 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 (!hasWaypoints) {
|
||||
trackFormatWriter.writeBeginWaypoints();
|
||||
hasWaypoints = true;
|
||||
}
|
||||
Waypoint waypoint = myTracksProviderUtils.createWaypoint(cursor);
|
||||
trackFormatWriter.writeWaypoint(waypoint);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
if (hasWaypoints) {
|
||||
trackFormatWriter.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();
|
||||
LocationIterator iterator = myTracksProviderUtils.getTrackPointLocationIterator(
|
||||
track.getId(), -1L, false, locationFactory);
|
||||
|
||||
try {
|
||||
int locationNumber = 0;
|
||||
while (iterator.hasNext()) {
|
||||
Location location = iterator.next();
|
||||
setLocationTime(location, offset);
|
||||
if (Thread.interrupted()) {
|
||||
throw new InterruptedException();
|
||||
}
|
||||
locationNumber++;
|
||||
|
||||
boolean isLocationValid = LocationUtils.isValidLocation(location);
|
||||
boolean isSegmentValid = isLocationValid && isLastLocationValid;
|
||||
if (!wroteTrack && isSegmentValid) {
|
||||
// Found the first two consecutive locations that are valid
|
||||
trackFormatWriter.writeBeginTrack(track, locationFactory.lastLocation);
|
||||
wroteTrack = true;
|
||||
}
|
||||
|
||||
if (isSegmentValid) {
|
||||
if (!wroteSegment) {
|
||||
// Start a segment
|
||||
trackFormatWriter.writeOpenSegment();
|
||||
wroteSegment = true;
|
||||
|
||||
// Write the previous location, which we had previously skipped
|
||||
trackFormatWriter.writeLocation(locationFactory.lastLocation);
|
||||
}
|
||||
|
||||
// Write the current location
|
||||
trackFormatWriter.writeLocation(location);
|
||||
if (onWriteListener != null) {
|
||||
onWriteListener.onWrite(locationNumber, track.getNumberOfPoints());
|
||||
}
|
||||
} else {
|
||||
if (wroteSegment) {
|
||||
trackFormatWriter.writeCloseSegment();
|
||||
wroteSegment = false;
|
||||
}
|
||||
}
|
||||
locationFactory.swapLocations();
|
||||
isLastLocationValid = isLocationValid;
|
||||
}
|
||||
|
||||
if (wroteSegment) {
|
||||
trackFormatWriter.writeCloseSegment();
|
||||
wroteSegment = false;
|
||||
}
|
||||
if (wroteTrack) {
|
||||
Location lastValidTrackPoint = myTracksProviderUtils.getLastValidTrackPoint(track.getId());
|
||||
setLocationTime(lastValidTrackPoint, offset);
|
||||
trackFormatWriter.writeEndTrack(track, lastValidTrackPoint);
|
||||
} else {
|
||||
// Write an empty track
|
||||
trackFormatWriter.writeBeginTrack(track, null);
|
||||
trackFormatWriter.writeEndTrack(track, null);
|
||||
}
|
||||
} finally {
|
||||
iterator.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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+69
@@ -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.send_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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+405
@@ -0,0 +1,405 @@
|
||||
// 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.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 void saveResult() {}
|
||||
|
||||
@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(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;
|
||||
}
|
||||
|
||||
track.setTableId(tableId);
|
||||
myTracksProviderUtils.updateTrack(track);
|
||||
|
||||
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(String tableId) throws IOException, GoogleAuthException {
|
||||
boolean defaultTablePublic = PreferencesUtils.getBoolean(context,
|
||||
R.string.default_table_public_key,
|
||||
PreferencesUtils.DEFAULT_TABLE_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.setType("anyone");
|
||||
permission.setRole("reader");
|
||||
drive.permissions().insert(tableId, permission).execute();
|
||||
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 {
|
||||
Cursor cursor = null;
|
||||
try {
|
||||
cursor = myTracksProviderUtils.getTrackPointCursor(trackId, -1L, -1, false);
|
||||
if (cursor == null) {
|
||||
Log.d(TAG, "Location cursor is null");
|
||||
return false;
|
||||
}
|
||||
|
||||
int count = cursor.getCount();
|
||||
List<Location> locations = new ArrayList<Location>(MAX_POINTS_PER_UPLOAD);
|
||||
Location lastLocation = null;
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
cursor.moveToPosition(i);
|
||||
|
||||
Location location = myTracksProviderUtils.createTrackPoint(cursor);
|
||||
locations.add(location);
|
||||
|
||||
if (i == 0) {
|
||||
// Create a start marker
|
||||
String name = context.getString(R.string.marker_label_start, track.getName());
|
||||
createNewPoint(fusiontables, tableId, name, "", location, MARKER_TYPE_START);
|
||||
}
|
||||
|
||||
if (LocationUtils.isValidLocation(location)) {
|
||||
lastLocation = location;
|
||||
}
|
||||
|
||||
// Upload periodically
|
||||
int readCount = i + 1;
|
||||
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, count);
|
||||
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 (lastLocation != 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, lastLocation, MARKER_TYPE_END);
|
||||
}
|
||||
return true;
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.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);
|
||||
}
|
||||
}
|
||||
+158
@@ -0,0 +1,158 @@
|
||||
/*
|
||||
* 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
|
||||
* @return the url.
|
||||
*/
|
||||
public static String getMapUrl(Track track) {
|
||||
if (track == null
|
||||
|| track.getTripStatistics() == null
|
||||
|| track.getTableId() == null
|
||||
|| track.getTableId().length() == 0) {
|
||||
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, track.getTableId(), 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,51 @@
|
||||
/*
|
||||
* 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.android.apps.mytracks.Constants;
|
||||
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 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(Constants.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 <entry>.
|
||||
*
|
||||
* @param plainEntry The entry that will be filled.
|
||||
* @throws XmlPullParserException Thrown if the XML cannot be parsed.
|
||||
* @throws IOException Thrown if the underlying inputstream cannot be read.
|
||||
*/
|
||||
@Override
|
||||
protected void handleEntry(Entry plainEntry)
|
||||
throws XmlPullParserException, IOException, ParseException {
|
||||
XmlPullParser parser = getParser();
|
||||
if (!(plainEntry instanceof MapFeatureEntry)) {
|
||||
throw new IllegalArgumentException("Expected MapFeatureEntry!");
|
||||
}
|
||||
MapFeatureEntry entry = (MapFeatureEntry) plainEntry;
|
||||
|
||||
int eventType = parser.getEventType();
|
||||
entry.setPrivacy("public");
|
||||
while (eventType != XmlPullParser.END_DOCUMENT) {
|
||||
switch (eventType) {
|
||||
case XmlPullParser.START_TAG:
|
||||
String name = parser.getName();
|
||||
if ("entry".equals(name)) {
|
||||
// stop parsing here.
|
||||
return;
|
||||
} else if ("id".equals(name)) {
|
||||
entry.setId(XmlUtils.extractChildText(parser));
|
||||
} else if ("title".equals(name)) {
|
||||
entry.setTitle(XmlUtils.extractChildText(parser));
|
||||
} else if ("link".equals(name)) {
|
||||
String rel = parser.getAttributeValue(null /* ns */, "rel");
|
||||
String type = parser.getAttributeValue(null /* ns */, "type");
|
||||
String href = parser.getAttributeValue(null /* ns */, "href");
|
||||
if ("edit".equals(rel)) {
|
||||
entry.setEditUri(href);
|
||||
} else if ("alternate".equals(rel) && "text/html".equals(type)) {
|
||||
entry.setHtmlUri(href);
|
||||
} else {
|
||||
handleExtraLinkInEntry(rel, type, href, entry);
|
||||
}
|
||||
} else if ("summary".equals(name)) {
|
||||
entry.setSummary(XmlUtils.extractChildText(parser));
|
||||
} else if ("content".equals(name)) {
|
||||
StringBuilder contentBuilder = new StringBuilder();
|
||||
int parentDepth = parser.getDepth();
|
||||
while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
|
||||
int etype = parser.next();
|
||||
switch (etype) {
|
||||
case XmlPullParser.START_TAG:
|
||||
contentBuilder.append('<');
|
||||
contentBuilder.append(parser.getName());
|
||||
contentBuilder.append('>');
|
||||
break;
|
||||
case XmlPullParser.TEXT:
|
||||
contentBuilder.append("<![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();
|
||||
}
|
||||
}
|
||||
}
|
||||
+94
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+262
@@ -0,0 +1,262 @@
|
||||
// Copyright 2010 Google Inc. All Rights Reserved.
|
||||
package com.google.android.apps.mytracks.io.gdata.maps;
|
||||
|
||||
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", "application/vnd.google-earth.kml+xml");
|
||||
serializer.flush();
|
||||
stream.write(content.getBytes());
|
||||
} else {
|
||||
serializer.text(content);
|
||||
}
|
||||
serializer.endTag(null /* ns */, "content");
|
||||
}
|
||||
|
||||
private static void serializeAuthor(XmlSerializer serializer, String author,
|
||||
String email) throws IOException {
|
||||
if (StringUtils.isEmpty(author) || StringUtils.isEmpty(email)) {
|
||||
return;
|
||||
}
|
||||
serializer.startTag(null /* ns */, "author");
|
||||
serializer.startTag(null /* ns */, "name");
|
||||
serializer.text(author);
|
||||
serializer.endTag(null /* ns */, "name");
|
||||
serializer.startTag(null /* ns */, "email");
|
||||
serializer.text(email);
|
||||
serializer.endTag(null /* ns */, "email");
|
||||
serializer.endTag(null /* ns */, "author");
|
||||
}
|
||||
|
||||
private static void serializeCategory(XmlSerializer serializer,
|
||||
String category, String categoryScheme) throws IOException {
|
||||
if (StringUtils.isEmpty(category) && StringUtils.isEmpty(categoryScheme)) {
|
||||
return;
|
||||
}
|
||||
serializer.startTag(null /* ns */, "category");
|
||||
if (!StringUtils.isEmpty(category)) {
|
||||
serializer.attribute(null /* ns */, "term", category);
|
||||
}
|
||||
if (!StringUtils.isEmpty(categoryScheme)) {
|
||||
serializer.attribute(null /* ns */, "scheme", categoryScheme);
|
||||
}
|
||||
serializer.endTag(null /* ns */, "category");
|
||||
}
|
||||
|
||||
private static void serializePublicationDate(XmlSerializer serializer,
|
||||
String publicationDate) throws IOException {
|
||||
if (StringUtils.isEmpty(publicationDate)) {
|
||||
return;
|
||||
}
|
||||
serializer.startTag(null /* ns */, "published");
|
||||
serializer.text(publicationDate);
|
||||
serializer.endTag(null /* ns */, "published");
|
||||
}
|
||||
|
||||
private static void serializeUpdateDate(XmlSerializer serializer,
|
||||
String updateDate) throws IOException {
|
||||
if (StringUtils.isEmpty(updateDate)) {
|
||||
return;
|
||||
}
|
||||
serializer.startTag(null /* ns */, "updated");
|
||||
serializer.text(updateDate);
|
||||
serializer.endTag(null /* ns */, "updated");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void serializeExtraEntryContents(XmlSerializer serializer,
|
||||
int format) throws IOException {
|
||||
Map<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,248 @@
|
||||
/*
|
||||
* 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.gdata.maps.MapsMapMetadata;
|
||||
import com.google.android.apps.mytracks.io.sendtogoogle.SendRequest;
|
||||
import com.google.android.apps.mytracks.util.DialogUtils;
|
||||
import com.google.android.apps.mytracks.util.IntentUtils;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.app.Dialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Color;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.ListView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* An activity to choose a Google Map.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class ChooseMapActivity extends Activity {
|
||||
|
||||
private static final int DIALOG_PROGRESS_ID = 0;
|
||||
private static final int DIALOG_ERROR_ID = 1;
|
||||
|
||||
private SendRequest sendRequest;
|
||||
private ChooseMapAsyncTask asyncTask;
|
||||
@VisibleForTesting
|
||||
ArrayAdapter<ListItem> arrayAdapter;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
sendRequest = getIntent().getParcelableExtra(SendRequest.SEND_REQUEST_KEY);
|
||||
setContentView(R.layout.choose_map);
|
||||
|
||||
arrayAdapter = new ArrayAdapter<ListItem>(this, R.layout.choose_map_item, new ArrayList<
|
||||
ListItem>()) {
|
||||
@Override
|
||||
public View getView(int position, View convertView, ViewGroup parent) {
|
||||
if (convertView == null) {
|
||||
convertView = getLayoutInflater().inflate(R.layout.choose_map_item, parent, false);
|
||||
}
|
||||
MapsMapMetadata mapData = getItem(position).getMapData();
|
||||
|
||||
TextView title = (TextView) convertView.findViewById(R.id.choose_map_item_title);
|
||||
title.setText(mapData.getTitle());
|
||||
|
||||
TextView description = (TextView) convertView.findViewById(
|
||||
R.id.choose_map_item_description);
|
||||
String descriptionText = mapData.getDescription();
|
||||
if (descriptionText == null || descriptionText.equals("")) {
|
||||
description.setVisibility(View.GONE);
|
||||
} else {
|
||||
description.setVisibility(View.VISIBLE);
|
||||
description.setText(descriptionText);
|
||||
}
|
||||
|
||||
TextView searchStatus = (TextView) convertView.findViewById(
|
||||
R.id.choose_map_item_search_status);
|
||||
searchStatus.setTextColor(mapData.getSearchable() ? Color.RED : Color.GREEN);
|
||||
searchStatus.setText(mapData.getSearchable() ? R.string.maps_list_public_label
|
||||
: R.string.maps_list_unlisted_label);
|
||||
return convertView;
|
||||
}
|
||||
};
|
||||
|
||||
ListView list = (ListView) findViewById(R.id.choose_map_list_view);
|
||||
list.setEmptyView(findViewById(R.id.choose_map_empty_view));
|
||||
list.setOnItemClickListener(new AdapterView.OnItemClickListener() {
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
|
||||
startNextActivity(arrayAdapter.getItem(position).getMapId());
|
||||
}
|
||||
});
|
||||
list.setAdapter(arrayAdapter);
|
||||
|
||||
Object retained = getLastNonConfigurationInstance();
|
||||
if (retained instanceof ChooseMapAsyncTask) {
|
||||
asyncTask = (ChooseMapAsyncTask) retained;
|
||||
asyncTask.setActivity(this);
|
||||
} else {
|
||||
asyncTask = new ChooseMapAsyncTask(this, sendRequest.getAccount());
|
||||
asyncTask.execute();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object onRetainNonConfigurationInstance() {
|
||||
asyncTask.setActivity(null);
|
||||
return asyncTask;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Dialog onCreateDialog(int id) {
|
||||
switch (id) {
|
||||
case DIALOG_PROGRESS_ID:
|
||||
return DialogUtils.createSpinnerProgressDialog(
|
||||
this, R.string.maps_list_progress_message, new DialogInterface.OnCancelListener() {
|
||||
@Override
|
||||
public void onCancel(DialogInterface dialog) {
|
||||
asyncTask.cancel(true);
|
||||
finish();
|
||||
}
|
||||
});
|
||||
case DIALOG_ERROR_ID:
|
||||
return new AlertDialog.Builder(this)
|
||||
.setCancelable(true)
|
||||
.setIcon(android.R.drawable.ic_dialog_alert)
|
||||
.setMessage(R.string.maps_list_error)
|
||||
.setOnCancelListener(new DialogInterface.OnCancelListener() {
|
||||
@Override
|
||||
public void onCancel(DialogInterface dialog) {
|
||||
finish();
|
||||
}
|
||||
})
|
||||
.setPositiveButton(R.string.generic_ok, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int arg1) {
|
||||
finish();
|
||||
}
|
||||
})
|
||||
.setTitle(R.string.generic_error_title)
|
||||
.create();
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes when the associated AsyncTask completes.
|
||||
*
|
||||
* @param success true if success
|
||||
* @param mapIds an array of map ids
|
||||
* @param mapData an array of map data
|
||||
*/
|
||||
public void onAsyncTaskCompleted(
|
||||
boolean success, ArrayList<String> mapIds, ArrayList<MapsMapMetadata> mapData) {
|
||||
removeProgressDialog();
|
||||
if (success) {
|
||||
arrayAdapter.clear();
|
||||
// To prevent displaying the emptyView message momentarily before the
|
||||
// arrayAdapter is set, don't set the emptyView message in the xml layout.
|
||||
// Instead, set it only when needed.
|
||||
if (mapIds.size() == 0) {
|
||||
TextView emptyView = (TextView) findViewById(R.id.choose_map_empty_view);
|
||||
emptyView.setText(R.string.maps_list_no_maps);
|
||||
} else {
|
||||
for (int i = 0; i < mapIds.size(); i++) {
|
||||
arrayAdapter.add(new ListItem(mapIds.get(i), mapData.get(i)));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
showErrorDialog();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the progress dialog.
|
||||
*/
|
||||
public void showProgressDialog() {
|
||||
showDialog(DIALOG_PROGRESS_ID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the error dialog.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
void showErrorDialog() {
|
||||
showDialog(DIALOG_ERROR_ID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the progress dialog.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
void removeProgressDialog() {
|
||||
removeDialog(DIALOG_PROGRESS_ID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the next activity, {@link SendMapsActivity}.
|
||||
*
|
||||
* @param mapId the chosen map id
|
||||
*/
|
||||
private void startNextActivity(String mapId) {
|
||||
sendRequest.setMapsExistingMapId(mapId);
|
||||
Intent intent = IntentUtils.newIntent(this, SendMapsActivity.class)
|
||||
.putExtra(SendRequest.SEND_REQUEST_KEY, sendRequest);
|
||||
startActivity(intent);
|
||||
finish();
|
||||
}
|
||||
|
||||
/**
|
||||
* A class containing {@link ChooseMapActivity} list item.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
@VisibleForTesting
|
||||
class ListItem {
|
||||
private String mapId;
|
||||
private MapsMapMetadata mapData;
|
||||
|
||||
private ListItem(String mapId, MapsMapMetadata mapData) {
|
||||
this.mapId = mapId;
|
||||
this.mapData = mapData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the map id.
|
||||
*/
|
||||
public String getMapId() {
|
||||
return mapId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the map data.
|
||||
*/
|
||||
public MapsMapMetadata getMapData() {
|
||||
return mapData;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
/*
|
||||
* 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.io.gdata.GDataClientFactory;
|
||||
import com.google.android.apps.mytracks.io.gdata.maps.MapFeatureEntry;
|
||||
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.MapsMapMetadata;
|
||||
import com.google.android.apps.mytracks.io.gdata.maps.XmlMapsGDataParserFactory;
|
||||
import com.google.android.common.gdata.AndroidXmlParserFactory;
|
||||
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.GDataParser;
|
||||
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.os.AsyncTask;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* AsyncTask for {@link ChooseMapActivity} to get all the maps from Google Maps.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class ChooseMapAsyncTask extends AsyncTask<Void, Integer, Boolean> {
|
||||
private static final String TAG = ChooseMapAsyncTask.class.getSimpleName();
|
||||
|
||||
private ChooseMapActivity activity;
|
||||
private final Account account;
|
||||
private final Context context;
|
||||
private final GDataClient gDataClient;
|
||||
private final MapsClient mapsClient;
|
||||
|
||||
/**
|
||||
* True if can retry sending to Google Fusion Tables.
|
||||
*/
|
||||
private boolean canRetry;
|
||||
|
||||
/**
|
||||
* True if the AsyncTask has completed.
|
||||
*/
|
||||
private boolean completed;
|
||||
|
||||
/**
|
||||
* True if the result is success.
|
||||
*/
|
||||
private boolean success;
|
||||
|
||||
// The following variables are for per request states
|
||||
private String authToken;
|
||||
private ArrayList<String> mapIds;
|
||||
private ArrayList<MapsMapMetadata> mapData;
|
||||
|
||||
public ChooseMapAsyncTask(ChooseMapActivity activity, Account account) {
|
||||
this.activity = activity;
|
||||
this.account = account;
|
||||
|
||||
this.context = activity.getApplicationContext();
|
||||
this.gDataClient = GDataClientFactory
|
||||
.getGDataClient(activity.getApplicationContext());
|
||||
this.mapsClient = new MapsClient(
|
||||
GDataClientFactory.getGDataClient(activity.getApplicationContext()),
|
||||
new XmlMapsGDataParserFactory(new AndroidXmlParserFactory()));
|
||||
|
||||
canRetry = true;
|
||||
completed = false;
|
||||
success = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the activity associated with this AyncTask.
|
||||
*
|
||||
* @param activity the activity.
|
||||
*/
|
||||
public void setActivity(ChooseMapActivity activity) {
|
||||
this.activity = activity;
|
||||
if (completed && activity != null) {
|
||||
activity.onAsyncTaskCompleted(success, mapIds, mapData);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
activity.showProgressDialog();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Boolean doInBackground(Void... params) {
|
||||
try {
|
||||
return getMaps();
|
||||
} finally {
|
||||
if (gDataClient != null) {
|
||||
gDataClient.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Boolean result) {
|
||||
success = result;
|
||||
completed = true;
|
||||
if (activity != null) {
|
||||
activity.onAsyncTaskCompleted(success, mapIds, mapData);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all the maps from Google Maps.
|
||||
*
|
||||
* @return true if success.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
boolean getMaps() {
|
||||
// Reset the per request states
|
||||
authToken = null;
|
||||
mapIds = new ArrayList<String>();
|
||||
mapData = new ArrayList<MapsMapMetadata>();
|
||||
|
||||
try {
|
||||
authToken = AccountManager.get(context).blockingGetAuthToken(
|
||||
account, MapsConstants.SERVICE_NAME, false);
|
||||
} catch (OperationCanceledException e) {
|
||||
Log.d(TAG, "Unable to get auth token", e);
|
||||
return retryUpload();
|
||||
} catch (AuthenticatorException e) {
|
||||
Log.d(TAG, "Unable to get auth token", e);
|
||||
return retryUpload();
|
||||
} catch (IOException e) {
|
||||
Log.d(TAG, "Unable to get auth token", e);
|
||||
return retryUpload();
|
||||
}
|
||||
|
||||
if (isCancelled()) {
|
||||
return false;
|
||||
}
|
||||
GDataParser gDataParser = null;
|
||||
try {
|
||||
gDataParser = mapsClient.getParserForFeed(
|
||||
MapFeatureEntry.class, MapsClient.getMapsFeed(), authToken);
|
||||
gDataParser.init();
|
||||
while (gDataParser.hasMoreData()) {
|
||||
MapFeatureEntry entry = (MapFeatureEntry) gDataParser.readNextEntry(null);
|
||||
mapIds.add(MapsGDataConverter.getMapidForEntry(entry));
|
||||
mapData.add(MapsGDataConverter.getMapMetadataForEntry(entry));
|
||||
}
|
||||
} catch (ParseException e) {
|
||||
Log.d(TAG, "Unable to get maps", e);
|
||||
return retryUpload();
|
||||
} catch (IOException e) {
|
||||
Log.d(TAG, "Unable to get maps", e);
|
||||
return retryUpload();
|
||||
} catch (HttpException e) {
|
||||
Log.d(TAG, "Unable to get maps", e);
|
||||
return retryUpload();
|
||||
} finally {
|
||||
if (gDataParser != null) {
|
||||
gDataParser.close();
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retries upload. Invalidates the authToken. If can retry, invokes
|
||||
* {@link ChooseMapAsyncTask#getMaps()}. Returns false if cannot retry.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
boolean retryUpload() {
|
||||
if (isCancelled()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
AccountManager.get(context).invalidateAuthToken(Constants.ACCOUNT_TYPE, authToken);
|
||||
if (canRetry) {
|
||||
canRetry = false;
|
||||
return getMaps();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the complete status of task.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
boolean getCompleted() {
|
||||
return completed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the complete status of task.
|
||||
* @param completed
|
||||
*/
|
||||
@VisibleForTesting
|
||||
void setCompleted(boolean completed) {
|
||||
this.completed = completed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the status of canRetry.
|
||||
* @param canRetry status of canRetry
|
||||
*/
|
||||
@VisibleForTesting
|
||||
void setCanRetry(boolean canRetry) {
|
||||
this.canRetry = canRetry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the status of canRetry.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
boolean getCanRetry() {
|
||||
return canRetry;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* 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(), sendRequest.getMapsExistingMapId());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getServiceName() {
|
||||
return getString(R.string.send_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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user