mirror of
https://codeberg.org/OpenTracksApp/OpenTracks.git
synced 2026-10-03 18:03:06 +02:00
Handles interrupt and error cleanup during import
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,425 @@
|
||||
/*
|
||||
* 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.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.ConfirmDeleteDialogFragment;
|
||||
import com.google.android.apps.mytracks.fragments.ConfirmDeleteDialogFragment.ConfirmDeleteCaller;
|
||||
import com.google.android.apps.mytracks.fragments.ConfirmPlayDialogFragment;
|
||||
import com.google.android.apps.mytracks.fragments.ConfirmPlayDialogFragment.ConfirmPlayCaller;
|
||||
import com.google.android.apps.mytracks.fragments.ConfirmSyncDialogFragment;
|
||||
import com.google.android.apps.mytracks.fragments.ConfirmSyncDialogFragment.ConfirmSyncCaller;
|
||||
import com.google.android.apps.mytracks.fragments.InstallEarthDialogFragment;
|
||||
import com.google.android.apps.mytracks.fragments.ShareTrackDialogFragment;
|
||||
import com.google.android.apps.mytracks.fragments.ShareTrackDialogFragment.ShareTrackCaller;
|
||||
import com.google.android.apps.mytracks.io.drive.SendDriveActivity;
|
||||
import com.google.android.apps.mytracks.io.file.TrackFileFormat;
|
||||
import com.google.android.apps.mytracks.io.file.export.SaveActivity;
|
||||
import com.google.android.apps.mytracks.io.fusiontables.SendFusionTablesActivity;
|
||||
import com.google.android.apps.mytracks.io.gdata.maps.MapsConstants;
|
||||
import com.google.android.apps.mytracks.io.maps.SendMapsActivity;
|
||||
import com.google.android.apps.mytracks.io.sendtogoogle.SendRequest;
|
||||
import com.google.android.apps.mytracks.io.sendtogoogle.SendToGoogleUtils;
|
||||
import com.google.android.apps.mytracks.io.sendtogoogle.UploadResultActivity;
|
||||
import com.google.android.apps.mytracks.io.spreadsheets.SendSpreadsheetsActivity;
|
||||
import com.google.android.apps.mytracks.io.sync.SyncUtils;
|
||||
import com.google.android.apps.mytracks.services.TrackRecordingServiceConnection;
|
||||
import com.google.android.apps.mytracks.util.AnalyticsUtils;
|
||||
import com.google.android.apps.mytracks.util.GoogleEarthUtils;
|
||||
import com.google.android.apps.mytracks.util.IntentUtils;
|
||||
import com.google.android.apps.mytracks.util.PreferencesUtils;
|
||||
import com.google.android.apps.mytracks.util.TrackRecordingServiceConnectionUtils;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.accounts.Account;
|
||||
import android.accounts.AccountManager;
|
||||
import android.accounts.AccountManagerCallback;
|
||||
import android.accounts.AccountManagerFuture;
|
||||
import android.accounts.AuthenticatorException;
|
||||
import android.accounts.OperationCanceledException;
|
||||
import android.app.Activity;
|
||||
import android.content.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, ConfirmSyncCaller, CheckPermissionCaller, ShareTrackCaller, ConfirmPlayCaller,
|
||||
ConfirmDeleteCaller {
|
||||
|
||||
private static final String TAG = AbstractMyTracksActivity.class.getSimpleName();
|
||||
private static final String SEND_REQUEST_KEY = "send_request_key";
|
||||
private static final int DRIVE_REQUEST_CODE = 0;
|
||||
private static final int FUSION_TABLES_REQUEST_CODE = 1;
|
||||
private static final int SPREADSHEETS_REQUEST_CODE = 2;
|
||||
private static final int DELETE_REQUEST_CODE = 3;
|
||||
protected static final int GOOGLE_PLAY_SERVICES_REQUEST_CODE = 4;
|
||||
protected static final int CAMERA_REQUEST_CODE = 5;
|
||||
|
||||
private SendRequest sendRequest;
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Share a track.
|
||||
*
|
||||
* @param trackId the track id
|
||||
*/
|
||||
protected void shareTrack(long trackId) {
|
||||
AnalyticsUtils.sendPageViews(this, AnalyticsUtils.ACTION_SHARE_DRIVE);
|
||||
SendRequest newRequest;
|
||||
newRequest = new SendRequest(trackId);
|
||||
newRequest.setSendDrive(true);
|
||||
newRequest.setDriveShare(true);
|
||||
sendToGoogle(newRequest);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a request to Google.
|
||||
*
|
||||
* @param request the request
|
||||
*/
|
||||
protected void sendToGoogle(SendRequest request) {
|
||||
sendRequest = request;
|
||||
new ChooseAccountDialogFragment().show(
|
||||
getSupportFragmentManager(), ChooseAccountDialogFragment.CHOOSE_ACCOUNT_DIALOG_TAG);
|
||||
}
|
||||
|
||||
@Override
|
||||
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));
|
||||
|
||||
if (sendRequest.isSendDrive() && sendRequest.isDriveSync() && sendRequest.isDriveSyncConfirm()) {
|
||||
new ConfirmSyncDialogFragment().show(
|
||||
getSupportFragmentManager(), ConfirmSyncDialogFragment.CONFIRM_SYNC_DIALOG_TAG);
|
||||
} else {
|
||||
onConfirmSyncDone(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfirmSyncDone(boolean enable) {
|
||||
if (enable) {
|
||||
// Check Drive permission
|
||||
boolean needDrivePermission = sendRequest.isSendDrive();
|
||||
if (!needDrivePermission && sendRequest.isSendFusionTables()) {
|
||||
needDrivePermission = PreferencesUtils.getBoolean(this,
|
||||
R.string.export_google_fusion_tables_public_key,
|
||||
PreferencesUtils.EXPORT_GOOGLE_FUSION_TABLES_PUBLIC_DEFAULT);
|
||||
}
|
||||
if (!needDrivePermission) {
|
||||
needDrivePermission = sendRequest.isSendSpreadsheets();
|
||||
}
|
||||
|
||||
if (needDrivePermission) {
|
||||
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 = DRIVE_REQUEST_CODE;
|
||||
} else if (scope.equals(SendToGoogleUtils.FUSION_TABLES_SCOPE)) {
|
||||
requestCode = FUSION_TABLES_REQUEST_CODE;
|
||||
} else {
|
||||
requestCode = SPREADSHEETS_REQUEST_CODE;
|
||||
}
|
||||
startActivityForResult(intent, requestCode);
|
||||
} else {
|
||||
onPermissionFailure();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
switch (requestCode) {
|
||||
case DRIVE_REQUEST_CODE:
|
||||
SendToGoogleUtils.cancelNotification(this, SendToGoogleUtils.DRIVE_NOTIFICATION_ID);
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
onDrivePermissionSuccess();
|
||||
} else {
|
||||
onPermissionFailure();
|
||||
}
|
||||
break;
|
||||
case FUSION_TABLES_REQUEST_CODE:
|
||||
SendToGoogleUtils.cancelNotification(this, SendToGoogleUtils.FUSION_TABLES_NOTIFICATION_ID);
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
onFusionTablesSuccess();
|
||||
} else {
|
||||
onPermissionFailure();
|
||||
}
|
||||
break;
|
||||
case SPREADSHEETS_REQUEST_CODE:
|
||||
SendToGoogleUtils.cancelNotification(this, SendToGoogleUtils.SPREADSHEETS_NOTIFICATION_ID);
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
onSpreadsheetsPermissionSuccess();
|
||||
} else {
|
||||
onPermissionFailure();
|
||||
}
|
||||
break;
|
||||
case DELETE_REQUEST_CODE:
|
||||
onDeleted();
|
||||
break;
|
||||
default:
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
}
|
||||
}
|
||||
|
||||
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.SPREADSHEETS_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 ShareTrackDialogFragment}
|
||||
* <p>
|
||||
* isSendDrive -> start {@link SendDriveActivity}
|
||||
* <p>
|
||||
* isSendMaps -> start {@link SendMapsActivity}
|
||||
* <p>
|
||||
* isSendFusionTables -> start {@link SendFusionTablesActivity}
|
||||
* <p>
|
||||
* isSendSpreadsheets -> start {@link SendSpreadsheetsActivity}
|
||||
* <p>
|
||||
* else -> start {@link UploadResultActivity}
|
||||
*/
|
||||
private void onSpreadsheetsPermissionSuccess() {
|
||||
Class<?> next;
|
||||
if (sendRequest.isSendDrive()) {
|
||||
if (sendRequest.isDriveSync()) {
|
||||
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()) {
|
||||
ShareTrackDialogFragment.newInstance(sendRequest.getTrackId())
|
||||
.show(getSupportFragmentManager(), ShareTrackDialogFragment.SHARE_TRACK_DIALOG_TAG);
|
||||
return;
|
||||
} else {
|
||||
next = SendDriveActivity.class;
|
||||
}
|
||||
} else if (sendRequest.isSendMaps()) {
|
||||
next = SendMapsActivity.class;
|
||||
} else if (sendRequest.isSendFusionTables()) {
|
||||
next = SendFusionTablesActivity.class;
|
||||
} else if (sendRequest.isSendSpreadsheets()) {
|
||||
next = SendSpreadsheetsActivity.class;
|
||||
} else {
|
||||
next = UploadResultActivity.class;
|
||||
}
|
||||
Intent intent = IntentUtils.newIntent(this, next)
|
||||
.putExtra(SendRequest.SEND_REQUEST_KEY, sendRequest);
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Call when not able to get permission for a google service.
|
||||
*/
|
||||
private void onPermissionFailure() {
|
||||
Toast.makeText(this, R.string.send_google_no_account_permission, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onShareTrackDone(String emails, boolean makePublic) {
|
||||
sendRequest.setDriveShareEmails(emails);
|
||||
sendRequest.setDriveSharePublic(makePublic);
|
||||
Intent intent = IntentUtils.newIntent(this, SendDriveActivity.class)
|
||||
.putExtra(SendRequest.SEND_REQUEST_KEY, sendRequest);
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirm playing tracks in Google Earth.
|
||||
*
|
||||
* @param trackIds the track ids
|
||||
*/
|
||||
protected void confirmPlay(long[] trackIds) {
|
||||
AnalyticsUtils.sendPageViews(this, AnalyticsUtils.ACTION_PLAY);
|
||||
if (GoogleEarthUtils.isEarthInstalled(this)) {
|
||||
if (PreferencesUtils.getBoolean(
|
||||
this, R.string.confirm_play_earth_key, PreferencesUtils.CONFIRM_PLAY_EARTH_DEFAULT)) {
|
||||
ConfirmPlayDialogFragment.newInstance(trackIds)
|
||||
.show(getSupportFragmentManager(), ConfirmPlayDialogFragment.CONFIRM_PLAY_DIALOG_TAG);
|
||||
} else {
|
||||
onConfirmPlayDone(trackIds);
|
||||
}
|
||||
} else {
|
||||
new InstallEarthDialogFragment().show(
|
||||
getSupportFragmentManager(), InstallEarthDialogFragment.INSTALL_EARTH_DIALOG_TAG);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfirmPlayDone(long[] trackIds) {
|
||||
Intent intent = IntentUtils.newIntent(this, SaveActivity.class)
|
||||
.putExtra(SaveActivity.EXTRA_TRACK_IDS, trackIds)
|
||||
.putExtra(SaveActivity.EXTRA_TRACK_FILE_FORMAT, (Parcelable) TrackFileFormat.KML)
|
||||
.putExtra(SaveActivity.EXTRA_PLAY_TRACK, true);
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
protected void deleteTrack(long[] trackIds) {
|
||||
ConfirmDeleteDialogFragment.newInstance(trackIds)
|
||||
.show(getSupportFragmentManager(), ConfirmDeleteDialogFragment.CONFIRM_DELETE_DIALOG_TAG);
|
||||
}
|
||||
|
||||
abstract protected TrackRecordingServiceConnection getTrackRecordingServiceConnection();
|
||||
|
||||
abstract protected void onDeleted();
|
||||
|
||||
@Override
|
||||
public void onConfirmDeleteDone(long[] trackIds) {
|
||||
boolean stopRecording = false;
|
||||
if (trackIds.length == 1 && trackIds[0] == -1L) {
|
||||
stopRecording = true;
|
||||
} else {
|
||||
long recordingTrackId = PreferencesUtils.getLong(this, R.string.recording_track_id_key);
|
||||
for (long trackId : trackIds) {
|
||||
if (trackId == recordingTrackId) {
|
||||
stopRecording = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (stopRecording) {
|
||||
TrackRecordingServiceConnectionUtils.stopRecording(
|
||||
this, getTrackRecordingServiceConnection(), false);
|
||||
}
|
||||
Intent intent = IntentUtils.newIntent(this, DeleteActivity.class);
|
||||
intent.putExtra(DeleteActivity.EXTRA_TRACK_IDS, trackIds);
|
||||
startActivityForResult(intent, DELETE_REQUEST_CODE);
|
||||
}
|
||||
}
|
||||
@@ -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,46 @@
|
||||
/*
|
||||
* Copyright 2012 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
|
||||
package com.google.android.apps.mytracks;
|
||||
|
||||
import android.view.Menu;
|
||||
|
||||
/**
|
||||
* Callback when items in the contextual action mode are selected.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public interface ContextualActionModeCallback {
|
||||
|
||||
/**
|
||||
* Invoked to prepare the menu for the selected items.
|
||||
*
|
||||
* @param menu the menu
|
||||
* @param positions the selected items' positions
|
||||
* @param ids the selected items' ids, if available
|
||||
* @param showSelectAll true to show select all
|
||||
*/
|
||||
public void onPrepare(Menu menu, int[] positions, long[] ids, boolean showSelectAll);
|
||||
|
||||
/**
|
||||
* Invoked when items are selected.
|
||||
*
|
||||
* @param itemId the context menu item id
|
||||
* @param positions the selected items' positions
|
||||
* @param ids the selected items' ids, if available
|
||||
*/
|
||||
public boolean onClick(int itemId, int[] positions, long[] ids);
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright 2013 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
|
||||
package com.google.android.apps.mytracks;
|
||||
|
||||
import com.google.android.apps.mytracks.util.DialogUtils;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Dialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
|
||||
/**
|
||||
* An activity for delete tracks.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class DeleteActivity extends Activity {
|
||||
|
||||
public static final String EXTRA_TRACK_IDS = "track_ids";
|
||||
|
||||
private static final int DIALOG_PROGRESS_ID = 0;
|
||||
|
||||
private DeleteAsyncTask deleteAsyncTask;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
setResult(RESULT_CANCELED);
|
||||
|
||||
Intent intent = getIntent();
|
||||
long[] trackIds = intent.getLongArrayExtra(EXTRA_TRACK_IDS);
|
||||
|
||||
Object retained = getLastNonConfigurationInstance();
|
||||
if (retained instanceof DeleteAsyncTask) {
|
||||
deleteAsyncTask = (DeleteAsyncTask) retained;
|
||||
deleteAsyncTask.setActivity(this);
|
||||
} else {
|
||||
deleteAsyncTask = new DeleteAsyncTask(this, trackIds);
|
||||
deleteAsyncTask.execute();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object onRetainNonConfigurationInstance() {
|
||||
deleteAsyncTask.setActivity(null);
|
||||
return deleteAsyncTask;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Dialog onCreateDialog(int id) {
|
||||
if (id != DIALOG_PROGRESS_ID) {
|
||||
return null;
|
||||
}
|
||||
return DialogUtils.createSpinnerProgressDialog(
|
||||
this, R.string.track_delete_progress_message, new DialogInterface.OnCancelListener() {
|
||||
@Override
|
||||
public void onCancel(DialogInterface dialog) {
|
||||
deleteAsyncTask.cancel(true);
|
||||
dialog.dismiss();
|
||||
finish();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes when the associated AsyncTask completes.
|
||||
*/
|
||||
public void onAsyncTaskCompleted() {
|
||||
removeDialog(DIALOG_PROGRESS_ID);
|
||||
setResult(RESULT_OK);
|
||||
finish();
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the progress dialog.
|
||||
*/
|
||||
public void showProgressDialog() {
|
||||
showDialog(DIALOG_PROGRESS_ID);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package com.google.android.apps.mytracks;
|
||||
|
||||
/*
|
||||
* Copyright 2013 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
|
||||
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
|
||||
import com.google.android.apps.mytracks.io.sync.SyncUtils;
|
||||
import com.google.android.apps.mytracks.util.PreferencesUtils;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.AsyncTask;
|
||||
|
||||
/**
|
||||
* Async Task to delete tracks.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class DeleteAsyncTask extends AsyncTask<Void, Integer, Boolean> {
|
||||
|
||||
private DeleteActivity deleteActivity;
|
||||
private final long[] trackIds;
|
||||
private final Context context;
|
||||
|
||||
// true if the AsyncTask has completed
|
||||
private boolean completed;
|
||||
|
||||
/**
|
||||
* Creates an AsyncTask.
|
||||
*
|
||||
* @param deleteActivity the activity currently associated with this task
|
||||
* @param trackIds the track ids to delete. To delete all, set to size 1 with
|
||||
* trackIds[0] == -1L
|
||||
*/
|
||||
public DeleteAsyncTask(DeleteActivity deleteActivity, long[] trackIds) {
|
||||
this.deleteActivity = deleteActivity;
|
||||
this.trackIds = trackIds;
|
||||
context = deleteActivity.getApplicationContext();
|
||||
completed = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current activity associated with this AyncTask.
|
||||
*
|
||||
* @param deleteActivity the current activity, can be null
|
||||
*/
|
||||
public void setActivity(DeleteActivity deleteActivity) {
|
||||
this.deleteActivity = deleteActivity;
|
||||
if (completed && deleteActivity != null) {
|
||||
deleteActivity.onAsyncTaskCompleted();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
if (deleteActivity != null) {
|
||||
deleteActivity.showProgressDialog();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Boolean doInBackground(Void... params) {
|
||||
MyTracksProviderUtils myTracksProviderUtils = MyTracksProviderUtils.Factory.get(context);
|
||||
|
||||
if (trackIds.length == 1 && trackIds[0] == -1L) {
|
||||
try {
|
||||
while (SyncUtils.isSyncActive(context)) {
|
||||
if (isCancelled()) {
|
||||
return false;
|
||||
}
|
||||
Thread.sleep(1000);
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
return false;
|
||||
}
|
||||
PreferencesUtils.setBoolean(
|
||||
context, R.string.drive_sync_key, PreferencesUtils.DRIVE_SYNC_DEFAULT);
|
||||
SyncUtils.disableSync(context);
|
||||
SyncUtils.clearSyncState(context);
|
||||
myTracksProviderUtils.deleteAllTracks();
|
||||
return true;
|
||||
} else {
|
||||
for (long id : trackIds) {
|
||||
if (isCancelled()) {
|
||||
return false;
|
||||
}
|
||||
myTracksProviderUtils.deleteTrack(id);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Boolean result) {
|
||||
completed = true;
|
||||
if (deleteActivity != null) {
|
||||
deleteActivity.onAsyncTaskCompleted();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,300 @@
|
||||
/*
|
||||
* 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.stats.TripStatistics;
|
||||
import com.google.android.apps.mytracks.util.LocationUtils;
|
||||
import com.google.android.apps.mytracks.util.PreferencesUtils;
|
||||
import com.google.android.apps.mytracks.util.UnitConversions;
|
||||
import com.google.android.gms.maps.GoogleMap;
|
||||
import com.google.android.gms.maps.model.BitmapDescriptorFactory;
|
||||
import com.google.android.gms.maps.model.LatLng;
|
||||
import com.google.android.gms.maps.model.MarkerOptions;
|
||||
import com.google.android.gms.maps.model.Polyline;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
|
||||
import android.location.Location;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ArrayBlockingQueue;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
|
||||
/**
|
||||
* A map overlay that displays my location arrow, error circle, and track info.
|
||||
*
|
||||
* @author Leif Hendrik Wilden
|
||||
*/
|
||||
public class MapOverlay {
|
||||
|
||||
public static final float WAYPOINT_X_ANCHOR = 13f / 48f;
|
||||
|
||||
private static final 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 = location.hasSpeed() ? (int) Math.floor(
|
||||
location.getSpeed() * UnitConversions.MS_TO_KMH)
|
||||
: -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 tripStatistics the trip statistics
|
||||
* @param reload true to reload all points
|
||||
* @return true if has the start marker
|
||||
*/
|
||||
public boolean update(GoogleMap googleMap, ArrayList<Polyline> paths,
|
||||
TripStatistics tripStatistics, boolean reload) {
|
||||
synchronized (locations) {
|
||||
boolean hasStartMarker = false;
|
||||
// Merge pendingLocations with locations
|
||||
int newLocations = pendingLocations.drainTo(locations);
|
||||
// Call updateState first because we want to update its state each time
|
||||
// (for dynamic coloring)
|
||||
if (trackPath.updateState(tripStatistics) || reload) {
|
||||
googleMap.clear();
|
||||
paths.clear();
|
||||
trackPath.updatePath(googleMap, paths, 0, locations);
|
||||
hasStartMarker = updateStartAndEndMarkers(googleMap);
|
||||
updateWaypoints(googleMap);
|
||||
} else {
|
||||
if (newLocations != 0) {
|
||||
int numLocations = locations.size();
|
||||
trackPath.updatePath(googleMap, paths, numLocations - newLocations, locations);
|
||||
}
|
||||
}
|
||||
return hasStartMarker;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the start and end markers.
|
||||
*
|
||||
* @param googleMap the google map
|
||||
* @return true if has the start marker
|
||||
*/
|
||||
private boolean updateStartAndEndMarkers(GoogleMap googleMap) {
|
||||
// Add the end marker
|
||||
if (showEndMarker) {
|
||||
for (int i = locations.size() - 1; i >= 0; i--) {
|
||||
CachedLocation cachedLocation = locations.get(i);
|
||||
if (cachedLocation.valid) {
|
||||
MarkerOptions markerOptions = new MarkerOptions().position(cachedLocation.getLatLng())
|
||||
.anchor(MARKER_X_ANCHOR, MARKER_Y_ANCHOR).draggable(false).visible(true)
|
||||
.icon(BitmapDescriptorFactory.fromResource(R.drawable.ic_marker_red_paddle));
|
||||
googleMap.addMarker(markerOptions);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add the start marker
|
||||
boolean hasStartMarker = false;
|
||||
for (int i = 0; i < locations.size(); i++) {
|
||||
CachedLocation cachedLocation = locations.get(i);
|
||||
if (cachedLocation.valid) {
|
||||
MarkerOptions markerOptions = new MarkerOptions().position(cachedLocation.getLatLng())
|
||||
.anchor(MARKER_X_ANCHOR, MARKER_Y_ANCHOR).draggable(false).visible(true)
|
||||
.icon(BitmapDescriptorFactory.fromResource(R.drawable.ic_marker_green_paddle));
|
||||
googleMap.addMarker(markerOptions);
|
||||
hasStartMarker = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return hasStartMarker;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the waypoints.
|
||||
*
|
||||
* @param googleMap the google map.
|
||||
*/
|
||||
private void updateWaypoints(GoogleMap googleMap) {
|
||||
synchronized (waypoints) {
|
||||
for (Waypoint waypoint : waypoints) {
|
||||
Location location = waypoint.getLocation();
|
||||
LatLng latLng = new LatLng(location.getLatitude(), location.getLongitude());
|
||||
int drawableId = waypoint.getType() == WaypointType.STATISTICS
|
||||
? R.drawable.ic_marker_yellow_pushpin : R.drawable.ic_marker_blue_pushpin;
|
||||
MarkerOptions markerOptions = new MarkerOptions().position(latLng)
|
||||
.anchor(WAYPOINT_X_ANCHOR, WAYPOINT_Y_ANCHOR).draggable(false).visible(true)
|
||||
.icon(BitmapDescriptorFactory.fromResource(drawableId))
|
||||
.title(String.valueOf(waypoint.getId()));
|
||||
googleMap.addMarker(markerOptions);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
* Copyright 2009 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
|
||||
package com.google.android.apps.mytracks;
|
||||
|
||||
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
|
||||
import com.google.android.apps.mytracks.content.Waypoint;
|
||||
import com.google.android.apps.mytracks.fragments.DeleteMarkerDialogFragment.DeleteMarkerCaller;
|
||||
import com.google.android.apps.mytracks.fragments.MarkerDetailFragment;
|
||||
import com.google.android.apps.mytracks.util.IntentUtils;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.app.FragmentManager;
|
||||
import android.support.v4.app.FragmentStatePagerAdapter;
|
||||
import android.support.v4.view.ViewPager;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* An activity to display marker detail info.
|
||||
*
|
||||
* @author Leif Hendrik Wilden
|
||||
*/
|
||||
public class MarkerDetailActivity extends AbstractMyTracksActivity implements DeleteMarkerCaller {
|
||||
|
||||
public static final String EXTRA_MARKER_ID = "marker_id";
|
||||
private static final String TAG = MarkerDetailActivity.class.getSimpleName();
|
||||
|
||||
private Waypoint waypoint;
|
||||
private ArrayList<Long> markerIds;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle bundle) {
|
||||
super.onCreate(bundle);
|
||||
|
||||
long markerId = getIntent().getLongExtra(EXTRA_MARKER_ID, -1L);
|
||||
if (markerId == -1L) {
|
||||
Log.d(TAG, "invalid marker id");
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
MyTracksProviderUtils myTracksProviderUtils = MyTracksProviderUtils.Factory.get(this);
|
||||
waypoint = myTracksProviderUtils.getWaypoint(markerId);
|
||||
|
||||
markerIds = new ArrayList<Long>();
|
||||
int markerIndex = -1;
|
||||
Cursor cursor = null;
|
||||
|
||||
try {
|
||||
cursor = myTracksProviderUtils.getWaypointCursor(waypoint.getTrackId(), -1L, -1);
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
/*
|
||||
* Yes, this will skip the first waypoint and that is intentional as the
|
||||
* first waypoint holds the stats for the track.
|
||||
*/
|
||||
while (cursor.moveToNext()) {
|
||||
Waypoint current = myTracksProviderUtils.createWaypoint(cursor);
|
||||
|
||||
markerIds.add(current.getId());
|
||||
if (current.getId() == markerId) {
|
||||
markerIndex = markerIds.size() - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
ViewPager viewPager = (ViewPager) findViewById(R.id.maker_detail_activity_view_pager);
|
||||
viewPager.setAdapter(new MarkerDetailPagerAdapter(getSupportFragmentManager()));
|
||||
if (markerIndex != -1) {
|
||||
viewPager.setCurrentItem(markerIndex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getLayoutResId() {
|
||||
return R.layout.marker_detail_activity;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onHomeSelected() {
|
||||
Intent intent = IntentUtils.newIntent(this, MarkerListActivity.class)
|
||||
.putExtra(MarkerListActivity.EXTRA_TRACK_ID, waypoint.getTrackId());
|
||||
startActivity(intent);
|
||||
finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDeleteMarkerDone() {
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
finish();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Marker detail pager adapter.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
private class MarkerDetailPagerAdapter extends FragmentStatePagerAdapter {
|
||||
|
||||
public MarkerDetailPagerAdapter(FragmentManager fragmentManager) {
|
||||
super(fragmentManager);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Fragment getItem(int position) {
|
||||
return MarkerDetailFragment.newInstance(markerIds.get(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return markerIds.size();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
/*
|
||||
* 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,
|
||||
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,350 @@
|
||||
/*
|
||||
* Copyright 2009 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
|
||||
package com.google.android.apps.mytracks;
|
||||
|
||||
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
|
||||
import com.google.android.apps.mytracks.content.Track;
|
||||
import com.google.android.apps.mytracks.content.Waypoint.WaypointType;
|
||||
import com.google.android.apps.mytracks.content.WaypointsColumns;
|
||||
import com.google.android.apps.mytracks.fragments.DeleteMarkerDialogFragment;
|
||||
import com.google.android.apps.mytracks.fragments.DeleteMarkerDialogFragment.DeleteMarkerCaller;
|
||||
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
|
||||
import com.google.android.apps.mytracks.util.IntentUtils;
|
||||
import com.google.android.apps.mytracks.util.ListItemUtils;
|
||||
import com.google.android.apps.mytracks.util.PreferencesUtils;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
|
||||
import android.database.Cursor;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.LoaderManager.LoaderCallbacks;
|
||||
import android.support.v4.content.CursorLoader;
|
||||
import android.support.v4.content.Loader;
|
||||
import android.support.v4.widget.ResourceCursorAdapter;
|
||||
import android.util.Log;
|
||||
import android.view.ContextMenu;
|
||||
import android.view.ContextMenu.ContextMenuInfo;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.AdapterView.AdapterContextMenuInfo;
|
||||
import android.widget.AdapterView.OnItemClickListener;
|
||||
import android.widget.ListView;
|
||||
|
||||
/**
|
||||
* Activity to show a list of markers in a track.
|
||||
*
|
||||
* @author Leif Hendrik Wilden
|
||||
*/
|
||||
public class MarkerListActivity extends AbstractMyTracksActivity implements DeleteMarkerCaller {
|
||||
|
||||
public static final String EXTRA_TRACK_ID = "track_id";
|
||||
|
||||
private static final String TAG = MarkerListActivity.class.getSimpleName();
|
||||
|
||||
private static final String[] PROJECTION = new String[] { WaypointsColumns._ID,
|
||||
WaypointsColumns.NAME, WaypointsColumns.DESCRIPTION, WaypointsColumns.CATEGORY,
|
||||
WaypointsColumns.TYPE, WaypointsColumns.TIME, WaypointsColumns.PHOTOURL };
|
||||
|
||||
// Callback when an item is selected in the contextual action mode
|
||||
private ContextualActionModeCallback
|
||||
contextualActionModeCallback = new ContextualActionModeCallback() {
|
||||
@Override
|
||||
public void onPrepare(Menu menu, int[] positions, long[] ids, boolean showSelectAll) {
|
||||
boolean isSingleSelection = ids.length == 1;
|
||||
|
||||
// Always disable
|
||||
menu.findItem(R.id.list_context_menu_play).setVisible(false);
|
||||
// Always disable
|
||||
menu.findItem(R.id.list_context_menu_share).setVisible(false);
|
||||
// One item
|
||||
menu.findItem(R.id.list_context_menu_show_on_map).setVisible(isSingleSelection);
|
||||
// One item, track not sharedWithMe
|
||||
menu.findItem(R.id.list_context_menu_edit)
|
||||
.setVisible(isSingleSelection && !track.isSharedWithMe());
|
||||
// Track not sharedWithMe
|
||||
menu.findItem(R.id.list_context_menu_delete).setVisible(!track.isSharedWithMe());
|
||||
/*
|
||||
* Set select all to the same visibility as delete since delete is the
|
||||
* only action that can be applied to multiple markers.
|
||||
*/
|
||||
menu.findItem(R.id.list_context_menu_select_all)
|
||||
.setVisible(showSelectAll && !track.isSharedWithMe());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onClick(int itemId, int[] positions, long[] ids) {
|
||||
return handleContextItem(itemId, ids);
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* Note that sharedPreferenceChangeListener cannot be an anonymous inner
|
||||
* class. Anonymous inner class will get garbage collected.
|
||||
*/
|
||||
private final OnSharedPreferenceChangeListener
|
||||
sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
|
||||
// Note that the key can be null
|
||||
if (key == null || key.equals(
|
||||
PreferencesUtils.getKey(MarkerListActivity.this, R.string.recording_track_id_key))) {
|
||||
recordingTrackId = PreferencesUtils.getLong(
|
||||
MarkerListActivity.this, R.string.recording_track_id_key);
|
||||
}
|
||||
if (key == null || key.equals(PreferencesUtils.getKey(
|
||||
MarkerListActivity.this, R.string.recording_track_paused_key))) {
|
||||
recordingTrackPaused = PreferencesUtils.getBoolean(MarkerListActivity.this,
|
||||
R.string.recording_track_paused_key,
|
||||
PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT);
|
||||
}
|
||||
if (key != null) {
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
ApiAdapterFactory.getApiAdapter().invalidMenu(MarkerListActivity.this);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private MyTracksProviderUtils myTracksProviderUtils;
|
||||
private SharedPreferences sharedPreferences;
|
||||
|
||||
private long recordingTrackId = PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
|
||||
private boolean recordingTrackPaused = PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT;
|
||||
|
||||
private Track track;
|
||||
private ResourceCursorAdapter resourceCursorAdapter;
|
||||
|
||||
// UI elements
|
||||
private ListView listView;
|
||||
private MenuItem insertMarkerMenuItem;
|
||||
private MenuItem searchMenuItem;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
myTracksProviderUtils = MyTracksProviderUtils.Factory.get(this);
|
||||
sharedPreferences = getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
|
||||
long trackId = getIntent().getLongExtra(EXTRA_TRACK_ID, -1L);
|
||||
if (trackId == -1L) {
|
||||
Log.d(TAG, "invalid track id");
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
track = myTracksProviderUtils.getTrack(trackId);
|
||||
|
||||
setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
|
||||
|
||||
listView = (ListView) findViewById(R.id.marker_list);
|
||||
listView.setEmptyView(findViewById(R.id.marker_list_empty));
|
||||
listView.setOnItemClickListener(new OnItemClickListener() {
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
|
||||
Intent intent = IntentUtils.newIntent(MarkerListActivity.this, MarkerDetailActivity.class)
|
||||
.putExtra(MarkerDetailActivity.EXTRA_MARKER_ID, id);
|
||||
startActivity(intent);
|
||||
}
|
||||
});
|
||||
resourceCursorAdapter = new ResourceCursorAdapter(this, R.layout.list_item, null, 0) {
|
||||
@Override
|
||||
public void bindView(View view, Context context, Cursor cursor) {
|
||||
int typeIndex = cursor.getColumnIndex(WaypointsColumns.TYPE);
|
||||
int nameIndex = cursor.getColumnIndex(WaypointsColumns.NAME);
|
||||
int photoUrlIndex = cursor.getColumnIndex(WaypointsColumns.PHOTOURL);
|
||||
int timeIndex = cursor.getColumnIndexOrThrow(WaypointsColumns.TIME);
|
||||
int categoryIndex = cursor.getColumnIndex(WaypointsColumns.CATEGORY);
|
||||
int descriptionIndex = cursor.getColumnIndex(WaypointsColumns.DESCRIPTION);
|
||||
|
||||
boolean statistics = WaypointType.values()[cursor.getInt(typeIndex)]
|
||||
== WaypointType.STATISTICS;
|
||||
int iconId = statistics ? R.drawable.ic_marker_yellow_pushpin
|
||||
: R.drawable.ic_marker_blue_pushpin;
|
||||
String name = cursor.getString(nameIndex);
|
||||
String sharedOwner = track.getSharedOwner();
|
||||
String photoUrl = cursor.getString(photoUrlIndex);
|
||||
long time = cursor.getLong(timeIndex);
|
||||
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, name, sharedOwner, null, null, photoUrl, time, category,
|
||||
description);
|
||||
}
|
||||
};
|
||||
listView.setAdapter(resourceCursorAdapter);
|
||||
ApiAdapterFactory.getApiAdapter()
|
||||
.configureListViewContextualMenu(this, listView, contextualActionModeCallback);
|
||||
|
||||
final long firstWaypointId = myTracksProviderUtils.getFirstWaypointId(trackId);
|
||||
getSupportLoaderManager().initLoader(0, null, new LoaderCallbacks<Cursor>() {
|
||||
@Override
|
||||
public Loader<Cursor> onCreateLoader(int arg0, Bundle arg1) {
|
||||
return new CursorLoader(MarkerListActivity.this, WaypointsColumns.CONTENT_URI, PROJECTION,
|
||||
WaypointsColumns.TRACKID + "=? AND " + WaypointsColumns._ID + "!=?",
|
||||
new String[] { String.valueOf(track.getId()), String.valueOf(firstWaypointId) }, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
|
||||
resourceCursorAdapter.swapCursor(cursor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(Loader<Cursor> loader) {
|
||||
resourceCursorAdapter.swapCursor(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStart() {
|
||||
super.onStart();
|
||||
sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
|
||||
sharedPreferenceChangeListener.onSharedPreferenceChanged(null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
ApiAdapterFactory.getApiAdapter().invalidMenu(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStop() {
|
||||
super.onStop();
|
||||
sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getLayoutResId() {
|
||||
return R.layout.marker_list;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
getMenuInflater().inflate(R.menu.marker_list, menu);
|
||||
insertMarkerMenuItem = menu.findItem(R.id.marker_list_insert_marker);
|
||||
searchMenuItem = menu.findItem(R.id.marker_list_search);
|
||||
ApiAdapterFactory.getApiAdapter().configureSearchWidget(this, searchMenuItem, null);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareOptionsMenu(Menu menu) {
|
||||
if (insertMarkerMenuItem != null) {
|
||||
insertMarkerMenuItem.setVisible(track.getId() == recordingTrackId && !recordingTrackPaused);
|
||||
}
|
||||
return super.onPrepareOptionsMenu(menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.marker_list_insert_marker:
|
||||
Intent intent = IntentUtils.newIntent(this, MarkerEditActivity.class)
|
||||
.putExtra(MarkerEditActivity.EXTRA_TRACK_ID, track.getId());
|
||||
startActivity(intent);
|
||||
return true;
|
||||
case R.id.marker_list_search:
|
||||
return ApiAdapterFactory.getApiAdapter().handleSearchMenuSelection(this);
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
|
||||
super.onCreateContextMenu(menu, v, menuInfo);
|
||||
getMenuInflater().inflate(R.menu.list_context_menu, menu);
|
||||
|
||||
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
|
||||
contextualActionModeCallback.onPrepare(
|
||||
menu, new int[] { info.position }, new long[] { info.id }, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onContextItemSelected(MenuItem item) {
|
||||
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
|
||||
if (handleContextItem(item.getItemId(), new long[] {info.id})) {
|
||||
return true;
|
||||
}
|
||||
return super.onContextItemSelected(item);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a context item selection.
|
||||
*
|
||||
* @param itemId the menu item id
|
||||
* @param markerIds the marker ids
|
||||
* @return true if handled.
|
||||
*/
|
||||
private boolean handleContextItem(int itemId, long[] markerIds) {
|
||||
Intent intent;
|
||||
switch (itemId) {
|
||||
case R.id.list_context_menu_show_on_map:
|
||||
if (markerIds.length == 1) {
|
||||
intent = IntentUtils.newIntent(this, TrackDetailActivity.class)
|
||||
.putExtra(TrackDetailActivity.EXTRA_MARKER_ID, markerIds[0]);
|
||||
startActivity(intent);
|
||||
}
|
||||
return true;
|
||||
case R.id.list_context_menu_edit:
|
||||
if (markerIds.length == 1) {
|
||||
intent = IntentUtils.newIntent(this, MarkerEditActivity.class)
|
||||
.putExtra(MarkerEditActivity.EXTRA_MARKER_ID, markerIds[0]);
|
||||
startActivity(intent);
|
||||
}
|
||||
return true;
|
||||
case R.id.list_context_menu_delete:
|
||||
DeleteMarkerDialogFragment.newInstance(markerIds)
|
||||
.show(getSupportFragmentManager(), DeleteMarkerDialogFragment.DELETE_MARKER_DIALOG_TAG);
|
||||
return true;
|
||||
case R.id.list_context_menu_select_all:
|
||||
int size = listView.getCount();
|
||||
for (int i = 0; i < size; i++) {
|
||||
listView.setItemChecked(i, true);
|
||||
}
|
||||
return false;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyUp(int keyCode, KeyEvent event) {
|
||||
if (keyCode == KeyEvent.KEYCODE_SEARCH && searchMenuItem != null) {
|
||||
if (ApiAdapterFactory.getApiAdapter().handleSearchKey(searchMenuItem)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return super.onKeyUp(keyCode, event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDeleteMarkerDone() {
|
||||
// Do nothing
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,559 @@
|
||||
/*
|
||||
* Copyright 2011 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
|
||||
package com.google.android.apps.mytracks;
|
||||
|
||||
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
|
||||
import com.google.android.apps.mytracks.content.SearchEngine;
|
||||
import com.google.android.apps.mytracks.content.SearchEngine.ScoredResult;
|
||||
import com.google.android.apps.mytracks.content.SearchEngine.SearchQuery;
|
||||
import com.google.android.apps.mytracks.content.SearchEngineProvider;
|
||||
import com.google.android.apps.mytracks.content.Track;
|
||||
import com.google.android.apps.mytracks.content.Waypoint;
|
||||
import com.google.android.apps.mytracks.content.Waypoint.WaypointType;
|
||||
import com.google.android.apps.mytracks.fragments.ConfirmDeleteDialogFragment;
|
||||
import com.google.android.apps.mytracks.fragments.DeleteMarkerDialogFragment;
|
||||
import com.google.android.apps.mytracks.fragments.DeleteMarkerDialogFragment.DeleteMarkerCaller;
|
||||
import com.google.android.apps.mytracks.services.MyTracksLocationManager;
|
||||
import com.google.android.apps.mytracks.services.TrackRecordingServiceConnection;
|
||||
import com.google.android.apps.mytracks.stats.TripStatistics;
|
||||
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
|
||||
import com.google.android.apps.mytracks.util.IntentUtils;
|
||||
import com.google.android.apps.mytracks.util.ListItemUtils;
|
||||
import com.google.android.apps.mytracks.util.PreferencesUtils;
|
||||
import com.google.android.apps.mytracks.util.StringUtils;
|
||||
import com.google.android.apps.mytracks.util.TrackIconUtils;
|
||||
import com.google.android.apps.mytracks.util.TrackRecordingServiceConnectionUtils;
|
||||
import com.google.android.gms.location.LocationListener;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.app.SearchManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
|
||||
import android.location.Location;
|
||||
import android.os.Bundle;
|
||||
import android.os.Looper;
|
||||
import android.provider.SearchRecentSuggestions;
|
||||
import android.util.Log;
|
||||
import android.view.ContextMenu;
|
||||
import android.view.ContextMenu.ContextMenuInfo;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.AdapterView.AdapterContextMenuInfo;
|
||||
import android.widget.AdapterView.OnItemClickListener;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.ListView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.SortedSet;
|
||||
|
||||
/**
|
||||
* An activity to display a list of search results.
|
||||
*
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
public class SearchListActivity extends AbstractSendToGoogleActivity implements DeleteMarkerCaller {
|
||||
|
||||
private static final String TAG = SearchListActivity.class.getSimpleName();
|
||||
|
||||
private static final String IS_RECORDING_FIELD = "isRecording";
|
||||
private static final String IS_PAUSED_FIELD = "isPaused";
|
||||
private static final String ICON_ID_FIELD = "icon";
|
||||
private static final String ICON_CONTENT_DESCRIPTION_ID_FIELD = "iconContentDescription";
|
||||
private static final String NAME_FIELD = "name";
|
||||
private static final String SHARED_OWNER_FIELD = "sharedOwner";
|
||||
private static final String TOTAL_TIME_FIELD = "totalTime";
|
||||
private static final String TOTAL_DISTANCE_FIELD = "totalDistance";
|
||||
private static final String PHOTO_URL_FIELD = "photoUrl";
|
||||
private static final String START_TIME_FIELD = "startTime";
|
||||
private static final String CATEGORY_FIELD = "category";
|
||||
private static final String DESCRIPTION_FIELD = "description";
|
||||
|
||||
private static final String TRACK_ID_FIELD = "trackId";
|
||||
private static final String MARKER_ID_FIELD = "markerId";
|
||||
|
||||
private final OnSharedPreferenceChangeListener
|
||||
sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
|
||||
if (key == null || key.equals(
|
||||
PreferencesUtils.getKey(SearchListActivity.this, R.string.stats_units_key))) {
|
||||
metricUnits = PreferencesUtils.isMetricUnits(SearchListActivity.this);
|
||||
}
|
||||
if (key == null || key.equals(
|
||||
PreferencesUtils.getKey(SearchListActivity.this, R.string.recording_track_id_key))) {
|
||||
recordingTrackId = PreferencesUtils.getLong(
|
||||
SearchListActivity.this, R.string.recording_track_id_key);
|
||||
}
|
||||
if (key == null || key.equals(PreferencesUtils.getKey(
|
||||
SearchListActivity.this, R.string.recording_track_paused_key))) {
|
||||
recordingTrackPaused = PreferencesUtils.getBoolean(SearchListActivity.this,
|
||||
R.string.recording_track_paused_key,
|
||||
PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT);
|
||||
}
|
||||
if (key != null) {
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
arrayAdapter.notifyDataSetChanged();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Callback when an item is selected in the contextual action mode
|
||||
private ContextualActionModeCallback
|
||||
contextualActionModeCallback = new ContextualActionModeCallback() {
|
||||
@Override
|
||||
public boolean onClick(int itemId, int[] positions, long[] ids) {
|
||||
return handleContextItem(itemId, positions);
|
||||
}
|
||||
@Override
|
||||
public void onPrepare(Menu menu, int[] positions, long[] ids, boolean showSelectAll) {
|
||||
boolean isRecording = recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
|
||||
boolean isSingleSelection = positions.length == 1;
|
||||
boolean isSingleSelectionShareWithMe;
|
||||
boolean isSingleSelectionTrack;
|
||||
if (isSingleSelection) {
|
||||
Map<String, Object> item = arrayAdapter.getItem(positions[0]);
|
||||
Long trackId = (Long) item.get(TRACK_ID_FIELD);
|
||||
Track track = myTracksProviderUtils.getTrack(trackId);
|
||||
|
||||
isSingleSelectionShareWithMe = track.isSharedWithMe();
|
||||
isSingleSelectionTrack = item.get(MARKER_ID_FIELD) == null;
|
||||
} else {
|
||||
isSingleSelectionShareWithMe = false;
|
||||
isSingleSelectionTrack = false;
|
||||
}
|
||||
// Not recording, one item, item is a track
|
||||
menu.findItem(R.id.list_context_menu_play)
|
||||
.setVisible(!isRecording && isSingleSelection && isSingleSelectionTrack);
|
||||
// Not recording, one item, item is a track, not shareWithMe item
|
||||
menu.findItem(R.id.list_context_menu_share).setVisible(!isRecording && isSingleSelection
|
||||
&& isSingleSelectionTrack && !isSingleSelectionShareWithMe);
|
||||
// One item, item is a marker
|
||||
menu.findItem(R.id.list_context_menu_show_on_map)
|
||||
.setVisible(isSingleSelection && !isSingleSelectionTrack);
|
||||
// One item, can be a track or a marker, cannot be a sharedWithMe item
|
||||
menu.findItem(R.id.list_context_menu_edit)
|
||||
.setVisible(isSingleSelection && !isSingleSelectionShareWithMe);
|
||||
// One item. If track, no restriction. If marker, cannot be a
|
||||
// shareWithMe item
|
||||
menu.findItem(R.id.list_context_menu_delete).setVisible(
|
||||
isSingleSelection && (isSingleSelectionTrack || !isSingleSelectionShareWithMe));
|
||||
// Disable select all, no action is available for multiple selection
|
||||
menu.findItem(R.id.list_context_menu_select_all).setVisible(false);
|
||||
}
|
||||
};
|
||||
|
||||
private MyTracksProviderUtils myTracksProviderUtils;
|
||||
private SharedPreferences sharedPreferences;
|
||||
private TrackRecordingServiceConnection trackRecordingServiceConnection;
|
||||
private SearchEngine searchEngine;
|
||||
private SearchRecentSuggestions searchRecentSuggestions;
|
||||
private ArrayAdapter<Map<String, Object>> arrayAdapter;
|
||||
|
||||
private boolean metricUnits = true;
|
||||
private long recordingTrackId = PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
|
||||
private boolean recordingTrackPaused = PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT;
|
||||
|
||||
// UI elements
|
||||
private ListView listView;
|
||||
private MenuItem searchMenuItem;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
|
||||
|
||||
myTracksProviderUtils = MyTracksProviderUtils.Factory.get(this);
|
||||
sharedPreferences = getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
|
||||
trackRecordingServiceConnection = new TrackRecordingServiceConnection(this, null);
|
||||
searchEngine = new SearchEngine(myTracksProviderUtils);
|
||||
searchRecentSuggestions = SearchEngineProvider.newHelper(this);
|
||||
|
||||
listView = (ListView) findViewById(R.id.search_list);
|
||||
listView.setEmptyView(findViewById(R.id.search_list_empty));
|
||||
listView.setOnItemClickListener(new OnItemClickListener() {
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
|
||||
Map<String, Object> item = arrayAdapter.getItem(position);
|
||||
Long trackId = (Long) item.get(TRACK_ID_FIELD);
|
||||
Long markerId = (Long) item.get(MARKER_ID_FIELD);
|
||||
Intent intent;
|
||||
if (markerId != null) {
|
||||
intent = IntentUtils.newIntent(SearchListActivity.this, TrackDetailActivity.class)
|
||||
.putExtra(TrackDetailActivity.EXTRA_MARKER_ID, markerId);
|
||||
} else {
|
||||
intent = IntentUtils.newIntent(SearchListActivity.this, TrackDetailActivity.class)
|
||||
.putExtra(TrackDetailActivity.EXTRA_TRACK_ID, trackId);
|
||||
}
|
||||
startActivity(intent);
|
||||
}
|
||||
});
|
||||
arrayAdapter = new ArrayAdapter<Map<String, Object>>(
|
||||
this, R.layout.list_item, R.id.list_item_name) {
|
||||
@Override
|
||||
public View getView(int position, View convertView, android.view.ViewGroup parent) {
|
||||
View view;
|
||||
if (convertView == null) {
|
||||
view = getLayoutInflater().inflate(R.layout.list_item, parent, false);
|
||||
} else {
|
||||
view = convertView;
|
||||
}
|
||||
Map<String, Object> resultMap = getItem(position);
|
||||
boolean isRecording = (Boolean) resultMap.get(IS_RECORDING_FIELD);
|
||||
boolean isPaused = (Boolean) resultMap.get(IS_PAUSED_FIELD);
|
||||
int iconId = (Integer) resultMap.get(ICON_ID_FIELD);
|
||||
int iconContentDescriptionId = (Integer) resultMap.get(ICON_CONTENT_DESCRIPTION_ID_FIELD);
|
||||
String name = (String) resultMap.get(NAME_FIELD);
|
||||
String sharedOwner = (String) resultMap.get(SHARED_OWNER_FIELD);
|
||||
String totalTime = (String) resultMap.get(TOTAL_TIME_FIELD);
|
||||
String totalDistance = (String) resultMap.get(TOTAL_DISTANCE_FIELD);
|
||||
String photoUrl = (String) resultMap.get(PHOTO_URL_FIELD);
|
||||
Long startTime = (Long) resultMap.get(START_TIME_FIELD);
|
||||
String category = (String) resultMap.get(CATEGORY_FIELD);
|
||||
String description = (String) resultMap.get(DESCRIPTION_FIELD);
|
||||
|
||||
ListItemUtils.setListItem(SearchListActivity.this, view, isRecording, isPaused, iconId,
|
||||
iconContentDescriptionId, name, sharedOwner, totalTime, totalDistance, photoUrl,
|
||||
startTime, category, description);
|
||||
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 }, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onContextItemSelected(MenuItem item) {
|
||||
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
|
||||
if (handleContextItem(item.getItemId(), new int[] {info.position})) {
|
||||
return true;
|
||||
}
|
||||
return super.onContextItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyUp(int keyCode, KeyEvent event) {
|
||||
if (keyCode == KeyEvent.KEYCODE_SEARCH && searchMenuItem != null) {
|
||||
if (ApiAdapterFactory.getApiAdapter().handleSearchKey(searchMenuItem)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return super.onKeyUp(keyCode, event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a context item selection.
|
||||
*
|
||||
* @param itemId the menu item id
|
||||
* @param positions the positions of the selected rows
|
||||
* @return true if handled.
|
||||
*/
|
||||
private boolean handleContextItem(int itemId, int[] positions) {
|
||||
if (positions.length != 1) {
|
||||
return false;
|
||||
}
|
||||
Map<String, Object> item = arrayAdapter.getItem(positions[0]);
|
||||
Long trackId = (Long) item.get(TRACK_ID_FIELD);
|
||||
Long markerId = (Long) item.get(MARKER_ID_FIELD);
|
||||
Intent intent;
|
||||
switch (itemId) {
|
||||
case R.id.list_context_menu_play:
|
||||
confirmPlay(new long[] {trackId});
|
||||
return true;
|
||||
case R.id.list_context_menu_share:
|
||||
shareTrack(trackId);
|
||||
return true;
|
||||
case R.id.list_context_menu_show_on_map:
|
||||
intent = IntentUtils.newIntent(this, TrackDetailActivity.class)
|
||||
.putExtra(TrackDetailActivity.EXTRA_MARKER_ID, markerId);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
case R.id.list_context_menu_edit:
|
||||
if (markerId != null) {
|
||||
intent = IntentUtils.newIntent(this, MarkerEditActivity.class)
|
||||
.putExtra(MarkerEditActivity.EXTRA_MARKER_ID, markerId);
|
||||
} else {
|
||||
intent = IntentUtils.newIntent(this, TrackEditActivity.class)
|
||||
.putExtra(TrackEditActivity.EXTRA_TRACK_ID, trackId);
|
||||
}
|
||||
startActivity(intent);
|
||||
|
||||
// Close the search result since its content can change after edit.
|
||||
finish();
|
||||
return true;
|
||||
case R.id.list_context_menu_delete:
|
||||
if (markerId != null) {
|
||||
DeleteMarkerDialogFragment.newInstance(new long[] { markerId }).show(
|
||||
getSupportFragmentManager(), DeleteMarkerDialogFragment.DELETE_MARKER_DIALOG_TAG);
|
||||
} else {
|
||||
ConfirmDeleteDialogFragment.newInstance(new long[] { trackId })
|
||||
.show(getSupportFragmentManager(), ConfirmDeleteDialogFragment.CONFIRM_DELETE_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);
|
||||
|
||||
final MyTracksLocationManager myTracksLocationManager = new MyTracksLocationManager(
|
||||
this, Looper.myLooper(), true);
|
||||
LocationListener locationListener = new LocationListener() {
|
||||
@Override
|
||||
public void onLocationChanged(final Location location) {
|
||||
myTracksLocationManager.close();
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
SearchQuery query = new SearchQuery(
|
||||
textQuery, location, -1L, System.currentTimeMillis());
|
||||
doSearch(query);
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
};
|
||||
myTracksLocationManager.requestLastLocation(locationListener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Do the search.
|
||||
*
|
||||
* @param query the query
|
||||
*/
|
||||
private void doSearch(SearchQuery query) {
|
||||
SortedSet<ScoredResult> scoredResults = searchEngine.search(query);
|
||||
final List<Map<String, Object>> displayResults = prepareResultsforDisplay(scoredResults);
|
||||
|
||||
// Use the UI thread to display the results
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
arrayAdapter.clear();
|
||||
ApiAdapterFactory.getApiAdapter().addAllToArrayAdapter(arrayAdapter, displayResults);
|
||||
}
|
||||
});
|
||||
|
||||
// Save the query as a suggestion for the future
|
||||
searchRecentSuggestions.saveRecentQuery(query.textQuery, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the result for display.
|
||||
*
|
||||
* @param scoredResults a list of score results
|
||||
* @return a list of result maps
|
||||
*/
|
||||
private List<Map<String, Object>> prepareResultsforDisplay(
|
||||
Collection<ScoredResult> scoredResults) {
|
||||
ArrayList<Map<String, Object>> output = new ArrayList<Map<String, Object>>(
|
||||
scoredResults.size());
|
||||
for (ScoredResult result : scoredResults) {
|
||||
Map<String, Object> resultMap = new HashMap<String, Object>();
|
||||
if (result.track != null) {
|
||||
prepareTrackForDisplay(result.track, resultMap);
|
||||
} else {
|
||||
prepareMarkerForDisplay(result.waypoint, resultMap);
|
||||
}
|
||||
output.add(resultMap);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares a marker for display by filling in a result map.
|
||||
*
|
||||
* @param waypoint the marker
|
||||
* @param resultMap the result map
|
||||
*/
|
||||
private void prepareMarkerForDisplay(Waypoint waypoint, Map<String, Object> resultMap) {
|
||||
/*
|
||||
* TODO: It may be more appropriate to obtain the track name as a join in
|
||||
* the retrieval phase of the search.
|
||||
*/
|
||||
String trackName = null;
|
||||
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.ic_marker_yellow_pushpin : R.drawable.ic_marker_blue_pushpin);
|
||||
resultMap.put(ICON_CONTENT_DESCRIPTION_ID_FIELD, R.string.icon_marker);
|
||||
resultMap.put(NAME_FIELD, waypoint.getName());
|
||||
resultMap.put(SHARED_OWNER_FIELD, sharedOwner);
|
||||
// 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(PHOTO_URL_FIELD, waypoint.getPhotoUrl());
|
||||
resultMap.put(START_TIME_FIELD, waypoint.getLocation().getTime());
|
||||
resultMap.put(CATEGORY_FIELD, statistics ? null : waypoint.getCategory());
|
||||
resultMap.put(DESCRIPTION_FIELD, statistics ? null : waypoint.getDescription());
|
||||
resultMap.put(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(SHARED_OWNER_FIELD, track.getSharedOwner());
|
||||
resultMap.put(TOTAL_TIME_FIELD, StringUtils.formatElapsedTime(tripStatitics.getTotalTime()));
|
||||
resultMap.put(TOTAL_DISTANCE_FIELD,
|
||||
StringUtils.formatDistance(this, tripStatitics.getTotalDistance(), metricUnits));
|
||||
resultMap.put(PHOTO_URL_FIELD, null);
|
||||
resultMap.put(START_TIME_FIELD, tripStatitics.getStartTime());
|
||||
resultMap.put(CATEGORY_FIELD, category);
|
||||
resultMap.put(DESCRIPTION_FIELD, track.getDescription());
|
||||
resultMap.put(TRACK_ID_FIELD, track.getId());
|
||||
resultMap.put(MARKER_ID_FIELD, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDeleteMarkerDone() {
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
handleIntent(getIntent());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected TrackRecordingServiceConnection getTrackRecordingServiceConnection() {
|
||||
return trackRecordingServiceConnection;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDeleted() {
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
handleIntent(getIntent());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
* Copyright 2013 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
|
||||
package com.google.android.apps.mytracks;
|
||||
|
||||
import com.google.android.apps.mytracks.content.TracksColumns;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.support.v4.widget.ResourceCursorAdapter;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
/**
|
||||
* A section based resource cursor adapter. Currently only supports one
|
||||
* additional section header, "Shared with me".
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public abstract class SectionResourceCursorAdapter extends ResourceCursorAdapter {
|
||||
|
||||
private enum ItemType {
|
||||
TRACK, HEADER
|
||||
}
|
||||
|
||||
private final LayoutInflater layoutInflater;
|
||||
private int sharedWithMeIndex = -1;
|
||||
|
||||
public SectionResourceCursorAdapter(Context context, int layout, Cursor cursor, int flags) {
|
||||
super(context, layout, cursor, flags);
|
||||
this.layoutInflater = LayoutInflater.from(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getView(int position, View convertView, ViewGroup parent) {
|
||||
if (getItemViewType(position) == ItemType.TRACK.ordinal()) {
|
||||
return super.getView(getCursorPosition(position), convertView, parent);
|
||||
} else {
|
||||
TextView textView;
|
||||
if (convertView == null) {
|
||||
textView = (TextView) layoutInflater.inflate(R.layout.track_list_header, parent, false);
|
||||
} else {
|
||||
textView = (TextView) convertView;
|
||||
}
|
||||
return textView;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getViewTypeCount() {
|
||||
return ItemType.values().length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
int count = super.getCount();
|
||||
return count != 0 && sharedWithMeIndex != -1 ? count + 1 : count;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled(int position) {
|
||||
return getItemViewType(position) == ItemType.TRACK.ordinal();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getItem(int position) {
|
||||
if (getItemViewType(position) == ItemType.TRACK.ordinal()) {
|
||||
return super.getItem(getCursorPosition(position));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
if (getItemViewType(position) == ItemType.TRACK.ordinal()) {
|
||||
return super.getItemId(getCursorPosition(position));
|
||||
}
|
||||
return -1L;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
if (sharedWithMeIndex != -1 && sharedWithMeIndex == position) {
|
||||
return ItemType.HEADER.ordinal();
|
||||
}
|
||||
return ItemType.TRACK.ordinal();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor swapCursor(Cursor newCursor) {
|
||||
Cursor oldCursor = super.swapCursor(newCursor);
|
||||
sharedWithMeIndex = getSharedWithMeIndex();
|
||||
return oldCursor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the Shared with me header index.
|
||||
*/
|
||||
private int getSharedWithMeIndex() {
|
||||
int i = 0;
|
||||
Cursor cursor = getCursor();
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
int index = cursor.getColumnIndex(TracksColumns.SHAREDWITHME);
|
||||
do {
|
||||
int sharedWithMe = cursor.getInt(index);
|
||||
if (sharedWithMe == 1) {
|
||||
// Returns the index where sharedWithMe == 1
|
||||
return i;
|
||||
}
|
||||
i++;
|
||||
} while (cursor.moveToNext());
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the underlying cursor position.
|
||||
*
|
||||
* @param position the requested position
|
||||
*/
|
||||
private int getCursorPosition(int position) {
|
||||
if (sharedWithMeIndex != -1 && position > sharedWithMeIndex) {
|
||||
return position - 1;
|
||||
}
|
||||
return position;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
/*
|
||||
* 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.Handler;
|
||||
import android.os.RemoteException;
|
||||
import android.text.format.DateFormat;
|
||||
import android.util.Log;
|
||||
import android.widget.TextView;
|
||||
|
||||
/**
|
||||
* An activity that displays information about sensors.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class SensorStateActivity extends AbstractMyTracksActivity {
|
||||
|
||||
private static final String TAG = SensorStateActivity.class.getName();
|
||||
private static final long ONE_SECOND = 1000;
|
||||
|
||||
private TrackRecordingServiceConnection trackRecordingServiceConnection;
|
||||
private Handler handler;
|
||||
private SensorManager tempSensorManager;
|
||||
|
||||
private final Runnable updateUiRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
ITrackRecordingService trackRecordingService = trackRecordingServiceConnection
|
||||
.getServiceIfBound();
|
||||
|
||||
// Check if service is available and recording
|
||||
boolean isRecording = false;
|
||||
if (trackRecordingService != null) {
|
||||
try {
|
||||
isRecording = trackRecordingService.isRecording();
|
||||
} catch (RemoteException e) {
|
||||
Log.e(TAG, "Unable to determine if the track recording service is recording.", e);
|
||||
}
|
||||
}
|
||||
|
||||
if (!isRecording) {
|
||||
updateFromTempSensorManager();
|
||||
} else {
|
||||
stopTempSensorManager();
|
||||
updateFromSystemSensorManager();
|
||||
}
|
||||
handler.postDelayed(this, ONE_SECOND);
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
trackRecordingServiceConnection = new TrackRecordingServiceConnection(this, null);
|
||||
handler = new Handler();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStart() {
|
||||
super.onStart();
|
||||
TrackRecordingServiceConnectionUtils.startConnection(this, trackRecordingServiceConnection);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
handler.post(updateUiRunnable);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
handler.removeCallbacks(updateUiRunnable);
|
||||
stopTempSensorManager();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStop() {
|
||||
super.onStop();
|
||||
trackRecordingServiceConnection.unbind();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getLayoutResId() {
|
||||
return R.layout.sensor_state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the temp sensor manager.
|
||||
*/
|
||||
private void stopTempSensorManager() {
|
||||
if (tempSensorManager != null) {
|
||||
SensorManagerFactory.releaseTempSensorManager();
|
||||
tempSensorManager = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates from a temp sensor manager.
|
||||
*/
|
||||
private void updateFromTempSensorManager() {
|
||||
Sensor.SensorState sensorState = Sensor.SensorState.NONE;
|
||||
Sensor.SensorDataSet sensorDataSet = null;
|
||||
|
||||
if (tempSensorManager == null) {
|
||||
tempSensorManager = SensorManagerFactory.getTempSensorManager(this);
|
||||
}
|
||||
|
||||
if (tempSensorManager != null) {
|
||||
sensorState = tempSensorManager.getSensorState();
|
||||
sensorDataSet = tempSensorManager.getSensorDataSet();
|
||||
}
|
||||
|
||||
updateSensorStateAndDataSet(sensorState, sensorDataSet);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates from the system sensor manager.
|
||||
*/
|
||||
private void updateFromSystemSensorManager() {
|
||||
Sensor.SensorState sensorState = Sensor.SensorState.NONE;
|
||||
Sensor.SensorDataSet sensorDataSet = null;
|
||||
|
||||
ITrackRecordingService trackRecordingService = trackRecordingServiceConnection
|
||||
.getServiceIfBound();
|
||||
|
||||
// Get sensor details from the service.
|
||||
if (trackRecordingService == null) {
|
||||
Log.d(TAG, "Cannot get teh track recording service.");
|
||||
} else {
|
||||
try {
|
||||
sensorState = Sensor.SensorState.valueOf(trackRecordingService.getSensorState());
|
||||
} catch (RemoteException e) {
|
||||
Log.e(TAG, "Cannote read sensor state.", e);
|
||||
sensorState = Sensor.SensorState.NONE;
|
||||
}
|
||||
|
||||
try {
|
||||
byte[] buff = trackRecordingService.getSensorData();
|
||||
if (buff != null) {
|
||||
sensorDataSet = Sensor.SensorDataSet.parseFrom(buff);
|
||||
}
|
||||
} catch (RemoteException e) {
|
||||
Log.e(TAG, "Cannot read sensor data set.", e);
|
||||
} catch (InvalidProtocolBufferException e) {
|
||||
Log.e(TAG, "Cannot read sensor data set.", e);
|
||||
}
|
||||
}
|
||||
|
||||
updateSensorStateAndDataSet(sensorState, sensorDataSet);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the sensor state and data set.
|
||||
*
|
||||
* @param sensorState sensor state
|
||||
* @param sensorDataSet sensor data set
|
||||
*/
|
||||
private void updateSensorStateAndDataSet(
|
||||
Sensor.SensorState sensorState, Sensor.SensorDataSet sensorDataSet) {
|
||||
((TextView) findViewById(R.id.sensor_state)).setText(
|
||||
SensorUtils.getStateAsString(sensorState, this));
|
||||
|
||||
String lastSensorTime = sensorDataSet == null ? getString(R.string.value_unknown)
|
||||
: getLastSensorTime(sensorDataSet);
|
||||
String heartRate = sensorDataSet == null ? getString(R.string.value_unknown)
|
||||
: getHeartRate(sensorDataSet);
|
||||
String cadence = sensorDataSet == null ? getString(R.string.value_unknown)
|
||||
: getCadence(sensorDataSet);
|
||||
String power = sensorDataSet == null ? getString(R.string.value_unknown)
|
||||
: getPower(sensorDataSet);
|
||||
String battery = sensorDataSet == null ? getString(R.string.value_unknown)
|
||||
: getBattery(sensorDataSet);
|
||||
|
||||
((TextView) findViewById(R.id.sensor_state_last_sensor_time)).setText(lastSensorTime);
|
||||
((TextView) findViewById(R.id.sensor_state_heart_rate)).setText(heartRate);
|
||||
((TextView) findViewById(R.id.sensor_state_cadence)).setText(cadence);
|
||||
((TextView) findViewById(R.id.sensor_state_power)).setText(power);
|
||||
((TextView) findViewById(R.id.sensor_state_battery)).setText(battery);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the last sensor time.
|
||||
*
|
||||
* @param sensorDataSet sensor data set
|
||||
*/
|
||||
private String getLastSensorTime(Sensor.SensorDataSet sensorDataSet) {
|
||||
return DateFormat.format("h:mm:ss aa", sensorDataSet.getCreationTime()).toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the heart rate.
|
||||
*
|
||||
* @param sensorDataSet sensor data set
|
||||
*/
|
||||
private String getHeartRate(Sensor.SensorDataSet sensorDataSet) {
|
||||
String value;
|
||||
if (sensorDataSet.hasHeartRate() && sensorDataSet.getHeartRate().hasValue()
|
||||
&& sensorDataSet.getHeartRate().getState() == Sensor.SensorState.SENDING) {
|
||||
value = getString(
|
||||
R.string.sensor_state_heart_rate_value, sensorDataSet.getHeartRate().getValue());
|
||||
} else {
|
||||
value = SensorUtils.getStateAsString(
|
||||
sensorDataSet.hasHeartRate() ? sensorDataSet.getHeartRate().getState()
|
||||
: Sensor.SensorState.NONE, this);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the cadence.
|
||||
*
|
||||
* @param sensorDataSet sensor data set
|
||||
*/
|
||||
private String getCadence(Sensor.SensorDataSet sensorDataSet) {
|
||||
String value;
|
||||
if (sensorDataSet.hasCadence() && sensorDataSet.getCadence().hasValue()
|
||||
&& sensorDataSet.getCadence().getState() == Sensor.SensorState.SENDING) {
|
||||
value = getString(R.string.sensor_state_cadence_value, sensorDataSet.getCadence().getValue());
|
||||
} else {
|
||||
value = SensorUtils.getStateAsString(
|
||||
sensorDataSet.hasCadence() ? sensorDataSet.getCadence().getState()
|
||||
: Sensor.SensorState.NONE, this);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the power.
|
||||
*
|
||||
* @param sensorDataSet sensor data set
|
||||
*/
|
||||
private String getPower(Sensor.SensorDataSet sensorDataSet) {
|
||||
String value;
|
||||
if (sensorDataSet.hasPower() && sensorDataSet.getPower().hasValue()
|
||||
&& sensorDataSet.getPower().getState() == Sensor.SensorState.SENDING) {
|
||||
value = getString(R.string.sensor_state_power_value, sensorDataSet.getPower().getValue());
|
||||
} else {
|
||||
value = SensorUtils.getStateAsString(
|
||||
sensorDataSet.hasPower() ? sensorDataSet.getPower().getState() : Sensor.SensorState.NONE,
|
||||
this);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the battery.
|
||||
*
|
||||
* @param sensorDataSet sensor data set
|
||||
*/
|
||||
private String getBattery(Sensor.SensorDataSet sensorDataSet) {
|
||||
String value;
|
||||
if (sensorDataSet.hasBatteryLevel() && sensorDataSet.getBatteryLevel().hasValue()
|
||||
&& sensorDataSet.getBatteryLevel().getState() == Sensor.SensorState.SENDING) {
|
||||
value = getString(R.string.value_integer_percent, sensorDataSet.getBatteryLevel().getValue());
|
||||
} else {
|
||||
value = SensorUtils.getStateAsString(
|
||||
sensorDataSet.hasBatteryLevel() ? sensorDataSet.getBatteryLevel().getState()
|
||||
: Sensor.SensorState.NONE, this);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
/*
|
||||
* Copyright 2013q Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
|
||||
package com.google.android.apps.mytracks;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.app.FragmentActivity;
|
||||
import android.support.v4.app.FragmentPagerAdapter;
|
||||
import android.support.v4.view.ViewPager;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TabHost;
|
||||
import android.widget.TabHost.TabContentFactory;
|
||||
import android.widget.TabWidget;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* This is a helper class that implements the management of tabs and all details
|
||||
* of connecting a ViewPager with associated TabHost. It relies on a trick.
|
||||
* Normally a tab host has a simple API for supplying a View or Intent that each
|
||||
* tab will show. This is not sufficient for switching between pages. So instead
|
||||
* we make the content part of the tab host 0dp high (it is not shown) and the
|
||||
* TabsAdapter supplies its own dummy view to show as the tab content. It
|
||||
* listens to changes in tabs, and takes care of switch to the correct paged in
|
||||
* the ViewPager whenever the selected tab changes.
|
||||
* <p>
|
||||
* Copied from the FragmentTabsPager sample in the support library.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class TabsAdapter extends FragmentPagerAdapter
|
||||
implements TabHost.OnTabChangeListener, ViewPager.OnPageChangeListener {
|
||||
|
||||
private final Context context;
|
||||
private final TabHost tabHost;
|
||||
private final ViewPager viewPager;
|
||||
private final ArrayList<TabInfo> tabInfos = new ArrayList<TabInfo>();
|
||||
|
||||
/**
|
||||
* An object to hold a tab's info.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
private static final class TabInfo {
|
||||
|
||||
private final Class<?> clss;
|
||||
private final Bundle bundle;
|
||||
|
||||
public TabInfo(Class<?> clss, Bundle bunlde) {
|
||||
this.clss = clss;
|
||||
this.bundle = bunlde;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A dummy {@link TabContentFactory} that creates an empty view to satisfy the
|
||||
* {@link TabHost} API.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
private static class DummyTabFactory implements TabHost.TabContentFactory {
|
||||
|
||||
private final Context context;
|
||||
|
||||
public DummyTabFactory(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View createTabContent(String tag) {
|
||||
View view = new View(context);
|
||||
view.setMinimumWidth(0);
|
||||
view.setMinimumHeight(0);
|
||||
return view;
|
||||
}
|
||||
}
|
||||
|
||||
public TabsAdapter(FragmentActivity activity, TabHost tabHost, ViewPager viewPager) {
|
||||
super(activity.getSupportFragmentManager());
|
||||
this.context = activity;
|
||||
this.tabHost = tabHost;
|
||||
this.viewPager = viewPager;
|
||||
this.tabHost.setOnTabChangedListener(this);
|
||||
this.viewPager.setAdapter(this);
|
||||
this.viewPager.setOnPageChangeListener(this);
|
||||
}
|
||||
|
||||
public void addTab(TabHost.TabSpec tabSpec, Class<?> clss, Bundle bundle) {
|
||||
tabSpec.setContent(new DummyTabFactory(context));
|
||||
|
||||
TabInfo info = new TabInfo(clss, bundle);
|
||||
|
||||
tabInfos.add(info);
|
||||
tabHost.addTab(tabSpec);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return tabInfos.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Fragment getItem(int position) {
|
||||
TabInfo info = tabInfos.get(position);
|
||||
return Fragment.instantiate(context, info.clss.getName(), info.bundle);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTabChanged(String tabId) {
|
||||
int position = tabHost.getCurrentTab();
|
||||
viewPager.setCurrentItem(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {}
|
||||
|
||||
@Override
|
||||
public void onPageSelected(int position) {
|
||||
/*
|
||||
* Unfortunately when TabHost changes the current tab, it kindly also takes
|
||||
* care of putting focus on it when not in touch mode. The jerk. This hack
|
||||
* tries to prevent this from pulling focus out of our ViewPager.
|
||||
*/
|
||||
TabWidget tabWidget = tabHost.getTabWidget();
|
||||
int oldFocusability = tabWidget.getDescendantFocusability();
|
||||
tabWidget.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
|
||||
tabHost.setCurrentTab(position);
|
||||
tabWidget.setDescendantFocusability(oldFocusability);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageScrollStateChanged(int state) {}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
/*
|
||||
* 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;
|
||||
boolean visible = alwaysShow || isRecording;
|
||||
containerView.setVisibility(visible ? View.VISIBLE : View.GONE);
|
||||
|
||||
if (!visible) {
|
||||
handler.removeCallbacks(updateTotalTimeRunnable);
|
||||
return;
|
||||
}
|
||||
|
||||
recordImageButton.setImageResource(
|
||||
isRecording && !isPaused ? R.drawable.button_pause : R.drawable.button_record);
|
||||
recordImageButton.setContentDescription(activity.getString(
|
||||
isRecording && !isPaused ? R.string.icon_pause_recording : R.string.icon_record_track));
|
||||
|
||||
stopImageButton.setImageResource(
|
||||
isRecording ? R.drawable.button_stop : R.drawable.ic_button_stop_disabled);
|
||||
stopImageButton.setEnabled(isRecording);
|
||||
|
||||
statusTextView.setVisibility(isRecording ? View.VISIBLE : View.INVISIBLE);
|
||||
if (isRecording) {
|
||||
statusTextView.setTextColor(activity.getResources()
|
||||
.getColor(isPaused ? android.R.color.white : R.color.recording_text));
|
||||
statusTextView.setText(isPaused ? R.string.generic_paused : R.string.generic_recording);
|
||||
}
|
||||
|
||||
handler.removeCallbacks(updateTotalTimeRunnable);
|
||||
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;
|
||||
handler.removeCallbacks(updateTotalTimeRunnable);
|
||||
}
|
||||
|
||||
public void hide() {
|
||||
containerView.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
public void show() {
|
||||
containerView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the total time for the current recording track.
|
||||
*/
|
||||
private long getTotalTime() {
|
||||
ITrackRecordingService trackRecordingService = trackRecordingServiceConnection
|
||||
.getServiceIfBound();
|
||||
try {
|
||||
return trackRecordingService != null ? trackRecordingService.getTotalTime() : 0L;
|
||||
} catch (RemoteException e) {
|
||||
Log.e(TAG, "exception", e);
|
||||
return 0L;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,628 @@
|
||||
/*
|
||||
* Copyright 2008 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
|
||||
package com.google.android.apps.mytracks;
|
||||
|
||||
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
|
||||
import com.google.android.apps.mytracks.content.Track;
|
||||
import com.google.android.apps.mytracks.content.TrackDataHub;
|
||||
import com.google.android.apps.mytracks.content.Waypoint;
|
||||
import com.google.android.apps.mytracks.content.Waypoint.WaypointType;
|
||||
import com.google.android.apps.mytracks.content.WaypointCreationRequest;
|
||||
import com.google.android.apps.mytracks.fragments.ChartFragment;
|
||||
import com.google.android.apps.mytracks.fragments.ConfirmDeleteDialogFragment;
|
||||
import com.google.android.apps.mytracks.fragments.ExportDialogFragment;
|
||||
import com.google.android.apps.mytracks.fragments.ExportDialogFragment.ExportCaller;
|
||||
import com.google.android.apps.mytracks.fragments.ExportDialogFragment.ExportType;
|
||||
import com.google.android.apps.mytracks.fragments.FrequencyDialogFragment;
|
||||
import com.google.android.apps.mytracks.fragments.MyTracksMapFragment;
|
||||
import com.google.android.apps.mytracks.fragments.StatsFragment;
|
||||
import com.google.android.apps.mytracks.io.file.TrackFileFormat;
|
||||
import com.google.android.apps.mytracks.io.file.export.SaveActivity;
|
||||
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.ApiAdapterFactory;
|
||||
import com.google.android.apps.mytracks.util.FileUtils;
|
||||
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.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Environment;
|
||||
import android.os.Parcelable;
|
||||
import android.provider.MediaStore;
|
||||
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 android.widget.Toast;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* An activity to show the track detail.
|
||||
*
|
||||
* @author Leif Hendrik Wilden
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
public class TrackDetailActivity extends AbstractSendToGoogleActivity implements ExportCaller {
|
||||
|
||||
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";
|
||||
private static final String PHOTO_URI_KEY = "photo_uri_key";
|
||||
private static final String HAS_PHOTO_KEY = "has_photo_key";
|
||||
|
||||
// The following are set in onCreate
|
||||
private boolean hasCamera;
|
||||
private Uri photoUri;
|
||||
private boolean hasPhoto;
|
||||
private MyTracksProviderUtils myTracksProviderUtils;
|
||||
private SharedPreferences sharedPreferences;
|
||||
private TrackRecordingServiceConnection trackRecordingServiceConnection;
|
||||
private TrackDataHub trackDataHub;
|
||||
private TabHost tabHost;
|
||||
private ViewPager viewPager;
|
||||
private TabsAdapter tabsAdapter;
|
||||
private TrackController trackController;
|
||||
|
||||
// From intent
|
||||
private long trackId;
|
||||
private long markerId;
|
||||
|
||||
// Preferences
|
||||
private long recordingTrackId = PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
|
||||
private boolean recordingTrackPaused = PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT;
|
||||
private String sensorType = PreferencesUtils.SENSOR_TYPE_DEFAULT;
|
||||
|
||||
private MenuItem insertMarkerMenuItem;
|
||||
private MenuItem insertPhotoMenuItem;
|
||||
private MenuItem playMenuItem;
|
||||
private MenuItem shareMenuItem;
|
||||
private MenuItem exportMenuItem;
|
||||
private MenuItem voiceFrequencyMenuItem;
|
||||
private MenuItem splitFrequencyMenuItem;
|
||||
private MenuItem sensorStateMenuItem;
|
||||
|
||||
private final Runnable bindChangedCallback = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// After binding changes (is available), update the total time in
|
||||
// trackController.
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
trackController.update(trackId == recordingTrackId, recordingTrackPaused);
|
||||
if (hasPhoto && photoUri != null) {
|
||||
WaypointCreationRequest waypointCreationRequest = new WaypointCreationRequest(
|
||||
WaypointType.WAYPOINT, false, null, null, null, null, photoUri.toString());
|
||||
long id = TrackRecordingServiceConnectionUtils.addMarker(
|
||||
TrackDetailActivity.this, trackRecordingServiceConnection, waypointCreationRequest);
|
||||
hasPhoto = false;
|
||||
|
||||
if (id != -1L) {
|
||||
Intent mediaScanIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
|
||||
mediaScanIntent.setData(photoUri);
|
||||
sendBroadcast(mediaScanIntent);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* Note that sharedPreferenceChangeListener cannot be an anonymous inner
|
||||
* class. Anonymous inner class will get garbage collected.
|
||||
*/
|
||||
private final OnSharedPreferenceChangeListener
|
||||
sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
|
||||
// Note that key can be null
|
||||
if (key == null || key.equals(
|
||||
PreferencesUtils.getKey(TrackDetailActivity.this, R.string.recording_track_id_key))) {
|
||||
recordingTrackId = PreferencesUtils.getLong(
|
||||
TrackDetailActivity.this, R.string.recording_track_id_key);
|
||||
}
|
||||
if (key == null || key.equals(PreferencesUtils.getKey(
|
||||
TrackDetailActivity.this, R.string.recording_track_paused_key))) {
|
||||
recordingTrackPaused = PreferencesUtils.getBoolean(TrackDetailActivity.this,
|
||||
R.string.recording_track_paused_key,
|
||||
PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT);
|
||||
}
|
||||
if (key == null || key.equals(
|
||||
PreferencesUtils.getKey(TrackDetailActivity.this, R.string.sensor_type_key))) {
|
||||
sensorType = PreferencesUtils.getString(TrackDetailActivity.this,
|
||||
R.string.sensor_type_key, PreferencesUtils.SENSOR_TYPE_DEFAULT);
|
||||
}
|
||||
if (key != null) {
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
ApiAdapterFactory.getApiAdapter().invalidMenu(TrackDetailActivity.this);
|
||||
boolean isRecording = trackId == recordingTrackId;
|
||||
trackController.update(isRecording, recordingTrackPaused);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private final OnClickListener recordListener = new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (recordingTrackPaused) {
|
||||
// Paused -> Resume
|
||||
AnalyticsUtils.sendPageViews(TrackDetailActivity.this, AnalyticsUtils.ACTION_RESUME_TRACK);
|
||||
updateMenuItems(true, false);
|
||||
TrackRecordingServiceConnectionUtils.resumeTrack(trackRecordingServiceConnection);
|
||||
trackController.update(true, false);
|
||||
} else {
|
||||
// Recording -> Paused
|
||||
AnalyticsUtils.sendPageViews(TrackDetailActivity.this, AnalyticsUtils.ACTION_PAUSE_TRACK);
|
||||
updateMenuItems(true, true);
|
||||
TrackRecordingServiceConnectionUtils.pauseTrack(trackRecordingServiceConnection);
|
||||
trackController.update(true, true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private final OnClickListener stopListener = new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
AnalyticsUtils.sendPageViews(TrackDetailActivity.this, AnalyticsUtils.ACTION_STOP_RECORDING);
|
||||
updateMenuItems(false, true);
|
||||
TrackRecordingServiceConnectionUtils.stopRecording(
|
||||
TrackDetailActivity.this, trackRecordingServiceConnection, true);
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
hasCamera = getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA);
|
||||
photoUri = savedInstanceState != null ? (Uri) savedInstanceState.getParcelable(PHOTO_URI_KEY) : null;
|
||||
hasPhoto = savedInstanceState != null ? savedInstanceState.getBoolean(HAS_PHOTO_KEY, false) : false;
|
||||
|
||||
myTracksProviderUtils = MyTracksProviderUtils.Factory.get(this);
|
||||
handleIntent(getIntent());
|
||||
|
||||
sharedPreferences = getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
|
||||
|
||||
trackRecordingServiceConnection = new TrackRecordingServiceConnection(
|
||||
this, bindChangedCallback);
|
||||
trackDataHub = TrackDataHub.newInstance(this);
|
||||
|
||||
tabHost = (TabHost) findViewById(android.R.id.tabhost);
|
||||
tabHost.setup();
|
||||
|
||||
viewPager = (ViewPager) findViewById(R.id.pager);
|
||||
|
||||
tabsAdapter = new TabsAdapter(this, tabHost, viewPager);
|
||||
|
||||
TabSpec mapTabSpec = tabHost.newTabSpec(MyTracksMapFragment.MAP_FRAGMENT_TAG).setIndicator(
|
||||
getString(R.string.track_detail_map_tab),
|
||||
getResources().getDrawable(R.drawable.ic_tab_map));
|
||||
tabsAdapter.addTab(mapTabSpec, MyTracksMapFragment.class, null);
|
||||
|
||||
TabSpec chartTabSpec = tabHost.newTabSpec(ChartFragment.CHART_FRAGMENT_TAG).setIndicator(
|
||||
getString(R.string.track_detail_chart_tab),
|
||||
getResources().getDrawable(R.drawable.ic_tab_chart));
|
||||
tabsAdapter.addTab(chartTabSpec, ChartFragment.class, null);
|
||||
|
||||
TabSpec statsTabSpec = tabHost.newTabSpec(StatsFragment.STATS_FRAGMENT_TAG).setIndicator(
|
||||
getString(R.string.track_detail_stats_tab),
|
||||
getResources().getDrawable(R.drawable.ic_tab_stats));
|
||||
tabsAdapter.addTab(statsTabSpec, StatsFragment.class, null);
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
tabHost.setCurrentTabByTag(savedInstanceState.getString(CURRENT_TAB_TAG_KEY));
|
||||
}
|
||||
|
||||
// Set the background after all three tabs are added
|
||||
ApiAdapterFactory.getApiAdapter().setTabBackground(tabHost.getTabWidget());
|
||||
|
||||
trackController = new TrackController(
|
||||
this, trackRecordingServiceConnection, false, recordListener, stopListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStart() {
|
||||
super.onStart();
|
||||
|
||||
sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
|
||||
sharedPreferenceChangeListener.onSharedPreferenceChanged(null, null);
|
||||
|
||||
TrackRecordingServiceConnectionUtils.startConnection(this, trackRecordingServiceConnection);
|
||||
trackDataHub.start();
|
||||
AnalyticsUtils.sendPageViews(this, AnalyticsUtils.PAGE_TRACK_DETAIL);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
trackDataHub.loadTrack(trackId);
|
||||
|
||||
// Update UI
|
||||
ApiAdapterFactory.getApiAdapter().invalidMenu(this);
|
||||
boolean isRecording = trackId == recordingTrackId;
|
||||
trackController.onResume(isRecording, recordingTrackPaused);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
trackController.onPause();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStop() {
|
||||
super.onStop();
|
||||
sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
|
||||
trackRecordingServiceConnection.unbind();
|
||||
trackDataHub.stop();
|
||||
AnalyticsUtils.dispatch();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putString(CURRENT_TAB_TAG_KEY, tabHost.getCurrentTabTag());
|
||||
if (photoUri != null) {
|
||||
outState.putParcelable(PHOTO_URI_KEY, photoUri);
|
||||
}
|
||||
outState.putBoolean(HAS_PHOTO_KEY, hasPhoto);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
if (requestCode == CAMERA_REQUEST_CODE) {
|
||||
hasPhoto = resultCode == RESULT_OK;
|
||||
} else {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getLayoutResId() {
|
||||
return R.layout.track_detail;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean hideTitle() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onHomeSelected() {
|
||||
/*
|
||||
* According to
|
||||
* http://developer.android.com/training/implementing-navigation
|
||||
* /ancestral.html, we should use NavUtils.shouldUpRecreateTask instead of
|
||||
* always creating a new back stack. However, NavUtils.shouldUpRecreateTask
|
||||
* seems to always return false.
|
||||
*/
|
||||
TaskStackBuilder.create(this).addParentStack(TrackDetailActivity.class).startActivities();
|
||||
finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNewIntent(Intent intent) {
|
||||
setIntent(intent);
|
||||
handleIntent(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
getMenuInflater().inflate(R.menu.track_detail, menu);
|
||||
|
||||
Track track = myTracksProviderUtils.getTrack(trackId);
|
||||
boolean isSharedWithMe = track != null ? track.isSharedWithMe() : true;
|
||||
|
||||
menu.findItem(R.id.track_detail_edit).setVisible(!isSharedWithMe);
|
||||
menu.findItem(R.id.track_detail_feedback)
|
||||
.setVisible(ApiAdapterFactory.getApiAdapter().isGoogleFeedbackAvailable());
|
||||
|
||||
insertMarkerMenuItem = menu.findItem(R.id.track_detail_insert_marker);
|
||||
insertPhotoMenuItem = menu.findItem(R.id.track_detail_insert_photo);
|
||||
playMenuItem = menu.findItem(R.id.track_detail_play);
|
||||
|
||||
shareMenuItem = menu.findItem(R.id.track_detail_share);
|
||||
shareMenuItem.setEnabled(!isSharedWithMe);
|
||||
shareMenuItem.setVisible(!isSharedWithMe);
|
||||
|
||||
exportMenuItem = menu.findItem(R.id.track_detail_export);
|
||||
voiceFrequencyMenuItem = menu.findItem(R.id.track_detail_voice_frequency);
|
||||
splitFrequencyMenuItem = menu.findItem(R.id.track_detail_split_frequency);
|
||||
sensorStateMenuItem = menu.findItem(R.id.track_detail_sensor_state);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareOptionsMenu(Menu menu) {
|
||||
updateMenuItems(trackId == recordingTrackId, recordingTrackPaused);
|
||||
return super.onPrepareOptionsMenu(menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
Intent intent;
|
||||
switch (item.getItemId()) {
|
||||
case R.id.track_detail_insert_marker:
|
||||
AnalyticsUtils.sendPageViews(this, AnalyticsUtils.ACTION_INSERT_MARKER);
|
||||
intent = IntentUtils.newIntent(this, MarkerEditActivity.class)
|
||||
.putExtra(MarkerEditActivity.EXTRA_TRACK_ID, trackId);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
case R.id.track_detail_insert_photo:
|
||||
try {
|
||||
if (!FileUtils.isExternalStorageWriteable()) {
|
||||
Toast.makeText(this, R.string.external_storage_not_writable, Toast.LENGTH_LONG).show();
|
||||
return false;
|
||||
}
|
||||
|
||||
File dir = new File(
|
||||
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES),
|
||||
FileUtils.SDCARD_TOP_DIR);
|
||||
|
||||
FileUtils.ensureDirectoryExists(dir);
|
||||
|
||||
String fileName = SimpleDateFormat.getDateTimeInstance().format(new Date());
|
||||
File file = new File(dir, FileUtils.buildUniqueFileName(dir, fileName, "jpeg"));
|
||||
|
||||
if (file.exists()) {
|
||||
Toast.makeText(this, R.string.marker_insert_photo_error, Toast.LENGTH_LONG).show();
|
||||
return false;
|
||||
}
|
||||
file.createNewFile();
|
||||
photoUri = Uri.fromFile(file);
|
||||
intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE).putExtra(
|
||||
MediaStore.EXTRA_OUTPUT, photoUri);
|
||||
startActivityForResult(intent, CAMERA_REQUEST_CODE);
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Unable to insert photo marker", e);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
case R.id.track_detail_play:
|
||||
confirmPlay(new long[] {trackId});
|
||||
return true;
|
||||
case R.id.track_detail_share:
|
||||
shareTrack(trackId);
|
||||
return true;
|
||||
case R.id.track_detail_markers:
|
||||
intent = IntentUtils.newIntent(this, MarkerListActivity.class)
|
||||
.putExtra(MarkerListActivity.EXTRA_TRACK_ID, trackId);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
case R.id.track_detail_voice_frequency:
|
||||
FrequencyDialogFragment.newInstance(R.string.voice_frequency_key,
|
||||
PreferencesUtils.VOICE_FREQUENCY_DEFAULT, R.string.menu_voice_frequency)
|
||||
.show(getSupportFragmentManager(), FrequencyDialogFragment.FREQUENCY_DIALOG_TAG);
|
||||
return true;
|
||||
case R.id.track_detail_split_frequency:
|
||||
FrequencyDialogFragment.newInstance(R.string.split_frequency_key,
|
||||
PreferencesUtils.SPLIT_FREQUENCY_DEFAULT, R.string.menu_split_frequency)
|
||||
.show(getSupportFragmentManager(), FrequencyDialogFragment.FREQUENCY_DIALOG_TAG);
|
||||
return true;
|
||||
case R.id.track_detail_export:
|
||||
new ExportDialogFragment().show(getSupportFragmentManager(),
|
||||
ExportDialogFragment.EXPORT_DIALOG_TAG);
|
||||
return true;
|
||||
case R.id.track_detail_edit:
|
||||
intent = IntentUtils.newIntent(this, TrackEditActivity.class)
|
||||
.putExtra(TrackEditActivity.EXTRA_TRACK_ID, trackId);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
case R.id.track_detail_delete:
|
||||
ConfirmDeleteDialogFragment.newInstance(new long[] { trackId })
|
||||
.show(getSupportFragmentManager(), ConfirmDeleteDialogFragment.CONFIRM_DELETE_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 onExportDone(ExportType exportType, TrackFileFormat trackFileFormat) {
|
||||
if (exportType == ExportType.EXTERNAL_STORAGE) {
|
||||
AnalyticsUtils.sendPageViews(this,
|
||||
AnalyticsUtils.ACTION_EXPORT_PREFIX + trackFileFormat.getExtension());
|
||||
Intent intent = IntentUtils.newIntent(this, SaveActivity.class)
|
||||
.putExtra(SaveActivity.EXTRA_TRACK_IDS, new long[] { trackId })
|
||||
.putExtra(SaveActivity.EXTRA_TRACK_FILE_FORMAT, (Parcelable) trackFileFormat);
|
||||
startActivity(intent);
|
||||
} else {
|
||||
SendRequest sendRequest = new SendRequest(trackId);
|
||||
String pageView;
|
||||
switch (exportType) {
|
||||
case GOOGLE_MAPS:
|
||||
pageView = AnalyticsUtils.ACTION_EXPORT_MAPS;
|
||||
sendRequest.setSendMaps(true);
|
||||
break;
|
||||
case GOOGLE_FUSION_TABLES:
|
||||
pageView = AnalyticsUtils.ACTION_EXPORT_FUSION_TABLES;
|
||||
sendRequest.setSendFusionTables(true);
|
||||
break;
|
||||
default:
|
||||
pageView = AnalyticsUtils.ACTION_EXPORT_SPREADSHEETS;
|
||||
sendRequest.setSendSpreadsheets(true);
|
||||
}
|
||||
AnalyticsUtils.sendPageViews(this, pageView);
|
||||
sendToGoogle(sendRequest);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected TrackRecordingServiceConnection getTrackRecordingServiceConnection() {
|
||||
return trackRecordingServiceConnection;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDeleted() {
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
finish();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link TrackDataHub}.
|
||||
*/
|
||||
public TrackDataHub getTrackDataHub() {
|
||||
return trackDataHub;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the track id.
|
||||
*/
|
||||
public long getTrackId() {
|
||||
return trackId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the marker id.
|
||||
*/
|
||||
public long getMarkerId() {
|
||||
return markerId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the data in the intent.
|
||||
*/
|
||||
private void handleIntent(Intent intent) {
|
||||
trackId = intent.getLongExtra(EXTRA_TRACK_ID, -1L);
|
||||
markerId = intent.getLongExtra(EXTRA_MARKER_ID, -1L);
|
||||
if (markerId != -1L) {
|
||||
// Use the trackId from the marker
|
||||
Waypoint waypoint = myTracksProviderUtils.getWaypoint(markerId);
|
||||
if (waypoint == null) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
trackId = waypoint.getTrackId();
|
||||
}
|
||||
if (trackId == -1L) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
Track track = myTracksProviderUtils.getTrack(trackId);
|
||||
if (track == null) {
|
||||
// Use the last track if markerId is not set
|
||||
if (markerId == -1L) {
|
||||
track = myTracksProviderUtils.getLastTrack();
|
||||
if (track != null) {
|
||||
trackId = track.getId();
|
||||
return;
|
||||
}
|
||||
}
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the menu items.
|
||||
*
|
||||
* @param isRecording true if recording
|
||||
*/
|
||||
private void updateMenuItems(boolean isRecording, boolean isPaused) {
|
||||
if (insertMarkerMenuItem != null) {
|
||||
insertMarkerMenuItem.setVisible(isRecording && !isPaused);
|
||||
}
|
||||
if (insertPhotoMenuItem != null) {
|
||||
insertPhotoMenuItem.setVisible(hasCamera && isRecording && !isPaused);
|
||||
}
|
||||
if (playMenuItem != null) {
|
||||
playMenuItem.setVisible(!isRecording);
|
||||
}
|
||||
if (shareMenuItem != null && shareMenuItem.isEnabled()) {
|
||||
shareMenuItem.setVisible(!isRecording);
|
||||
}
|
||||
if (exportMenuItem != null) {
|
||||
exportMenuItem.setVisible(!isRecording);
|
||||
}
|
||||
if (voiceFrequencyMenuItem != null) {
|
||||
voiceFrequencyMenuItem.setVisible(isRecording);
|
||||
}
|
||||
if (splitFrequencyMenuItem != null) {
|
||||
splitFrequencyMenuItem.setVisible(isRecording);
|
||||
}
|
||||
if (sensorStateMenuItem != null) {
|
||||
sensorStateMenuItem.setVisible(!PreferencesUtils.SENSOR_TYPE_DEFAULT.equals(sensorType));
|
||||
}
|
||||
|
||||
String title;
|
||||
if (isRecording) {
|
||||
title = getString(isPaused ? R.string.generic_paused : R.string.generic_recording);
|
||||
} else {
|
||||
Track track = myTracksProviderUtils.getTrack(trackId);
|
||||
title = track != null ? track.getName() : "";
|
||||
}
|
||||
setTitle(title);
|
||||
}
|
||||
}
|
||||
@@ -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.ic_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,807 @@
|
||||
/*
|
||||
* 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.ConfirmDeleteDialogFragment;
|
||||
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.TrackFileFormat;
|
||||
import com.google.android.apps.mytracks.io.file.export.SaveActivity;
|
||||
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, FileTypeCaller {
|
||||
|
||||
private static final String TAG = TrackListActivity.class.getSimpleName();
|
||||
private static final String[] PROJECTION = new String[] { TracksColumns._ID, TracksColumns.NAME,
|
||||
TracksColumns.DESCRIPTION, TracksColumns.CATEGORY, TracksColumns.STARTTIME,
|
||||
TracksColumns.TOTALDISTANCE, TracksColumns.TOTALTIME, TracksColumns.ICON,
|
||||
TracksColumns.SHAREDWITHME, TracksColumns.SHAREDOWNER };
|
||||
|
||||
// Callback when the trackRecordingServiceConnection binding changes.
|
||||
private final Runnable bindChangedCallback = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
/*
|
||||
* After binding changes (e.g., becomes available), update the total time
|
||||
* in trackController.
|
||||
*/
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
trackController.update(recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT,
|
||||
recordingTrackPaused);
|
||||
}
|
||||
});
|
||||
|
||||
if (!startGps && !startNewRecording) {
|
||||
return;
|
||||
}
|
||||
|
||||
ITrackRecordingService service = trackRecordingServiceConnection.getServiceIfBound();
|
||||
if (service == null) {
|
||||
Log.d(TAG, "service not available to start gps or a new recording");
|
||||
return;
|
||||
}
|
||||
if (startNewRecording) {
|
||||
startGps = false;
|
||||
try {
|
||||
long trackId = service.startNewTrack();
|
||||
startNewRecording = false;
|
||||
Intent intent = IntentUtils.newIntent(TrackListActivity.this, TrackDetailActivity.class)
|
||||
.putExtra(TrackDetailActivity.EXTRA_TRACK_ID, trackId);
|
||||
startActivity(intent);
|
||||
Toast.makeText(
|
||||
TrackListActivity.this, R.string.track_list_record_success, Toast.LENGTH_SHORT)
|
||||
.show();
|
||||
} catch (RemoteException e) {
|
||||
Toast.makeText(
|
||||
TrackListActivity.this, R.string.track_list_record_error, Toast.LENGTH_LONG).show();
|
||||
Log.e(TAG, "Unable to start a new recording.", e);
|
||||
}
|
||||
}
|
||||
if (startGps) {
|
||||
try {
|
||||
service.startGps();
|
||||
startGps = false;
|
||||
} catch (RemoteException e) {
|
||||
Toast.makeText(TrackListActivity.this, R.string.gps_starting_error, Toast.LENGTH_LONG)
|
||||
.show();
|
||||
Log.e(TAG, "Unable to start gps");
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/*
|
||||
* Note that sharedPreferenceChangeListenr cannot be an anonymous inner class.
|
||||
* Anonymous inner class will get garbage collected.
|
||||
*/
|
||||
private final OnSharedPreferenceChangeListener
|
||||
sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() {
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
|
||||
if (key == null || key.equals(
|
||||
PreferencesUtils.getKey(TrackListActivity.this, R.string.stats_units_key))) {
|
||||
metricUnits = PreferencesUtils.isMetricUnits(TrackListActivity.this);
|
||||
}
|
||||
if (key == null || key.equals(
|
||||
PreferencesUtils.getKey(TrackListActivity.this, R.string.recording_track_id_key))) {
|
||||
recordingTrackId = PreferencesUtils.getLong(
|
||||
TrackListActivity.this, R.string.recording_track_id_key);
|
||||
if (key != null && recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT) {
|
||||
trackRecordingServiceConnection.startAndBind();
|
||||
}
|
||||
}
|
||||
if (key == null || key.equals(PreferencesUtils.getKey(
|
||||
TrackListActivity.this, R.string.recording_track_paused_key))) {
|
||||
recordingTrackPaused = PreferencesUtils.getBoolean(TrackListActivity.this,
|
||||
R.string.recording_track_paused_key,
|
||||
PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT);
|
||||
}
|
||||
if (key == null || key.equals(
|
||||
PreferencesUtils.getKey(TrackListActivity.this, R.string.drive_sync_key))) {
|
||||
driveSync = PreferencesUtils.getBoolean(TrackListActivity.this, R.string.drive_sync_key,
|
||||
PreferencesUtils.DRIVE_SYNC_DEFAULT);
|
||||
}
|
||||
if (key != null) {
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
ApiAdapterFactory.getApiAdapter().invalidMenu(TrackListActivity.this);
|
||||
getSupportLoaderManager().restartLoader(0, null, loaderCallbacks);
|
||||
boolean isRecording = recordingTrackId
|
||||
!= PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
|
||||
trackController.update(isRecording, recordingTrackPaused);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Callback when an item is selected in the contextual action mode
|
||||
private final ContextualActionModeCallback
|
||||
contextualActionModeCallback = new ContextualActionModeCallback() {
|
||||
|
||||
@Override
|
||||
public void onPrepare(Menu menu, int[] positions, long[] ids, boolean showSelectAll) {
|
||||
boolean isRecording = recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
|
||||
boolean isSingleSelection = ids.length == 1;
|
||||
boolean isSingleSelectionShareWithMe;
|
||||
if (isSingleSelection) {
|
||||
Track track = myTracksProviderUtils.getTrack(ids[0]);
|
||||
isSingleSelectionShareWithMe = track.isSharedWithMe();
|
||||
} else {
|
||||
isSingleSelectionShareWithMe = false;
|
||||
}
|
||||
|
||||
// Not recording
|
||||
menu.findItem(R.id.list_context_menu_play).setVisible(!isRecording);
|
||||
// Not recording, one item, not sharedWithMe item
|
||||
menu.findItem(R.id.list_context_menu_share)
|
||||
.setVisible(!isRecording && isSingleSelection && !isSingleSelectionShareWithMe);
|
||||
// Always disable
|
||||
menu.findItem(R.id.list_context_menu_show_on_map).setVisible(false);
|
||||
// One item, not sharedWithMe item
|
||||
menu.findItem(R.id.list_context_menu_edit)
|
||||
.setVisible(isSingleSelection && !isSingleSelectionShareWithMe);
|
||||
// delete is always enabled
|
||||
menu.findItem(R.id.list_context_menu_select_all).setVisible(showSelectAll);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onClick(int itemId, int[] positions, long[] ids) {
|
||||
return handleContextItem(itemId, ids);
|
||||
};
|
||||
};
|
||||
|
||||
private final OnClickListener recordListener = new OnClickListener() {
|
||||
public void onClick(View v) {
|
||||
if (recordingTrackId == PreferencesUtils.RECORDING_TRACK_ID_DEFAULT) {
|
||||
// Not recording -> Recording
|
||||
AnalyticsUtils.sendPageViews(TrackListActivity.this, AnalyticsUtils.ACTION_RECORD_TRACK);
|
||||
updateMenuItems(false, true);
|
||||
startRecording();
|
||||
} else {
|
||||
if (recordingTrackPaused) {
|
||||
// Paused -> Resume
|
||||
AnalyticsUtils.sendPageViews(TrackListActivity.this, AnalyticsUtils.ACTION_RESUME_TRACK);
|
||||
updateMenuItems(false, true);
|
||||
TrackRecordingServiceConnectionUtils.resumeTrack(trackRecordingServiceConnection);
|
||||
trackController.update(true, false);
|
||||
} else {
|
||||
// Recording -> Paused
|
||||
AnalyticsUtils.sendPageViews(TrackListActivity.this, AnalyticsUtils.ACTION_PAUSE_TRACK);
|
||||
updateMenuItems(false, true);
|
||||
TrackRecordingServiceConnectionUtils.pauseTrack(trackRecordingServiceConnection);
|
||||
trackController.update(true, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private final OnClickListener stopListener = new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
AnalyticsUtils.sendPageViews(TrackListActivity.this, AnalyticsUtils.ACTION_STOP_RECORDING);
|
||||
updateMenuItems(false, false);
|
||||
TrackRecordingServiceConnectionUtils.stopRecording(
|
||||
TrackListActivity.this, trackRecordingServiceConnection, true);
|
||||
}
|
||||
};
|
||||
|
||||
private final LoaderCallbacks<Cursor> loaderCallbacks = new LoaderCallbacks<Cursor>() {
|
||||
@Override
|
||||
public Loader<Cursor> onCreateLoader(int arg0, Bundle arg1) {
|
||||
return new CursorLoader(TrackListActivity.this, TracksColumns.CONTENT_URI, PROJECTION, null,
|
||||
null,
|
||||
"IFNULL(" + TracksColumns.SHAREDWITHME + ",0) 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);
|
||||
}
|
||||
};
|
||||
|
||||
// The following are set in onCreate
|
||||
private MyTracksProviderUtils myTracksProviderUtils;
|
||||
private SharedPreferences sharedPreferences;
|
||||
private TrackRecordingServiceConnection trackRecordingServiceConnection;
|
||||
private TrackController trackController;
|
||||
private ListView listView;
|
||||
private SectionResourceCursorAdapter sectionResourceCursorAdapter;
|
||||
|
||||
// Preferences
|
||||
private boolean metricUnits = true;
|
||||
private long recordingTrackId = PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
|
||||
private boolean recordingTrackPaused = PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT;
|
||||
private boolean driveSync = PreferencesUtils.DRIVE_SYNC_DEFAULT;
|
||||
|
||||
// Menu items
|
||||
private MenuItem searchMenuItem;
|
||||
private MenuItem startGpsMenuItem;
|
||||
private MenuItem refreshMenuItem;
|
||||
private MenuItem exportAllMenuItem;
|
||||
private MenuItem importAllMenuItem;
|
||||
private MenuItem deleteAllMenuItem;
|
||||
|
||||
private boolean startGps = false; // true to start gps
|
||||
private boolean startNewRecording = false; // true to start a new recording
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
ApiAdapterFactory.getApiAdapter().enableStrictMode();
|
||||
}
|
||||
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 sharedOwnerIndex = cursor.getColumnIndex(TracksColumns.SHAREDOWNER);
|
||||
int totalTimeIndex = cursor.getColumnIndexOrThrow(TracksColumns.TOTALTIME);
|
||||
int totalDistanceIndex = cursor.getColumnIndexOrThrow(TracksColumns.TOTALDISTANCE);
|
||||
int startTimeIndex = cursor.getColumnIndexOrThrow(TracksColumns.STARTTIME);
|
||||
int categoryIndex = cursor.getColumnIndex(TracksColumns.CATEGORY);
|
||||
int descriptionIndex = cursor.getColumnIndex(TracksColumns.DESCRIPTION);
|
||||
|
||||
boolean isRecording = cursor.getLong(idIndex) == recordingTrackId;
|
||||
String icon = cursor.getString(iconIndex);
|
||||
int iconId = TrackIconUtils.getIconDrawable(icon);
|
||||
String name = cursor.getString(nameIndex);
|
||||
String sharedOwner = cursor.getString(sharedOwnerIndex);
|
||||
String totalTime = StringUtils.formatElapsedTime(cursor.getLong(totalTimeIndex));
|
||||
String totalDistance = StringUtils.formatDistance(
|
||||
TrackListActivity.this, cursor.getDouble(totalDistanceIndex), metricUnits);
|
||||
long startTime = cursor.getLong(startTimeIndex);
|
||||
String category = icon != null && !icon.equals("") ? null : cursor.getString(categoryIndex);
|
||||
String description = cursor.getString(descriptionIndex);
|
||||
|
||||
ListItemUtils.setListItem(TrackListActivity.this, view, isRecording, recordingTrackPaused,
|
||||
iconId, R.string.icon_track, name, sharedOwner, totalTime, totalDistance, null,
|
||||
startTime, category, description);
|
||||
}
|
||||
};
|
||||
listView.setAdapter(sectionResourceCursorAdapter);
|
||||
ApiAdapterFactory.getApiAdapter()
|
||||
.configureListViewContextualMenu(this, listView, contextualActionModeCallback);
|
||||
|
||||
getSupportLoaderManager().initLoader(0, null, loaderCallbacks);
|
||||
showStartupDialogs();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStart() {
|
||||
super.onStart();
|
||||
|
||||
// Register shared preferences listener
|
||||
sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
|
||||
|
||||
// Update shared preferences
|
||||
sharedPreferenceChangeListener.onSharedPreferenceChanged(null, null);
|
||||
|
||||
// Update track recording service connection
|
||||
TrackRecordingServiceConnectionUtils.startConnection(this, trackRecordingServiceConnection);
|
||||
|
||||
AnalyticsUtils.sendPageViews(this, AnalyticsUtils.PAGE_TRACK_LIST);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
|
||||
// Update UI
|
||||
ApiAdapterFactory.getApiAdapter().invalidMenu(this);
|
||||
getSupportLoaderManager().restartLoader(0, null, loaderCallbacks);
|
||||
boolean isRecording = recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
|
||||
trackController.onResume(isRecording, recordingTrackPaused);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
|
||||
// Update UI
|
||||
trackController.onPause();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStop() {
|
||||
super.onStop();
|
||||
|
||||
// Unregister shared preferences listener
|
||||
sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener);
|
||||
|
||||
trackRecordingServiceConnection.unbind();
|
||||
|
||||
AnalyticsUtils.dispatch();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
if (requestCode == GOOGLE_PLAY_SERVICES_REQUEST_CODE) {
|
||||
checkGooglePlayServices();
|
||||
} else {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getLayoutResId() {
|
||||
return R.layout.track_list;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean configureActionBarHomeAsUp() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
getMenuInflater().inflate(R.menu.track_list, menu);
|
||||
|
||||
menu.findItem(R.id.track_list_feedback)
|
||||
.setVisible(ApiAdapterFactory.getApiAdapter().isGoogleFeedbackAvailable());
|
||||
|
||||
searchMenuItem = menu.findItem(R.id.track_list_search);
|
||||
ApiAdapterFactory.getApiAdapter().configureSearchWidget(this, searchMenuItem, trackController);
|
||||
|
||||
startGpsMenuItem = menu.findItem(R.id.track_list_start_gps);
|
||||
refreshMenuItem = menu.findItem(R.id.track_list_refresh);
|
||||
exportAllMenuItem = menu.findItem(R.id.track_list_export_all);
|
||||
importAllMenuItem = menu.findItem(R.id.track_list_import_all);
|
||||
deleteAllMenuItem = menu.findItem(R.id.track_list_delete_all);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareOptionsMenu(Menu menu) {
|
||||
boolean isGpsStarted = TrackRecordingServiceConnectionUtils.isRecordingServiceRunning(this);
|
||||
boolean isRecording = recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
|
||||
updateMenuItems(isGpsStarted, isRecording);
|
||||
return super.onPrepareOptionsMenu(menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
Intent intent;
|
||||
switch (item.getItemId()) {
|
||||
case R.id.track_list_search:
|
||||
return ApiAdapterFactory.getApiAdapter().handleSearchMenuSelection(this);
|
||||
case R.id.track_list_start_gps:
|
||||
MyTracksLocationManager myTracksLocationManager = new MyTracksLocationManager(
|
||||
this, Looper.myLooper(), false);
|
||||
if (!myTracksLocationManager.isGpsProviderEnabled()) {
|
||||
intent = GoogleLocationUtils.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 {
|
||||
startGps = !TrackRecordingServiceConnectionUtils.isRecordingServiceRunning(
|
||||
this);
|
||||
|
||||
// Show toast
|
||||
Toast toast = Toast.makeText(
|
||||
this, startGps ? R.string.gps_starting : R.string.gps_stopping, Toast.LENGTH_SHORT);
|
||||
toast.setGravity(Gravity.CENTER, 0, 0);
|
||||
toast.show();
|
||||
|
||||
// Invoke trackRecordingService
|
||||
if (startGps) {
|
||||
trackRecordingServiceConnection.startAndBind();
|
||||
bindChangedCallback.run();
|
||||
} else {
|
||||
ITrackRecordingService trackRecordingService = trackRecordingServiceConnection
|
||||
.getServiceIfBound();
|
||||
if (trackRecordingService != null) {
|
||||
try {
|
||||
trackRecordingService.stopGps();
|
||||
} catch (RemoteException e) {
|
||||
Log.e(TAG, "Unable to stop gps.", e);
|
||||
}
|
||||
}
|
||||
trackRecordingServiceConnection.unbindAndStop();
|
||||
}
|
||||
|
||||
// Update menu after starting or stopping gps
|
||||
ApiAdapterFactory.getApiAdapter().invalidMenu(this);
|
||||
}
|
||||
myTracksLocationManager.close();
|
||||
return true;
|
||||
case R.id.track_list_refresh:
|
||||
if (driveSync) {
|
||||
SyncUtils.syncNow(this);
|
||||
} else {
|
||||
PreferencesUtils.setString(
|
||||
this, R.string.google_account_key, PreferencesUtils.GOOGLE_ACCOUNT_DEFAULT);
|
||||
SendRequest sendRequest = new SendRequest(-1L);
|
||||
sendRequest.setSendDrive(true);
|
||||
sendRequest.setDriveSync(true);
|
||||
sendRequest.setDriveSyncConfirm(true);
|
||||
sendToGoogle(sendRequest);
|
||||
}
|
||||
return true;
|
||||
case R.id.track_list_aggregated_statistics:
|
||||
intent = IntentUtils.newIntent(this, AggregatedStatsActivity.class);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
case R.id.track_list_export_all:
|
||||
FileTypeDialogFragment.newInstance(R.id.track_list_export_all,
|
||||
R.string.export_all_title, R.string.export_all_option, 4)
|
||||
.show(getSupportFragmentManager(), FileTypeDialogFragment.FILE_TYPE_DIALOG_TAG);
|
||||
return true;
|
||||
case R.id.track_list_import_all:
|
||||
FileTypeDialogFragment.newInstance(R.id.track_list_import_all,
|
||||
R.string.import_selection_title, R.string.import_selection_option, 2)
|
||||
.show(getSupportFragmentManager(), FileTypeDialogFragment.FILE_TYPE_DIALOG_TAG);
|
||||
return true;
|
||||
case R.id.track_list_delete_all:
|
||||
ConfirmDeleteDialogFragment.newInstance(new long[] {-1L})
|
||||
.show(getSupportFragmentManager(), ConfirmDeleteDialogFragment.CONFIRM_DELETE_DIALOG_TAG);
|
||||
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 }, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onContextItemSelected(MenuItem item) {
|
||||
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
|
||||
if (handleContextItem(item.getItemId(), new long[] { info.id })) {
|
||||
return true;
|
||||
}
|
||||
return super.onContextItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyUp(int keyCode, KeyEvent event) {
|
||||
if (keyCode == KeyEvent.KEYCODE_SEARCH && searchMenuItem != null) {
|
||||
if (ApiAdapterFactory.getApiAdapter().handleSearchKey(searchMenuItem)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return super.onKeyUp(keyCode, event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onSearchRequested() {
|
||||
// Hide trackController when search dialog is shown
|
||||
trackController.hide();
|
||||
return super.onSearchRequested();
|
||||
};
|
||||
|
||||
@Override
|
||||
protected TrackRecordingServiceConnection getTrackRecordingServiceConnection() {
|
||||
return trackRecordingServiceConnection;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDeleted() {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFileTypeDone(int menuId, TrackFileFormat trackFileFormat) {
|
||||
Intent intent;
|
||||
switch (menuId) {
|
||||
case R.id.track_list_export_all:
|
||||
AnalyticsUtils.sendPageViews(
|
||||
this, AnalyticsUtils.ACTION_EXPORT_ALL_PREFIX + trackFileFormat.getExtension());
|
||||
intent = IntentUtils.newIntent(this, SaveActivity.class)
|
||||
.putExtra(SaveActivity.EXTRA_TRACK_IDS, new long[] {-1L})
|
||||
.putExtra(SaveActivity.EXTRA_TRACK_FILE_FORMAT, (Parcelable) trackFileFormat);
|
||||
startActivity(intent);
|
||||
break;
|
||||
case R.id.track_list_import_all:
|
||||
AnalyticsUtils.sendPageViews(
|
||||
this, AnalyticsUtils.ACTION_IMPORT_ALL_PREFIX + trackFileFormat.getExtension());
|
||||
intent = IntentUtils.newIntent(this, ImportActivity.class)
|
||||
.putExtra(ImportActivity.EXTRA_IMPORT_ALL, true)
|
||||
.putExtra(ImportActivity.EXTRA_TRACK_FILE_FORMAT, (Parcelable) trackFileFormat);
|
||||
startActivity(intent);
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows start up dialogs.
|
||||
*/
|
||||
public void showStartupDialogs() {
|
||||
if (!EulaUtils.hasAcceptEula(this)) {
|
||||
Fragment fragment = getSupportFragmentManager()
|
||||
.findFragmentByTag(EulaDialogFragment.EULA_DIALOG_TAG);
|
||||
if (fragment == null) {
|
||||
EulaDialogFragment.newInstance(false)
|
||||
.show(getSupportFragmentManager(), EulaDialogFragment.EULA_DIALOG_TAG);
|
||||
}
|
||||
} else {
|
||||
if (!EulaUtils.hasDefaultUnits(this)) {
|
||||
String statsUnits = getString(
|
||||
Locale.US.equals(Locale.getDefault()) ? R.string.stats_units_imperial
|
||||
: R.string.stats_units_metric);
|
||||
PreferencesUtils.setString(this, R.string.stats_units_key, statsUnits);
|
||||
EulaUtils.setDefaultUnits(this);
|
||||
}
|
||||
checkGooglePlayServices();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEulaDone() {
|
||||
if (EulaUtils.hasAcceptEula(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.hasShowEnableSync(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.setDriveSync(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.ic_menu_stop_gps : R.drawable.ic_menu_start_gps);
|
||||
}
|
||||
}
|
||||
if (refreshMenuItem != null) {
|
||||
refreshMenuItem.setTitle(driveSync ? R.string.menu_refresh : R.string.menu_sync_drive);
|
||||
}
|
||||
if (exportAllMenuItem != null) {
|
||||
exportAllMenuItem.setVisible(!isRecording);
|
||||
}
|
||||
if (importAllMenuItem != null) {
|
||||
importAllMenuItem.setVisible(!isRecording);
|
||||
}
|
||||
if (deleteAllMenuItem != null) {
|
||||
deleteAllMenuItem.setVisible(!isRecording);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a new recording.
|
||||
*/
|
||||
private void startRecording() {
|
||||
startNewRecording = true;
|
||||
trackRecordingServiceConnection.startAndBind();
|
||||
|
||||
/*
|
||||
* If the binding has happened, then invoke the callback to start a new
|
||||
* recording. If the binding hasn't happened, then invoking the callback
|
||||
* will have no effect. But when the binding occurs, the callback will get
|
||||
* invoked.
|
||||
*/
|
||||
bindChangedCallback.run();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a context item selection.
|
||||
*
|
||||
* @param itemId the menu item id
|
||||
* @param trackIds the track ids
|
||||
* @return true if handled.
|
||||
*/
|
||||
private boolean handleContextItem(int itemId, long[] trackIds) {
|
||||
switch (itemId) {
|
||||
case R.id.list_context_menu_play:
|
||||
confirmPlay(trackIds);
|
||||
return true;
|
||||
case R.id.list_context_menu_share:
|
||||
shareTrack(trackIds[0]);
|
||||
return true;
|
||||
case R.id.list_context_menu_edit:
|
||||
Intent intent = IntentUtils.newIntent(this, TrackEditActivity.class)
|
||||
.putExtra(TrackEditActivity.EXTRA_TRACK_ID, trackIds[0]);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
case R.id.list_context_menu_delete:
|
||||
if (trackIds.length > 1 && trackIds.length == listView.getCount()) {
|
||||
trackIds = new long[] {-1L};
|
||||
}
|
||||
ConfirmDeleteDialogFragment.newInstance(trackIds)
|
||||
.show(getSupportFragmentManager(), ConfirmDeleteDialogFragment.CONFIRM_DELETE_DIALOG_TAG);
|
||||
return true;
|
||||
case R.id.list_context_menu_select_all:
|
||||
int size = listView.getCount();
|
||||
for (int i = 0; i < size; i++) {
|
||||
listView.setItemChecked(i, true);
|
||||
}
|
||||
return false;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* Copyright 2011 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
|
||||
package com.google.android.apps.mytracks.content;
|
||||
|
||||
import com.google.android.apps.mytracks.Constants;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
|
||||
import android.database.ContentObserver;
|
||||
import android.net.Uri;
|
||||
|
||||
/**
|
||||
* Data source on the phone.
|
||||
*
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
public class DataSource {
|
||||
|
||||
private final ContentResolver contentResolver;
|
||||
private final SharedPreferences sharedPreferences;
|
||||
|
||||
public DataSource(Context context) {
|
||||
contentResolver = context.getContentResolver();
|
||||
sharedPreferences = context.getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a content observer.
|
||||
*
|
||||
* @param uri the uri
|
||||
* @param observer the observer
|
||||
*/
|
||||
public void registerContentObserver(Uri uri, ContentObserver observer) {
|
||||
contentResolver.registerContentObserver(uri, false, observer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters a content observer.
|
||||
*
|
||||
* @param observer the observer
|
||||
*/
|
||||
public void unregisterContentObserver(ContentObserver observer) {
|
||||
contentResolver.unregisterContentObserver(observer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a shared preference change listener.
|
||||
*
|
||||
* @param listener the listener
|
||||
*/
|
||||
public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
|
||||
sharedPreferences.registerOnSharedPreferenceChangeListener(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters a shared preference change listener.
|
||||
*
|
||||
* @param listener the listener
|
||||
*/
|
||||
public void unregisterOnSharedPreferenceChangeListener(
|
||||
OnSharedPreferenceChangeListener listener) {
|
||||
sharedPreferences.unregisterOnSharedPreferenceChangeListener(listener);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright 2011 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
|
||||
package com.google.android.apps.mytracks.content;
|
||||
|
||||
|
||||
/**
|
||||
* Listener to be invoked when {@link DataSource} changes.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public interface DataSourceListener {
|
||||
|
||||
/**
|
||||
* Notifies when the tracks table is updated.
|
||||
*/
|
||||
public void notifyTracksTableUpdated();
|
||||
|
||||
/**
|
||||
* Notifies when the waypoints table is updated.
|
||||
*/
|
||||
public void notifyWaypointsTableUpdated();
|
||||
|
||||
/**
|
||||
* Notifies when the track points table is updated.
|
||||
*/
|
||||
public void notifyTrackPointsTableUpdated();
|
||||
|
||||
/**
|
||||
* Notifies when a preference changes.
|
||||
*
|
||||
* @param key the preference key
|
||||
*/
|
||||
public void notifyPreferenceChanged(String key);
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
/*
|
||||
* Copyright 2011 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
|
||||
package com.google.android.apps.mytracks.content;
|
||||
|
||||
import static com.google.android.apps.mytracks.Constants.TAG;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
|
||||
import android.database.ContentObserver;
|
||||
import android.os.Handler;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.EnumSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Data source manager. Creates observers/listeners and manages their
|
||||
* registration with {@link DataSource}. The observers/listeners calls
|
||||
* {@link DataSourceListener} when data changes.
|
||||
*
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
public class DataSourceManager {
|
||||
|
||||
/**
|
||||
* Observer when the tracks table is updated.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
private class TracksTableObserver extends ContentObserver {
|
||||
|
||||
public TracksTableObserver() {
|
||||
super(handler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChange(boolean selfChange) {
|
||||
dataSourceListener.notifyTracksTableUpdated();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Observer when the waypoints table is updated.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
private class WaypointsTableObserver extends ContentObserver {
|
||||
|
||||
public WaypointsTableObserver() {
|
||||
super(handler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChange(boolean selfChange) {
|
||||
dataSourceListener.notifyWaypointsTableUpdated();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Observer when the track points table is updated.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
private class TrackPointsTableObserver extends ContentObserver {
|
||||
|
||||
public TrackPointsTableObserver() {
|
||||
super(handler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChange(boolean selfChange) {
|
||||
dataSourceListener.notifyTrackPointsTableUpdated();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Listener for preference changes.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
private class PreferenceListener implements OnSharedPreferenceChangeListener {
|
||||
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
|
||||
dataSourceListener.notifyPreferenceChanged(key);
|
||||
}
|
||||
}
|
||||
|
||||
private final DataSource dataSource;
|
||||
private final DataSourceListener dataSourceListener;
|
||||
|
||||
// Registered listeners
|
||||
private final Set<TrackDataType> registeredListeners = EnumSet.noneOf(TrackDataType.class);
|
||||
|
||||
private final Handler handler;
|
||||
private final TracksTableObserver tracksTableObserver;
|
||||
private final WaypointsTableObserver waypointsTableObserver;
|
||||
private final TrackPointsTableObserver trackPointsTableObserver;
|
||||
private final PreferenceListener preferenceListener;
|
||||
|
||||
public DataSourceManager(DataSource dataSource, DataSourceListener dataSourceListener) {
|
||||
this.dataSource = dataSource;
|
||||
this.dataSourceListener = dataSourceListener;
|
||||
|
||||
handler = new Handler();
|
||||
tracksTableObserver = new TracksTableObserver();
|
||||
waypointsTableObserver = new WaypointsTableObserver();
|
||||
trackPointsTableObserver = new TrackPointsTableObserver();
|
||||
preferenceListener = new PreferenceListener();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates listeners with data source.
|
||||
*
|
||||
* @param listeners the listeners
|
||||
*/
|
||||
public void updateListeners(EnumSet<TrackDataType> listeners) {
|
||||
EnumSet<TrackDataType> neededListeners = EnumSet.copyOf(listeners);
|
||||
|
||||
/*
|
||||
* Map SAMPLED_OUT_POINT_UPDATES to POINT_UPDATES since they correspond to
|
||||
* the same internal listener
|
||||
*/
|
||||
if (neededListeners.contains(TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE)) {
|
||||
neededListeners.remove(TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE);
|
||||
neededListeners.add(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE);
|
||||
}
|
||||
|
||||
Log.d(TAG, "Updating listeners " + neededListeners);
|
||||
|
||||
// Unnecessary = registered - needed
|
||||
Set<TrackDataType> unnecessaryListeners = EnumSet.copyOf(registeredListeners);
|
||||
unnecessaryListeners.removeAll(neededListeners);
|
||||
|
||||
// Missing = needed - registered
|
||||
Set<TrackDataType> missingListeners = EnumSet.copyOf(neededListeners);
|
||||
missingListeners.removeAll(registeredListeners);
|
||||
|
||||
// Remove unnecessary listeners
|
||||
for (TrackDataType trackDataType : unnecessaryListeners) {
|
||||
unregisterListener(trackDataType);
|
||||
}
|
||||
|
||||
// Add missing listeners
|
||||
for (TrackDataType trackDataType : missingListeners) {
|
||||
registerListener(trackDataType);
|
||||
}
|
||||
|
||||
// Update registered listeners
|
||||
registeredListeners.clear();
|
||||
registeredListeners.addAll(neededListeners);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a listener with data source.
|
||||
*
|
||||
* @param trackDataType the listener data type
|
||||
*/
|
||||
private void registerListener(TrackDataType trackDataType) {
|
||||
switch (trackDataType) {
|
||||
case TRACKS_TABLE:
|
||||
dataSource.registerContentObserver(TracksColumns.CONTENT_URI, tracksTableObserver);
|
||||
break;
|
||||
case WAYPOINTS_TABLE:
|
||||
dataSource.registerContentObserver(WaypointsColumns.CONTENT_URI, waypointsTableObserver);
|
||||
break;
|
||||
case SAMPLED_IN_TRACK_POINTS_TABLE:
|
||||
dataSource.registerContentObserver(
|
||||
TrackPointsColumns.CONTENT_URI, trackPointsTableObserver);
|
||||
break;
|
||||
case SAMPLED_OUT_TRACK_POINTS_TABLE:
|
||||
// Do nothing. SAMPLED_OUT_POINT_UPDATES is mapped to POINT_UPDATES.
|
||||
break;
|
||||
case PREFERENCE:
|
||||
dataSource.registerOnSharedPreferenceChangeListener(preferenceListener);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters a listener with data source.
|
||||
*
|
||||
* @param trackDataType listener data type
|
||||
*/
|
||||
private void unregisterListener(TrackDataType trackDataType) {
|
||||
switch (trackDataType) {
|
||||
case TRACKS_TABLE:
|
||||
dataSource.unregisterContentObserver(tracksTableObserver);
|
||||
break;
|
||||
case WAYPOINTS_TABLE:
|
||||
dataSource.unregisterContentObserver(waypointsTableObserver);
|
||||
break;
|
||||
case SAMPLED_IN_TRACK_POINTS_TABLE:
|
||||
dataSource.unregisterContentObserver(trackPointsTableObserver);
|
||||
break;
|
||||
case SAMPLED_OUT_TRACK_POINTS_TABLE:
|
||||
// Do nothing. SAMPLED_OUT_POINT_UPDATES is mapped to POINT_UPDATES.
|
||||
break;
|
||||
case PREFERENCE:
|
||||
dataSource.unregisterOnSharedPreferenceChangeListener(preferenceListener);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters all listeners with data source.
|
||||
*/
|
||||
public void unregisterAllListeners() {
|
||||
for (TrackDataType trackDataType : TrackDataType.values()) {
|
||||
unregisterListener(trackDataType);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
/*
|
||||
* Copyright 2012 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
|
||||
package com.google.android.apps.mytracks.content;
|
||||
|
||||
import com.google.android.apps.mytracks.stats.TripStatistics;
|
||||
import com.google.android.apps.mytracks.util.ChartURLGenerator;
|
||||
import com.google.android.apps.mytracks.util.StringUtils;
|
||||
import com.google.android.apps.mytracks.util.UnitConversions;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import java.util.Vector;
|
||||
|
||||
/**
|
||||
* An implementation of {@link DescriptionGenerator} for My Tracks.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class DescriptionGeneratorImpl implements DescriptionGenerator {
|
||||
|
||||
private static final String HTML_LINE_BREAK = "<br>";
|
||||
private static final String HTML_PARAGRAPH_SEPARATOR = "<p>";
|
||||
private static final String TEXT_LINE_BREAK = "\n";
|
||||
private static final String TEXT_PARAGRAPH_SEPARATOR = "\n\n";
|
||||
|
||||
private Context context;
|
||||
|
||||
public DescriptionGeneratorImpl(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String generateTrackDescription(
|
||||
Track track, Vector<Double> distances, Vector<Double> elevations, boolean html) {
|
||||
String paragraphSeparator = html ? HTML_PARAGRAPH_SEPARATOR : TEXT_PARAGRAPH_SEPARATOR;
|
||||
String lineBreak = html ? HTML_LINE_BREAK : TEXT_LINE_BREAK;
|
||||
StringBuilder builder = new StringBuilder();
|
||||
|
||||
// Created by
|
||||
String beginAnchor = html
|
||||
? "<a href='http://" + context.getString(R.string.my_tracks_web_url) + "'>"
|
||||
: "";
|
||||
String endAnchor = html ? "</a>" : "";
|
||||
builder.append(context.getString(R.string.send_google_by_my_tracks, beginAnchor, endAnchor));
|
||||
builder.append(paragraphSeparator);
|
||||
|
||||
writeString(track.getName(), builder, R.string.generic_name_line, lineBreak);
|
||||
writeString(track.getCategory(), builder, R.string.description_activity_type, lineBreak);
|
||||
writeString(track.getDescription(), builder, R.string.generic_description_line, lineBreak);
|
||||
builder.append(generateTripStatisticsDescription(track.getTripStatistics(), html));
|
||||
|
||||
// Elevation chart
|
||||
if (html && distances != null && elevations != null) {
|
||||
builder.append("<img border=\"0\" src=\""
|
||||
+ ChartURLGenerator.getChartUrl(distances, elevations, track, context) + "\"/>");
|
||||
builder.append(HTML_LINE_BREAK);
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String generateWaypointDescription(TripStatistics tripStatistics) {
|
||||
return generateTripStatisticsDescription(tripStatistics, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a string to a string builder.
|
||||
*
|
||||
* @param text the string
|
||||
* @param builder the string builder
|
||||
* @param resId the resource id containing one string placeholder
|
||||
* @param lineBreak the line break
|
||||
*/
|
||||
private void writeString(String text, StringBuilder builder, int resId, String lineBreak) {
|
||||
if (text == null || text.length() == 0) {
|
||||
text = context.getString(R.string.value_unknown);
|
||||
}
|
||||
builder.append(context.getString(resId, text));
|
||||
builder.append(lineBreak);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a description for a {@link TripStatistics}.
|
||||
*
|
||||
* @param stats the trip statistics
|
||||
* @param html true to use "<br>" for line break instead of "\n"
|
||||
*/
|
||||
private String generateTripStatisticsDescription(TripStatistics stats, boolean html) {
|
||||
String lineBreak = html ? HTML_LINE_BREAK : TEXT_LINE_BREAK;
|
||||
StringBuilder builder = new StringBuilder();
|
||||
|
||||
// Total distance
|
||||
writeDistance(
|
||||
stats.getTotalDistance(), builder, R.string.description_total_distance, lineBreak);
|
||||
|
||||
// Total time
|
||||
writeTime(stats.getTotalTime(), builder, R.string.description_total_time, lineBreak);
|
||||
|
||||
// Moving time
|
||||
writeTime(stats.getMovingTime(), builder, R.string.description_moving_time, lineBreak);
|
||||
|
||||
// Average speed
|
||||
writeSpeed(stats.getAverageSpeed(), builder, R.string.description_average_speed, lineBreak);
|
||||
|
||||
// Average moving speed
|
||||
writeSpeed(stats.getAverageMovingSpeed(), builder, R.string.description_average_moving_speed,
|
||||
lineBreak);
|
||||
|
||||
// Max speed
|
||||
writeSpeed(stats.getMaxSpeed(), builder, R.string.description_max_speed, lineBreak);
|
||||
|
||||
// Average pace
|
||||
writePace(
|
||||
stats.getAverageSpeed(), builder, R.string.description_average_pace_in_minute, lineBreak);
|
||||
|
||||
// Average moving pace
|
||||
writePace(stats.getAverageMovingSpeed(), builder,
|
||||
R.string.description_average_moving_pace_in_minute, lineBreak);
|
||||
|
||||
// Fastest pace
|
||||
writePace(stats.getMaxSpeed(), builder, R.string.description_fastest_pace_in_minute, lineBreak);
|
||||
|
||||
// Max elevation
|
||||
writeElevation(stats.getMaxElevation(), builder, R.string.description_max_elevation, lineBreak);
|
||||
|
||||
// Min elevation
|
||||
writeElevation(stats.getMinElevation(), builder, R.string.description_min_elevation, lineBreak);
|
||||
|
||||
// Elevation gain
|
||||
writeElevation(
|
||||
stats.getTotalElevationGain(), builder, R.string.description_elevation_gain, lineBreak);
|
||||
|
||||
// Max grade
|
||||
writeGrade(stats.getMaxGrade(), builder, R.string.description_max_grade, lineBreak);
|
||||
|
||||
// Min grade
|
||||
writeGrade(stats.getMinGrade(), builder, R.string.description_min_grade, lineBreak);
|
||||
|
||||
// Recorded time
|
||||
builder.append(
|
||||
context.getString(R.string.description_recorded_time,
|
||||
StringUtils.formatDateTime(context, stats.getStartTime())));
|
||||
builder.append(lineBreak);
|
||||
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes distance.
|
||||
*
|
||||
* @param distance distance in meters
|
||||
* @param builder StringBuilder to append distance
|
||||
* @param resId resource id of distance string
|
||||
* @param lineBreak line break string
|
||||
*/
|
||||
@VisibleForTesting
|
||||
void writeDistance(double distance, StringBuilder builder, int resId, String lineBreak) {
|
||||
double distanceInKm = distance * UnitConversions.M_TO_KM;
|
||||
double distanceInMi = distanceInKm * UnitConversions.KM_TO_MI;
|
||||
builder.append(context.getString(resId, distanceInKm, distanceInMi));
|
||||
builder.append(lineBreak);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes time.
|
||||
*
|
||||
* @param time time in milliseconds.
|
||||
* @param builder StringBuilder to append time
|
||||
* @param resId resource id of time string
|
||||
* @param lineBreak line break string
|
||||
*/
|
||||
@VisibleForTesting
|
||||
void writeTime(long time, StringBuilder builder, int resId, String lineBreak) {
|
||||
builder.append(context.getString(resId, StringUtils.formatElapsedTime(time)));
|
||||
builder.append(lineBreak);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes speed.
|
||||
*
|
||||
* @param speed speed in meters per second
|
||||
* @param builder StringBuilder to append speed
|
||||
* @param resId resource id of speed string
|
||||
* @param lineBreak line break string
|
||||
*/
|
||||
@VisibleForTesting
|
||||
void writeSpeed(
|
||||
double speed, StringBuilder builder, int resId, String lineBreak) {
|
||||
double speedInKmHr = speed * UnitConversions.MS_TO_KMH;
|
||||
double speedInMiHr = speedInKmHr * UnitConversions.KM_TO_MI;
|
||||
builder.append(context.getString(resId, speedInKmHr, speedInMiHr));
|
||||
builder.append(lineBreak);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes pace.
|
||||
*
|
||||
* @param speed speed in meters per second
|
||||
* @param builder StringBuilder to append pace
|
||||
* @param resId resource id of pace string
|
||||
* @param lineBreak line break string
|
||||
*/
|
||||
@VisibleForTesting
|
||||
void writePace(double speed, StringBuilder builder, int resId, String lineBreak) {
|
||||
String[] paceInMetrics = StringUtils.getSpeedParts(context, speed, true, false);
|
||||
String[] paceInImperial = StringUtils.getSpeedParts(context, speed, false, false);
|
||||
|
||||
if (paceInMetrics[0] == null) {
|
||||
paceInMetrics[0] = context.getString(R.string.value_unknown);
|
||||
}
|
||||
|
||||
if (paceInImperial[0] == null) {
|
||||
paceInImperial[0] = context.getString(R.string.value_unknown);
|
||||
}
|
||||
|
||||
builder.append(context.getString(resId, paceInMetrics[0], paceInImperial[0]));
|
||||
builder.append(lineBreak);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes elevation.
|
||||
*
|
||||
* @param elevation elevation in meters
|
||||
* @param builder StringBuilder to append elevation
|
||||
* @param resId resource id of elevation string
|
||||
* @param lineBreak line break string
|
||||
*/
|
||||
@VisibleForTesting
|
||||
void writeElevation(
|
||||
double elevation, StringBuilder builder, int resId, String lineBreak) {
|
||||
long elevationInM = Math.round(elevation);
|
||||
long elevationInFt = Math.round(elevation * UnitConversions.M_TO_FT);
|
||||
builder.append(context.getString(resId, elevationInM, elevationInFt));
|
||||
builder.append(lineBreak);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes grade.
|
||||
*
|
||||
* @param grade grade in fraction
|
||||
* @param builder StringBuilder to append grade
|
||||
* @param resId resource id grade string
|
||||
* @param lineBreak line break string
|
||||
*/
|
||||
@VisibleForTesting
|
||||
void writeGrade(double grade, StringBuilder builder, int resId, String lineBreak) {
|
||||
long gradeInPercent = Double.isNaN(grade) || Double.isInfinite(grade) ? 0L
|
||||
: Math.round(grade * 100);
|
||||
builder.append(context.getString(resId, gradeInPercent));
|
||||
builder.append(lineBreak);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,547 @@
|
||||
/*
|
||||
* 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 = 22;
|
||||
|
||||
@VisibleForTesting
|
||||
static final String DATABASE_NAME = "mytracks.db";
|
||||
|
||||
/**
|
||||
* Database helper for creating and upgrading the database.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
static class DatabaseHelper extends SQLiteOpenHelper {
|
||||
|
||||
public DatabaseHelper(Context context) {
|
||||
this(context, DATABASE_NAME);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public DatabaseHelper(Context context, String databaseName) {
|
||||
super(context, databaseName, null, DATABASE_VERSION);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(SQLiteDatabase db) {
|
||||
db.execSQL(TrackPointsColumns.CREATE_TABLE);
|
||||
db.execSQL(TracksColumns.CREATE_TABLE);
|
||||
db.execSQL(WaypointsColumns.CREATE_TABLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
|
||||
Log.w(TAG, "Upgrading database from version " + oldVersion + " to " + newVersion);
|
||||
if (oldVersion < 17) {
|
||||
Log.w(TAG, "Deleting all old data.");
|
||||
db.execSQL("DROP TABLE IF EXISTS " + TrackPointsColumns.TABLE_NAME);
|
||||
db.execSQL("DROP TABLE IF EXISTS " + TracksColumns.TABLE_NAME);
|
||||
db.execSQL("DROP TABLE IF EXISTS " + WaypointsColumns.TABLE_NAME);
|
||||
onCreate(db);
|
||||
} else {
|
||||
// Incremental upgrades. One if statement per DB version.
|
||||
|
||||
// Add track 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");
|
||||
}
|
||||
|
||||
if (oldVersion <= 21) {
|
||||
Log.w(TAG, "Upgrade DB: Adding photo url column.");
|
||||
db.execSQL("ALTER TABLE " + WaypointsColumns.TABLE_NAME + " ADD " + WaypointsColumns.PHOTOURL
|
||||
+ " STRING");
|
||||
Log.w(TAG, "Upgrade DB: Adding track calorie column.");
|
||||
db.execSQL("ALTER TABLE " + TracksColumns.TABLE_NAME + " ADD " + TracksColumns.CALORIE
|
||||
+ " INTEGER");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,614 @@
|
||||
/*
|
||||
* 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.util.LocationUtils;
|
||||
import com.google.android.apps.mytracks.util.PreferencesUtils;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.location.Location;
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.EnumSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Track data hub. Receives data from {@link DataSource} and distributes it to
|
||||
* {@link TrackDataListener} after some processing.
|
||||
*
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
public class TrackDataHub implements DataSourceListener {
|
||||
|
||||
private static final String TAG = TrackDataHub.class.getSimpleName();
|
||||
|
||||
private final Context context;
|
||||
private final TrackDataManager trackDataManager;
|
||||
private final MyTracksProviderUtils myTracksProviderUtils;
|
||||
private final int targetNumPoints;
|
||||
|
||||
private boolean started;
|
||||
private HandlerThread handlerThread;
|
||||
private Handler handler;
|
||||
private DataSource dataSource;
|
||||
private DataSourceManager dataSourceManager;
|
||||
|
||||
// Preference values
|
||||
private long selectedTrackId;
|
||||
private long recordingTrackId;
|
||||
private boolean recordingTrackPaused;
|
||||
private boolean metricUnits;
|
||||
private boolean reportSpeed;
|
||||
private int recordingGpsAccuracy;
|
||||
private int recordingDistanceInterval;
|
||||
|
||||
// Track points sampling state
|
||||
private int numLoadedPoints;
|
||||
private long firstSeenLocationId;
|
||||
private long lastSeenLocationId;
|
||||
|
||||
/**
|
||||
* Creates a new instance.
|
||||
*/
|
||||
public synchronized static TrackDataHub newInstance(Context context) {
|
||||
return new TrackDataHub(context, new TrackDataManager(), MyTracksProviderUtils.Factory.get(
|
||||
context), TARGET_DISPLAYED_TRACK_POINTS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param context the context
|
||||
* @param trackDataManager the track data manager
|
||||
* @param myTracksProviderUtils the my tracks provider utils
|
||||
* @param targetNumPoints the target number of points
|
||||
*/
|
||||
@VisibleForTesting
|
||||
TrackDataHub(Context context, TrackDataManager trackDataManager,
|
||||
MyTracksProviderUtils myTracksProviderUtils, int targetNumPoints) {
|
||||
this.context = context;
|
||||
this.trackDataManager = trackDataManager;
|
||||
this.myTracksProviderUtils = myTracksProviderUtils;
|
||||
this.targetNumPoints = targetNumPoints;
|
||||
resetSamplingState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts.
|
||||
*/
|
||||
public void start() {
|
||||
if (started) {
|
||||
Log.i(TAG, "TrackDataHub already started, ignoring start.");
|
||||
return;
|
||||
}
|
||||
started = true;
|
||||
handlerThread = new HandlerThread("TrackDataHubHandlerThread");
|
||||
handlerThread.start();
|
||||
handler = new Handler(handlerThread.getLooper());
|
||||
dataSource = newDataSource();
|
||||
dataSourceManager = new DataSourceManager(dataSource, this);
|
||||
|
||||
notifyPreferenceChanged(null);
|
||||
runInHanderThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (dataSourceManager != null) {
|
||||
dataSourceManager.updateListeners(trackDataManager.getRegisteredTrackDataTypes());
|
||||
loadDataForAll();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops.
|
||||
*/
|
||||
public void stop() {
|
||||
if (!started) {
|
||||
Log.i(TAG, "TrackDataHub not started, ignoring stop.");
|
||||
return;
|
||||
}
|
||||
started = false;
|
||||
|
||||
dataSourceManager.unregisterAllListeners();
|
||||
if (handlerThread != null) {
|
||||
handlerThread.getLooper().quit();
|
||||
handlerThread = null;
|
||||
}
|
||||
handler = null;
|
||||
dataSource = null;
|
||||
dataSourceManager = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a track.
|
||||
*
|
||||
* @param trackId the track id
|
||||
*/
|
||||
public void loadTrack(final long trackId) {
|
||||
runInHanderThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (trackId == selectedTrackId) {
|
||||
Log.i(TAG, "Not reloading track " + trackId);
|
||||
return;
|
||||
}
|
||||
selectedTrackId = trackId;
|
||||
loadDataForAll();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a {@link TrackDataListener}.
|
||||
*
|
||||
* @param trackDataListener the track data listener
|
||||
* @param trackDataTypes the track data types
|
||||
*/
|
||||
public void registerTrackDataListener(
|
||||
final TrackDataListener trackDataListener, final EnumSet<TrackDataType> trackDataTypes) {
|
||||
runInHanderThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (dataSourceManager != null) {
|
||||
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() {
|
||||
if (dataSourceManager != null) {
|
||||
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.recording_track_id_key))) {
|
||||
recordingTrackId = PreferencesUtils.getLong(context, R.string.recording_track_id_key);
|
||||
}
|
||||
if (key == null
|
||||
|| key.equals(PreferencesUtils.getKey(context, R.string.recording_track_paused_key))) {
|
||||
recordingTrackPaused = PreferencesUtils.getBoolean(
|
||||
context, R.string.recording_track_paused_key,
|
||||
PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT);
|
||||
}
|
||||
if (key == null
|
||||
|| key.equals(PreferencesUtils.getKey(context, R.string.stats_units_key))) {
|
||||
metricUnits = PreferencesUtils.isMetricUnits(context);
|
||||
if (key != null) {
|
||||
for (TrackDataListener trackDataListener :
|
||||
trackDataManager.getListeners(TrackDataType.PREFERENCE)) {
|
||||
if (trackDataListener.onMetricUnitsChanged(metricUnits)) {
|
||||
loadDataForListener(trackDataListener);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (key == null
|
||||
|| key.equals(PreferencesUtils.getKey(context, R.string.stats_rate_key))) {
|
||||
reportSpeed = PreferencesUtils.isReportSpeed(context);
|
||||
if (key != null) {
|
||||
for (TrackDataListener trackDataListener :
|
||||
trackDataManager.getListeners(TrackDataType.PREFERENCE)) {
|
||||
if (trackDataListener.onReportSpeedChanged(reportSpeed)) {
|
||||
loadDataForListener(trackDataListener);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (key == null
|
||||
|| key.equals(PreferencesUtils.getKey(context, R.string.recording_gps_accuracy_key))) {
|
||||
recordingGpsAccuracy = PreferencesUtils.getInt(context, R.string.recording_gps_accuracy_key,
|
||||
PreferencesUtils.RECORDING_GPS_ACCURACY_DEFAULT);
|
||||
if (key != null) {
|
||||
for (TrackDataListener trackDataListener :
|
||||
trackDataManager.getListeners(TrackDataType.PREFERENCE)) {
|
||||
if (trackDataListener.onRecordingGpsAccuracy(recordingGpsAccuracy)) {
|
||||
loadDataForListener(trackDataListener);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (key == null || key.equals(
|
||||
PreferencesUtils.getKey(context, R.string.recording_distance_interval_key))) {
|
||||
recordingDistanceInterval = PreferencesUtils.getInt(
|
||||
context, R.string.recording_distance_interval_key,
|
||||
PreferencesUtils.RECORDING_DISTANCE_INTERVAL_DEFAULT);
|
||||
if (key != null) {
|
||||
for (TrackDataListener trackDataListener :
|
||||
trackDataManager.getListeners(TrackDataType.PREFERENCE)) {
|
||||
if (trackDataListener.onRecordingDistanceIntervalChanged(recordingDistanceInterval)) {
|
||||
loadDataForListener(trackDataListener);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads data for all listeners. To be run in the {@link #handler} thread.
|
||||
*/
|
||||
private void loadDataForAll() {
|
||||
resetSamplingState();
|
||||
if (trackDataManager.getNumberOfListeners() == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (TrackDataListener trackDataListener :
|
||||
trackDataManager.getListeners(TrackDataType.PREFERENCE)) {
|
||||
trackDataListener.onMetricUnitsChanged(metricUnits);
|
||||
trackDataListener.onReportSpeedChanged(reportSpeed);
|
||||
trackDataListener.onRecordingGpsAccuracy(recordingGpsAccuracy);
|
||||
trackDataListener.onRecordingDistanceIntervalChanged(recordingDistanceInterval);
|
||||
}
|
||||
|
||||
notifyTracksTableUpdate(trackDataManager.getListeners(TrackDataType.TRACKS_TABLE));
|
||||
|
||||
for (TrackDataListener listener :
|
||||
trackDataManager.getListeners(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE)) {
|
||||
listener.clearTrackPoints();
|
||||
}
|
||||
notifyTrackPointsTableUpdate(true,
|
||||
trackDataManager.getListeners(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE),
|
||||
trackDataManager.getListeners(TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE));
|
||||
notifyWaypointsTableUpdate(trackDataManager.getListeners(TrackDataType.WAYPOINTS_TABLE));
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads data for a listener. To be run in the {@link #handler} thread.
|
||||
*
|
||||
* @param trackDataListener the track data listener.
|
||||
*/
|
||||
private void loadDataForListener(TrackDataListener trackDataListener) {
|
||||
Set<TrackDataListener> trackDataListeners = Collections.singleton(trackDataListener);
|
||||
EnumSet<TrackDataType> trackDataTypes = trackDataManager.getTrackDataTypes(trackDataListener);
|
||||
|
||||
if (trackDataTypes.contains(TrackDataType.PREFERENCE)) {
|
||||
trackDataListener.onMetricUnitsChanged(metricUnits);
|
||||
trackDataListener.onReportSpeedChanged(reportSpeed);
|
||||
trackDataListener.onRecordingGpsAccuracy(recordingGpsAccuracy);
|
||||
trackDataListener.onRecordingDistanceIntervalChanged(recordingDistanceInterval);
|
||||
}
|
||||
|
||||
if (trackDataTypes.contains(TrackDataType.TRACKS_TABLE)) {
|
||||
notifyTracksTableUpdate(trackDataListeners);
|
||||
}
|
||||
|
||||
boolean hasSampledIn = trackDataTypes.contains(TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE);
|
||||
boolean hasSampledOut = trackDataTypes.contains(TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE);
|
||||
if (hasSampledIn || hasSampledOut) {
|
||||
trackDataListener.clearTrackPoints();
|
||||
boolean isOnlyListener = trackDataManager.getNumberOfListeners() == 1;
|
||||
if (isOnlyListener) {
|
||||
resetSamplingState();
|
||||
}
|
||||
Set<TrackDataListener> sampledInListeners = trackDataListeners;
|
||||
Set<TrackDataListener> sampledOutListeners = hasSampledOut ? trackDataListeners
|
||||
: Collections.<TrackDataListener> emptySet();
|
||||
notifyTrackPointsTableUpdate(isOnlyListener, sampledInListeners, sampledOutListeners);
|
||||
}
|
||||
|
||||
if (trackDataTypes.contains(TrackDataType.WAYPOINTS_TABLE)) {
|
||||
notifyWaypointsTableUpdate(trackDataListeners);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies track table update. To be run in the {@link #handler} thread.
|
||||
*
|
||||
* @param trackDataListeners the track data listeners to notify
|
||||
*/
|
||||
private void notifyTracksTableUpdate(Set<TrackDataListener> trackDataListeners) {
|
||||
if (trackDataListeners.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
Track track = myTracksProviderUtils.getTrack(selectedTrackId);
|
||||
for (TrackDataListener trackDataListener : trackDataListeners) {
|
||||
trackDataListener.onTrackUpdated(track);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies waypoint table update. Currently, reloads all the waypoints up to
|
||||
* {@link 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();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the track points sampling states.
|
||||
*/
|
||||
private void resetSamplingState() {
|
||||
numLoadedPoints = 0;
|
||||
firstSeenLocationId = -1L;
|
||||
lastSeenLocationId = -1L;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link DataSource}.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
protected DataSource newDataSource() {
|
||||
return new DataSource(context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run in the handler thread.
|
||||
*
|
||||
* @param runnable the runnable
|
||||
*/
|
||||
@VisibleForTesting
|
||||
protected void runInHanderThread(Runnable runnable) {
|
||||
if (handler == null) {
|
||||
// Use a Throwable to ensure the stack trace is logged.
|
||||
Log.d(TAG, "handler is null.", new Throwable());
|
||||
return;
|
||||
}
|
||||
handler.post(runnable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the value selectedTrackId.
|
||||
*
|
||||
* @return the selectedTrackId
|
||||
*/
|
||||
@VisibleForTesting
|
||||
long getSelectedTrackId() {
|
||||
return selectedTrackId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the recordingGpsAccuracy.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
int getRecordingGpsAccuracy() {
|
||||
return recordingGpsAccuracy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the metricUnits.
|
||||
*
|
||||
* @return the metricUnits
|
||||
*/
|
||||
@VisibleForTesting
|
||||
boolean isMetricUnits() {
|
||||
return metricUnits;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the reportSpeed.
|
||||
*
|
||||
* @return the reportSpeed
|
||||
*/
|
||||
@VisibleForTesting
|
||||
boolean isReportSpeed() {
|
||||
return reportSpeed;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
* Copyright 2011 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
|
||||
package com.google.android.apps.mytracks.content;
|
||||
|
||||
import android.location.Location;
|
||||
|
||||
/**
|
||||
* Listener for track data changes.
|
||||
*
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
public interface TrackDataListener {
|
||||
|
||||
/**
|
||||
* Location state.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public enum LocationState {
|
||||
DISABLED, NO_FIX, BAD_FIX, GOOD_FIX;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the track or its statistics has been updated.
|
||||
*
|
||||
* @param track the track
|
||||
*/
|
||||
public void onTrackUpdated(Track track);
|
||||
|
||||
/**
|
||||
* Called to clear previously-sent track points.
|
||||
*/
|
||||
public void clearTrackPoints();
|
||||
|
||||
/**
|
||||
* Called when a sampled in track point is read.
|
||||
*
|
||||
* @param location the location
|
||||
*/
|
||||
public void onSampledInTrackPoint(Location location);
|
||||
|
||||
/**
|
||||
* Called when a sampled out track point is read.
|
||||
*
|
||||
* @param location the location
|
||||
*/
|
||||
public void onSampledOutTrackPoint(Location location);
|
||||
|
||||
/**
|
||||
* Called when an invalid track point representing a segment split is read.
|
||||
*/
|
||||
public void onSegmentSplit(Location location);
|
||||
|
||||
/**
|
||||
* Called when finish sending new track points. This gets called after every
|
||||
* batch of calls to {@link #onSampledInTrackPoint(Location)},
|
||||
* {@link #onSampledOutTrackPoint(Location)} and
|
||||
* {@link #onSegmentSplit(Location)}.
|
||||
*/
|
||||
public void onNewTrackPointsDone();
|
||||
|
||||
/**
|
||||
* Called to clear previously sent waypoints.
|
||||
*/
|
||||
public void clearWaypoints();
|
||||
|
||||
/**
|
||||
* Called when a new waypoint is read.
|
||||
*
|
||||
* @param waypoint the waypoint
|
||||
*/
|
||||
public void onNewWaypoint(Waypoint waypoint);
|
||||
|
||||
/**
|
||||
* Called when finish sending new waypoints. This gets called after every
|
||||
* batch of calls to {@link #clearWaypoints()} and
|
||||
* {@link #onNewWaypoint(Waypoint)}.
|
||||
*/
|
||||
public void onNewWaypointsDone();
|
||||
|
||||
/**
|
||||
* Called when the metric units preference value is change.
|
||||
*
|
||||
* @param metricUnits true to use metric units, false to use imperial units
|
||||
* @return true to reload all the data, false otherwise.
|
||||
*/
|
||||
public boolean onMetricUnitsChanged(boolean metricUnits);
|
||||
|
||||
/**
|
||||
* Called when the report speed preference value is changed.
|
||||
*
|
||||
* @param reportSpeed true to report speed, false to report pace
|
||||
* @return true to reload all the data, false otherwise.
|
||||
*/
|
||||
public boolean onReportSpeedChanged(boolean reportSpeed);
|
||||
|
||||
/**
|
||||
* Called when the recording gps accuracy preference value is changed.
|
||||
*
|
||||
* @param recordingGpsAccuracy the new value
|
||||
* @return true to reload all the data, false otherwise
|
||||
*/
|
||||
public boolean onRecordingGpsAccuracy(int recordingGpsAccuracy);
|
||||
|
||||
/**
|
||||
* Called when the recording distance interval preference value is changed.
|
||||
*
|
||||
* @param recordingDistanceInterval the new value
|
||||
* @return true to reload all the data, false otherwise.
|
||||
*/
|
||||
public boolean onRecordingDistanceIntervalChanged(int recordingDistanceInterval);
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* Copyright 2011 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
|
||||
package com.google.android.apps.mytracks.content;
|
||||
|
||||
import 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);
|
||||
}
|
||||
return types;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright 2011 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
|
||||
package com.google.android.apps.mytracks.content;
|
||||
|
||||
/**
|
||||
* Types of track data.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public enum TrackDataType {
|
||||
TRACKS_TABLE, // tracks table changes
|
||||
WAYPOINTS_TABLE, // waypoints table changes
|
||||
SAMPLED_IN_TRACK_POINTS_TABLE, // sampled-in track points table changes
|
||||
SAMPLED_OUT_TRACK_POINTS_TABLE, // sampled-out track points table changes
|
||||
PREFERENCE // preference changes
|
||||
}
|
||||
@@ -0,0 +1,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,538 @@
|
||||
/*
|
||||
* 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.support.v4.app.FragmentActivity;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewGroup.LayoutParams;
|
||||
import android.widget.ZoomControls;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumSet;
|
||||
|
||||
/**
|
||||
* A fragment to display track chart to the user.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
public class ChartFragment extends Fragment implements TrackDataListener {
|
||||
|
||||
public static final String CHART_FRAGMENT_TAG = "chartFragment";
|
||||
|
||||
private final ArrayList<double[]> pendingPoints = new ArrayList<double[]>();
|
||||
|
||||
private TrackDataHub trackDataHub;
|
||||
|
||||
// Stats gathered from the received data
|
||||
private TripStatisticsUpdater tripStatisticsUpdater;
|
||||
private long startTime;
|
||||
|
||||
private boolean metricUnits = true;
|
||||
private boolean reportSpeed = true;
|
||||
private int recordingDistanceInterval = PreferencesUtils.RECORDING_DISTANCE_INTERVAL_DEFAULT;
|
||||
|
||||
// Modes of operation
|
||||
private boolean chartByDistance = true;
|
||||
private boolean[] chartShow = new boolean[] { true, true, true, true, true, true };
|
||||
|
||||
// UI elements
|
||||
private ChartView chartView;
|
||||
private ZoomControls zoomControls;
|
||||
|
||||
/**
|
||||
* A runnable that will enable/disable zoom controls and orange pointer as
|
||||
* appropriate and redraw.
|
||||
*/
|
||||
private final Runnable updateChart = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (!isResumed() || trackDataHub == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
zoomControls.setIsZoomInEnabled(chartView.canZoomIn());
|
||||
zoomControls.setIsZoomOutEnabled(chartView.canZoomOut());
|
||||
chartView.setShowPointer(isSelectedTrackRecording());
|
||||
chartView.invalidate();
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
/*
|
||||
* Create a chartView here to store data thus won't need to reload all the
|
||||
* data on every onStart or onResume.
|
||||
*/
|
||||
chartView = new ChartView(getActivity());
|
||||
};
|
||||
|
||||
@Override
|
||||
public View onCreateView(
|
||||
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.chart, container, false);
|
||||
zoomControls = (ZoomControls) view.findViewById(R.id.chart_zoom_controls);
|
||||
zoomControls.setOnZoomInClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
zoomIn();
|
||||
}
|
||||
});
|
||||
zoomControls.setOnZoomOutClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
zoomOut();
|
||||
}
|
||||
});
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
ViewGroup layout = (ViewGroup) getActivity().findViewById(R.id.chart_view_layout);
|
||||
LayoutParams layoutParams = new LayoutParams(
|
||||
LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
|
||||
layout.addView(chartView, layoutParams);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
resumeTrackDataHub();
|
||||
checkChartSettings();
|
||||
getActivity().runOnUiThread(updateChart);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
pauseTrackDataHub();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop() {
|
||||
super.onStop();
|
||||
ViewGroup layout = (ViewGroup) getActivity().findViewById(R.id.chart_view_layout);
|
||||
layout.removeView(chartView);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTrackUpdated(Track track) {
|
||||
if (isResumed()) {
|
||||
if (track == null || track.getTripStatistics() == null) {
|
||||
startTime = -1L;
|
||||
return;
|
||||
}
|
||||
startTime = track.getTripStatistics().getStartTime();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearTrackPoints() {
|
||||
if (isResumed()) {
|
||||
tripStatisticsUpdater = startTime != -1L ? new TripStatisticsUpdater(startTime) : null;
|
||||
pendingPoints.clear();
|
||||
chartView.reset();
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (isResumed()) {
|
||||
chartView.resetScroll();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSampledInTrackPoint(Location location) {
|
||||
if (isResumed()) {
|
||||
double[] data = new double[ChartView.NUM_SERIES + 1];
|
||||
fillDataPoint(location, data);
|
||||
pendingPoints.add(data);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSampledOutTrackPoint(Location location) {
|
||||
if (isResumed()) {
|
||||
fillDataPoint(location, null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSegmentSplit(Location location) {
|
||||
if (isResumed()) {
|
||||
fillDataPoint(location, null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNewTrackPointsDone() {
|
||||
if (isResumed()) {
|
||||
chartView.addDataPoints(pendingPoints);
|
||||
pendingPoints.clear();
|
||||
runOnUiThread(updateChart);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearWaypoints() {
|
||||
if (isResumed()) {
|
||||
chartView.clearWaypoints();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNewWaypoint(Waypoint waypoint) {
|
||||
if (isResumed() && waypoint != null && LocationUtils.isValidLocation(waypoint.getLocation())) {
|
||||
chartView.addWaypoint(waypoint);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNewWaypointsDone() {
|
||||
if (isResumed()) {
|
||||
runOnUiThread(updateChart);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMetricUnitsChanged(boolean metric) {
|
||||
if (isResumed()) {
|
||||
if (metricUnits == metric) {
|
||||
return false;
|
||||
}
|
||||
metricUnits = metric;
|
||||
chartView.setMetricUnits(metricUnits);
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (isResumed()) {
|
||||
chartView.requestLayout();
|
||||
}
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onReportSpeedChanged(boolean speed) {
|
||||
if (isResumed()) {
|
||||
if (reportSpeed == speed) {
|
||||
return false;
|
||||
}
|
||||
reportSpeed = speed;
|
||||
chartView.setReportSpeed(reportSpeed);
|
||||
boolean chartShowSpeed = PreferencesUtils.getBoolean(
|
||||
getActivity(), R.string.chart_show_speed_key, PreferencesUtils.CHART_SHOW_SPEED_DEFAULT);
|
||||
setSeriesEnabled(ChartView.SPEED_SERIES, chartShowSpeed && reportSpeed);
|
||||
setSeriesEnabled(ChartView.PACE_SERIES, chartShowSpeed && !reportSpeed);
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (isResumed()) {
|
||||
chartView.requestLayout();
|
||||
}
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onRecordingGpsAccuracy(int minRequiredAccuracy) {
|
||||
// We don't care.
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onRecordingDistanceIntervalChanged(int value) {
|
||||
if (isResumed()) {
|
||||
if (recordingDistanceInterval == value) {
|
||||
return false;
|
||||
}
|
||||
recordingDistanceInterval = value;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the chart settings.
|
||||
*/
|
||||
private void checkChartSettings() {
|
||||
boolean needUpdate = false;
|
||||
if (chartByDistance != PreferencesUtils.isChartByDistance(getActivity())) {
|
||||
chartByDistance = !chartByDistance;
|
||||
chartView.setChartByDistance(chartByDistance);
|
||||
reloadTrackDataHub();
|
||||
needUpdate = true;
|
||||
}
|
||||
if (setSeriesEnabled(ChartView.ELEVATION_SERIES, PreferencesUtils.getBoolean(getActivity(),
|
||||
R.string.chart_show_elevation_key, PreferencesUtils.CHART_SHOW_ELEVATION_DEFAULT))) {
|
||||
needUpdate = true;
|
||||
}
|
||||
|
||||
boolean chartShowSpeed = PreferencesUtils.getBoolean(
|
||||
getActivity(), R.string.chart_show_speed_key, PreferencesUtils.CHART_SHOW_SPEED_DEFAULT);
|
||||
if (setSeriesEnabled(ChartView.SPEED_SERIES, chartShowSpeed && reportSpeed)) {
|
||||
needUpdate = true;
|
||||
}
|
||||
if (setSeriesEnabled(ChartView.PACE_SERIES, chartShowSpeed && !reportSpeed)) {
|
||||
needUpdate = true;
|
||||
}
|
||||
if (setSeriesEnabled(ChartView.POWER_SERIES, PreferencesUtils.getBoolean(
|
||||
getActivity(), R.string.chart_show_power_key, PreferencesUtils.CHART_SHOW_POWER_DEFAULT))) {
|
||||
needUpdate = true;
|
||||
}
|
||||
if (setSeriesEnabled(ChartView.CADENCE_SERIES, PreferencesUtils.getBoolean(getActivity(),
|
||||
R.string.chart_show_cadence_key, PreferencesUtils.CHART_SHOW_CADENCE_DEFAULT))) {
|
||||
needUpdate = true;
|
||||
}
|
||||
if (setSeriesEnabled(ChartView.HEART_RATE_SERIES, PreferencesUtils.getBoolean(getActivity(),
|
||||
R.string.chart_show_heart_rate_key, PreferencesUtils.CHART_SHOW_HEART_RATE_DEFAULT))) {
|
||||
needUpdate = true;
|
||||
}
|
||||
if (needUpdate) {
|
||||
chartView.postInvalidate();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the series enabled value.
|
||||
*
|
||||
* @param index the series index
|
||||
* @param value the value
|
||||
* @return true if changed
|
||||
*/
|
||||
private boolean setSeriesEnabled(int index, boolean value) {
|
||||
if (chartShow[index] != value) {
|
||||
chartShow[index] = value;
|
||||
chartView.setChartValueSeriesEnabled(index, value);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resumes the trackDataHub. Needs to be synchronized because trackDataHub can
|
||||
* be accessed by multiple threads.
|
||||
*/
|
||||
private synchronized void resumeTrackDataHub() {
|
||||
trackDataHub = ((TrackDetailActivity) getActivity()).getTrackDataHub();
|
||||
trackDataHub.registerTrackDataListener(this, EnumSet.of(TrackDataType.TRACKS_TABLE,
|
||||
TrackDataType.WAYPOINTS_TABLE, TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE,
|
||||
TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE, TrackDataType.PREFERENCE));
|
||||
}
|
||||
|
||||
/**
|
||||
* Pauses the trackDataHub. Needs to be synchronized because trackDataHub can
|
||||
* be accessed by multiple threads.
|
||||
*/
|
||||
private synchronized void pauseTrackDataHub() {
|
||||
trackDataHub.unregisterTrackDataListener(this);
|
||||
trackDataHub = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the selected track is recording. Needs to be synchronized
|
||||
* because trackDataHub can be accessed by multiple threads.
|
||||
*/
|
||||
private synchronized boolean isSelectedTrackRecording() {
|
||||
return trackDataHub != null && trackDataHub.isSelectedTrackRecording();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reloads the trackDataHub. Needs to be synchronized because trackDataHub can
|
||||
* be accessed by multiple threads.
|
||||
*/
|
||||
private synchronized void reloadTrackDataHub() {
|
||||
if (trackDataHub != null) {
|
||||
trackDataHub.reloadDataForListener(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* To zoom in.
|
||||
*/
|
||||
private void zoomIn() {
|
||||
chartView.zoomIn();
|
||||
zoomControls.setIsZoomInEnabled(chartView.canZoomIn());
|
||||
zoomControls.setIsZoomOutEnabled(chartView.canZoomOut());
|
||||
}
|
||||
|
||||
/**
|
||||
* To zoom out.
|
||||
*/
|
||||
private void zoomOut() {
|
||||
chartView.zoomOut();
|
||||
zoomControls.setIsZoomInEnabled(chartView.canZoomIn());
|
||||
zoomControls.setIsZoomOutEnabled(chartView.canZoomOut());
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs a runnable on the UI thread if possible.
|
||||
*
|
||||
* @param runnable the runnable
|
||||
*/
|
||||
private void runOnUiThread(Runnable runnable) {
|
||||
FragmentActivity fragmentActivity = getActivity();
|
||||
if (fragmentActivity != null) {
|
||||
fragmentActivity.runOnUiThread(runnable);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a location, fill in a data point, an array of double[]. <br>
|
||||
* data[0] = time/distance <br>
|
||||
* data[1] = elevation <br>
|
||||
* data[2] = speed <br>
|
||||
* data[3] = pace <br>
|
||||
* data[4] = heart rate <br>
|
||||
* data[5] = cadence <br>
|
||||
* data[6] = power <br>
|
||||
*
|
||||
* @param location the location
|
||||
* @param data the data point to fill in, can be null
|
||||
*/
|
||||
@VisibleForTesting
|
||||
void fillDataPoint(Location location, double data[]) {
|
||||
double timeOrDistance = Double.NaN;
|
||||
double elevation = Double.NaN;
|
||||
double speed = Double.NaN;
|
||||
double pace = Double.NaN;
|
||||
double heartRate = Double.NaN;
|
||||
double cadence = Double.NaN;
|
||||
double power = Double.NaN;
|
||||
|
||||
if (tripStatisticsUpdater != null) {
|
||||
tripStatisticsUpdater.addLocation(location, recordingDistanceInterval, PreferencesUtils
|
||||
.getInt(getActivity(), R.string.stats_weight_key, PreferencesUtils.STATS_WEIGHT_DEFAULT));
|
||||
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,133 @@
|
||||
/*
|
||||
* 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) {
|
||||
Intent intent = null;
|
||||
try {
|
||||
// HACK: UserRecoverableAuthException.getIntent can throw a null pointer exception.
|
||||
intent = e.getIntent();
|
||||
} catch (Exception e1) {
|
||||
Log.e(TAG, "Exception in getIntent", e1);
|
||||
}
|
||||
finish(scope, false, intent);
|
||||
} 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_share_url_title).create();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCancel(DialogInterface dialog) {
|
||||
super.onCancel(dialog);
|
||||
caller.onChooseActivityDone(null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the display info.
|
||||
*/
|
||||
private List<DisplayInfo> getDisplayInfos() {
|
||||
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.ic_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;
|
||||
}
|
||||
}
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* Copyright 2012 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
|
||||
package com.google.android.apps.mytracks.fragments;
|
||||
|
||||
import com.google.android.apps.mytracks.util.DialogUtils;
|
||||
import com.google.android.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;
|
||||
|
||||
/**
|
||||
* A DialogFragment to confirm delete.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class ConfirmDeleteDialogFragment extends DialogFragment {
|
||||
|
||||
/**
|
||||
* Interface for caller of this dialog fragment.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public interface ConfirmDeleteCaller {
|
||||
|
||||
/**
|
||||
* Called when confirm delete is done.
|
||||
*
|
||||
* @param trackIds list of track ids to delete. To delete all, set to size 1
|
||||
* with trackIds[0] == -1L
|
||||
*/
|
||||
public void onConfirmDeleteDone(long[] trackIds);
|
||||
}
|
||||
|
||||
public static final String CONFIRM_DELETE_DIALOG_TAG = "confirmDeleteDialog";
|
||||
private static final String KEY_TRACK_IDS = "trackIds";
|
||||
|
||||
/**
|
||||
* Create a new instance.
|
||||
*
|
||||
* @param trackIds list of track ids to delete. To delete all, set to size 1
|
||||
* with trackIds[0] == -1L
|
||||
*/
|
||||
public static ConfirmDeleteDialogFragment newInstance(long[] trackIds) {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putLongArray(KEY_TRACK_IDS, trackIds);
|
||||
|
||||
ConfirmDeleteDialogFragment deleteTrackDialogFragment = new ConfirmDeleteDialogFragment();
|
||||
deleteTrackDialogFragment.setArguments(bundle);
|
||||
return deleteTrackDialogFragment;
|
||||
}
|
||||
|
||||
private ConfirmDeleteCaller caller;
|
||||
|
||||
@Override
|
||||
public void onAttach(Activity activity) {
|
||||
super.onAttach(activity);
|
||||
try {
|
||||
caller = (ConfirmDeleteCaller) activity;
|
||||
} catch (ClassCastException e) {
|
||||
throw new ClassCastException(
|
||||
activity.toString() + " must implement " + ConfirmDeleteCaller.class.getSimpleName());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
final long[] trackIds = getArguments().getLongArray(KEY_TRACK_IDS);
|
||||
int titleId;
|
||||
int messageId;
|
||||
if (trackIds.length == 1 && trackIds[0] == -1L) {
|
||||
titleId = R.string.track_delete_all_confirm_title;
|
||||
messageId = R.string.track_delete_all_confirm_message;
|
||||
} else {
|
||||
titleId = trackIds.length > 1 ? R.string.generic_delete_selected_confirm_title
|
||||
: R.string.track_delete_one_confirm_title;
|
||||
messageId = trackIds.length > 1 ? R.string.track_delete_multiple_confirm_message
|
||||
: R.string.track_delete_one_confirm_message;
|
||||
}
|
||||
return DialogUtils.createConfirmationDialog(
|
||||
getActivity(), titleId, getString(messageId), new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
caller.onConfirmDeleteDone(trackIds);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* Copyright 2012 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
|
||||
package com.google.android.apps.mytracks.fragments;
|
||||
|
||||
import com.google.android.apps.mytracks.util.DialogUtils;
|
||||
import com.google.android.apps.mytracks.util.PreferencesUtils;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Dialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Bundle;
|
||||
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 playing a track.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class ConfirmPlayDialogFragment extends DialogFragment {
|
||||
|
||||
/**
|
||||
* Interface for caller of this dialog fragment.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public interface ConfirmPlayCaller {
|
||||
|
||||
/**
|
||||
* Called when confirm play is done.
|
||||
*/
|
||||
public void onConfirmPlayDone(long[] trackIds);
|
||||
}
|
||||
|
||||
public static final String CONFIRM_PLAY_DIALOG_TAG = "confirmPlayDialog";
|
||||
|
||||
private static final String KEY_TRACK_IDS = "trackIds";
|
||||
|
||||
private CheckBox checkBox;
|
||||
|
||||
public static ConfirmPlayDialogFragment newInstance(long[] trackIds) {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putLongArray(KEY_TRACK_IDS, trackIds);
|
||||
|
||||
ConfirmPlayDialogFragment confirmDialogFragment = new ConfirmPlayDialogFragment();
|
||||
confirmDialogFragment.setArguments(bundle);
|
||||
return confirmDialogFragment;
|
||||
}
|
||||
|
||||
private ConfirmPlayCaller caller;
|
||||
private FragmentActivity fragmentActivity;
|
||||
|
||||
@Override
|
||||
public void onAttach(Activity activity) {
|
||||
super.onAttach(activity);
|
||||
try {
|
||||
caller = (ConfirmPlayCaller) activity;
|
||||
} catch (ClassCastException e) {
|
||||
throw new ClassCastException(
|
||||
activity.toString() + " must implement " + ConfirmPlayCaller.class.getSimpleName());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
fragmentActivity = getActivity();
|
||||
}
|
||||
|
||||
@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(R.string.track_detail_play_confirm_message);
|
||||
checkBox = (CheckBox) view.findViewById(R.id.confirm_dialog_check_box);
|
||||
|
||||
return DialogUtils.createConfirmationDialog(fragmentActivity,
|
||||
R.string.track_detail_play_confirm_title, view, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
long[] trackIds = getArguments().getLongArray(KEY_TRACK_IDS);
|
||||
PreferencesUtils.setBoolean(
|
||||
fragmentActivity, R.string.confirm_play_earth_key, !checkBox.isChecked());
|
||||
caller.onConfirmPlayDone(trackIds);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
+103
@@ -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.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 confirm sync to Google Drive.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class ConfirmSyncDialogFragment extends DialogFragment {
|
||||
|
||||
/**
|
||||
* Interface for caller of this dialog fragment.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public interface ConfirmSyncCaller {
|
||||
|
||||
/**
|
||||
* Called when confirm sync is done.
|
||||
*/
|
||||
public void onConfirmSyncDone(boolean enable);
|
||||
}
|
||||
|
||||
public static final String CONFIRM_SYNC_DIALOG_TAG = "confirmSyncDialog";
|
||||
|
||||
private ConfirmSyncCaller caller;
|
||||
private FragmentActivity fragmentActivity;
|
||||
|
||||
@Override
|
||||
public void onAttach(Activity activity) {
|
||||
super.onAttach(activity);
|
||||
try {
|
||||
caller = (ConfirmSyncCaller) activity;
|
||||
} catch (ClassCastException e) {
|
||||
throw new ClassCastException(
|
||||
activity.toString() + " must implement " + ConfirmSyncCaller.class.getSimpleName());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
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.onConfirmSyncDone(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
String googleAccount = PreferencesUtils.getString(
|
||||
fragmentActivity, R.string.google_account_key, PreferencesUtils.GOOGLE_ACCOUNT_DEFAULT);
|
||||
String message = getString(
|
||||
R.string.sync_drive_confirm_message, googleAccount, getString(R.string.my_tracks_app_name));
|
||||
return new AlertDialog.Builder(fragmentActivity).setMessage(message)
|
||||
.setNegativeButton(R.string.generic_no, new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
caller.onConfirmSyncDone(false);
|
||||
}
|
||||
}).setPositiveButton(R.string.generic_yes, new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
caller.onConfirmSyncDone(true);
|
||||
}
|
||||
}).setTitle(R.string.sync_drive_title).create();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCancel(DialogInterface arg0) {
|
||||
caller.onConfirmSyncDone(false);
|
||||
}
|
||||
}
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* Copyright 2012 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
|
||||
package com.google.android.apps.mytracks.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 titleId = markerIds.length > 1 ? R.string.generic_delete_selected_confirm_title
|
||||
: R.string.marker_delete_one_confirm_title;
|
||||
int messageId = markerIds.length > 1 ? R.string.marker_delete_multiple_confirm_message
|
||||
: R.string.marker_delete_one_confirm_message;
|
||||
return DialogUtils.createConfirmationDialog(
|
||||
fragmentActivity, titleId, getString(messageId), new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
for (long markerId : markerIds) {
|
||||
MyTracksProviderUtils.Factory.get(fragmentActivity)
|
||||
.deleteWaypoint(markerId, new DescriptionGeneratorImpl(fragmentActivity));
|
||||
}
|
||||
caller.onDeleteMarkerDone();
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* 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 to Google Drive.
|
||||
*
|
||||
* @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.sync_drive_enable_message)
|
||||
.setNegativeButton(R.string.generic_no, new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
caller.onEnableSyncDone(false);
|
||||
}
|
||||
}).setPositiveButton(R.string.generic_yes, new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
caller.onEnableSyncDone(true);
|
||||
}
|
||||
}).setTitle(R.string.sync_drive_title).create();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCancel(DialogInterface arg0) {
|
||||
caller.onEnableSyncDone(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
* Copyright 2012 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
|
||||
package com.google.android.apps.mytracks.fragments;
|
||||
|
||||
import com.google.android.apps.mytracks.util.EulaUtils;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.app.Dialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.DialogFragment;
|
||||
import android.support.v4.app.FragmentActivity;
|
||||
import android.text.SpannableString;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.text.util.Linkify;
|
||||
import android.view.KeyEvent;
|
||||
import android.widget.TextView;
|
||||
|
||||
/**
|
||||
* A DialogFragment to show EULA.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class EulaDialogFragment extends 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);
|
||||
|
||||
SpannableString message = new SpannableString(getEulaText());
|
||||
Linkify.addLinks(message, Linkify.WEB_URLS);
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(fragmentActivity).setMessage(message)
|
||||
.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);
|
||||
caller.onEulaDone();
|
||||
}
|
||||
});
|
||||
}
|
||||
return builder.create();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
TextView textView = (TextView) getDialog().findViewById(android.R.id.message);
|
||||
textView.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
textView.setTextAppearance(fragmentActivity, R.style.TextSmall);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCancel(DialogInterface arg0) {
|
||||
caller.onEulaDone();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the EULA text.
|
||||
*/
|
||||
private String getEulaText() {
|
||||
String tos = getString(R.string.eula_date) + "\n\n" + getString(R.string.eula_body, GOOGLE_URL)
|
||||
+ "\n\n" + getString(R.string.eula_footer, GOOGLE_URL) + "\n\n"
|
||||
+ getString(R.string.eula_copyright_year);
|
||||
boolean isKorean = getResources().getConfiguration().locale.getLanguage().equals(KOREAN);
|
||||
if (isKorean) {
|
||||
tos += "\n\n" + getString(R.string.eula_korean);
|
||||
}
|
||||
return tos;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,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.fragments;
|
||||
|
||||
import com.google.android.apps.mytracks.io.file.TrackFileFormat;
|
||||
import com.google.android.apps.mytracks.util.FileUtils;
|
||||
import com.google.android.apps.mytracks.util.PreferencesUtils;
|
||||
import com.google.android.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.RadioButton;
|
||||
import android.widget.RadioGroup;
|
||||
import android.widget.RadioGroup.OnCheckedChangeListener;
|
||||
|
||||
/**
|
||||
* A DialogFragment to export a track.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class ExportDialogFragment extends DialogFragment {
|
||||
|
||||
/**
|
||||
* Export types.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public enum ExportType {
|
||||
GOOGLE_MAPS, GOOGLE_FUSION_TABLES, GOOGLE_SPREADSHEET, EXTERNAL_STORAGE
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for caller of this dialog fragment.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public interface ExportCaller {
|
||||
|
||||
/**
|
||||
* Called when export is done.
|
||||
*/
|
||||
public void onExportDone(ExportType exportType, TrackFileFormat trackFileFormat);
|
||||
}
|
||||
|
||||
public static final String EXPORT_DIALOG_TAG = "export";
|
||||
|
||||
private ExportCaller caller;
|
||||
private RadioGroup exportTypeOptions;
|
||||
private RadioGroup exportGoogleMapsOptions;
|
||||
private RadioGroup exportGoogleFusionTablesOptions;
|
||||
private RadioGroup exportExternalStorageOptions;
|
||||
|
||||
@Override
|
||||
public void onAttach(Activity activity) {
|
||||
super.onAttach(activity);
|
||||
try {
|
||||
caller = (ExportCaller) activity;
|
||||
} catch (ClassCastException e) {
|
||||
throw new ClassCastException(
|
||||
activity.toString() + " must implement " + ExportCaller.class.getSimpleName());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
FragmentActivity fragmentActivity = getActivity();
|
||||
|
||||
View view = fragmentActivity.getLayoutInflater().inflate(R.layout.export, null);
|
||||
|
||||
exportTypeOptions = (RadioGroup) view.findViewById(R.id.export_type_options);
|
||||
exportGoogleMapsOptions = (RadioGroup) view.findViewById(R.id.export_google_maps_options);
|
||||
exportGoogleFusionTablesOptions = (RadioGroup) view.findViewById(
|
||||
R.id.export_google_fusion_tables_options);
|
||||
exportExternalStorageOptions = (RadioGroup) view.findViewById(
|
||||
R.id.export_external_storage_options);
|
||||
|
||||
exportTypeOptions.setOnCheckedChangeListener(new OnCheckedChangeListener() {
|
||||
@Override
|
||||
public void onCheckedChanged(RadioGroup group, int checkedId) {
|
||||
exportGoogleMapsOptions.setVisibility(
|
||||
checkedId == R.id.export_google_maps ? View.VISIBLE : View.GONE);
|
||||
exportGoogleFusionTablesOptions.setVisibility(
|
||||
checkedId == R.id.export_google_fusion_tables ? View.VISIBLE : View.GONE);
|
||||
exportExternalStorageOptions.setVisibility(
|
||||
checkedId == R.id.export_external_storage ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
});
|
||||
ExportType exportType = ExportType.valueOf(PreferencesUtils.getString(
|
||||
fragmentActivity, R.string.export_type_key, PreferencesUtils.EXPORT_TYPE_DEFAULT));
|
||||
exportTypeOptions.check(getExportTypeId(exportType));
|
||||
|
||||
boolean exportGoogleMapsPublic = PreferencesUtils.getBoolean(fragmentActivity,
|
||||
R.string.export_google_maps_public_key, PreferencesUtils.EXPORT_GOOGLE_MAPS_PUBLIC_DEFAULT);
|
||||
exportGoogleMapsOptions.check(
|
||||
exportGoogleMapsPublic ? R.id.export_google_maps_public : R.id.export_google_maps_unlisted);
|
||||
|
||||
boolean exportGoogleFusionTablesPublic = PreferencesUtils.getBoolean(fragmentActivity,
|
||||
R.string.export_google_fusion_tables_public_key,
|
||||
PreferencesUtils.EXPORT_GOOGLE_FUSION_TABLES_PUBLIC_DEFAULT);
|
||||
exportGoogleFusionTablesOptions.check(
|
||||
exportGoogleFusionTablesPublic ? R.id.export_google_fusion_tables_public
|
||||
: R.id.export_google_fusion_tables_private);
|
||||
|
||||
setExternalStorageOption(
|
||||
(RadioButton) view.findViewById(R.id.export_external_storage_kml), TrackFileFormat.KML);
|
||||
setExternalStorageOption(
|
||||
(RadioButton) view.findViewById(R.id.export_external_storage_gpx), TrackFileFormat.GPX);
|
||||
setExternalStorageOption(
|
||||
(RadioButton) view.findViewById(R.id.export_external_storage_csv), TrackFileFormat.CSV);
|
||||
setExternalStorageOption(
|
||||
(RadioButton) view.findViewById(R.id.export_external_storage_tcx), TrackFileFormat.TCX);
|
||||
TrackFileFormat trackFileFormat = TrackFileFormat.valueOf(PreferencesUtils.getString(
|
||||
fragmentActivity, R.string.export_external_storage_format_key,
|
||||
PreferencesUtils.EXPORT_EXTERNAL_STORAGE_FORMAT_DEFAULT));
|
||||
exportExternalStorageOptions.check(getExternalStorageFormatId(trackFileFormat));
|
||||
|
||||
return new AlertDialog.Builder(fragmentActivity).setNegativeButton(
|
||||
R.string.generic_cancel, null)
|
||||
.setPositiveButton(R.string.menu_export, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
FragmentActivity context = getActivity();
|
||||
int id = exportTypeOptions.getCheckedRadioButtonId();
|
||||
ExportType type = getExportType(id);
|
||||
TrackFileFormat format = null;
|
||||
|
||||
PreferencesUtils.setString(context, R.string.export_type_key, type.name());
|
||||
if (id == R.id.export_google_maps) {
|
||||
PreferencesUtils.setBoolean(context, R.string.export_google_maps_public_key,
|
||||
exportGoogleMapsOptions.getCheckedRadioButtonId()
|
||||
== R.id.export_google_maps_public);
|
||||
} else if (id == R.id.export_google_fusion_tables) {
|
||||
PreferencesUtils.setBoolean(context, R.string.export_google_fusion_tables_public_key,
|
||||
exportGoogleFusionTablesOptions.getCheckedRadioButtonId()
|
||||
== R.id.export_google_fusion_tables_public);
|
||||
} else if (id == R.id.export_external_storage) {
|
||||
format = getTrackFileFormat(exportExternalStorageOptions.getCheckedRadioButtonId());
|
||||
PreferencesUtils.setString(
|
||||
context, R.string.export_external_storage_format_key, format.name());
|
||||
}
|
||||
caller.onExportDone(type, format);
|
||||
}
|
||||
}).setTitle(R.string.export_title).setView(view).create();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets an external storage option.
|
||||
*
|
||||
* @param radioButton the radio button
|
||||
* @param trackFileFormat the track file format
|
||||
*/
|
||||
private void setExternalStorageOption(RadioButton radioButton, TrackFileFormat trackFileFormat) {
|
||||
radioButton.setText(getString(R.string.export_external_storage_option, trackFileFormat.name(),
|
||||
FileUtils.getDirectoryDisplayName(trackFileFormat.getExtension())));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the external storage format id from a track file format.
|
||||
*
|
||||
* @param trackFileFormat the track file format
|
||||
*/
|
||||
private int getExternalStorageFormatId(TrackFileFormat trackFileFormat) {
|
||||
switch (trackFileFormat) {
|
||||
case KML:
|
||||
return R.id.export_external_storage_kml;
|
||||
case GPX:
|
||||
return R.id.export_external_storage_gpx;
|
||||
case CSV:
|
||||
return R.id.export_external_storage_csv;
|
||||
default:
|
||||
return R.id.export_external_storage_tcx;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the track file format from an external storage format id.
|
||||
*
|
||||
* @param externalStorageFormatId the external storage format id
|
||||
*/
|
||||
private TrackFileFormat getTrackFileFormat(int externalStorageFormatId) {
|
||||
switch (externalStorageFormatId) {
|
||||
case R.id.export_external_storage_kml:
|
||||
return TrackFileFormat.KML;
|
||||
case R.id.export_external_storage_gpx:
|
||||
return TrackFileFormat.GPX;
|
||||
case R.id.export_external_storage_csv:
|
||||
return TrackFileFormat.CSV;
|
||||
default:
|
||||
return TrackFileFormat.TCX;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the export type id from an export type.
|
||||
*
|
||||
* @param exportType the export type
|
||||
*/
|
||||
private int getExportTypeId(ExportType exportType) {
|
||||
switch (exportType) {
|
||||
case GOOGLE_MAPS:
|
||||
return R.id.export_google_maps;
|
||||
case GOOGLE_FUSION_TABLES:
|
||||
return R.id.export_google_fusion_tables;
|
||||
case GOOGLE_SPREADSHEET:
|
||||
return R.id.export_google_spreadsheets;
|
||||
default:
|
||||
return R.id.export_external_storage;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the export type from an export type id.
|
||||
*
|
||||
* @param exportTypeId the export type id
|
||||
*/
|
||||
private ExportType getExportType(int exportTypeId) {
|
||||
switch (exportTypeId) {
|
||||
case R.id.export_google_maps:
|
||||
return ExportType.GOOGLE_MAPS;
|
||||
case R.id.export_google_fusion_tables:
|
||||
return ExportType.GOOGLE_FUSION_TABLES;
|
||||
case R.id.export_google_spreadsheets:
|
||||
return ExportType.GOOGLE_SPREADSHEET;
|
||||
default:
|
||||
return ExportType.EXTERNAL_STORAGE;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.io.file.TrackFileFormat;
|
||||
import com.google.android.apps.mytracks.util.FileUtils;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.app.Dialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.DialogInterface.OnClickListener;
|
||||
import android.os.Bundle;
|
||||
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) {
|
||||
int size = getArguments().getInt(KEY_SIZE);
|
||||
int optionId = getArguments().getInt(KEY_OPTION_ID);
|
||||
final int titleId = getArguments().getInt(KEY_TITLE_ID);
|
||||
final int menuId = getArguments().getInt(KEY_MENU_ID);
|
||||
String[] choices = new String[size];
|
||||
TrackFileFormat[] trackFileFormats = TrackFileFormat.values();
|
||||
for (int i = 0; i < choices.length; i++) {
|
||||
TrackFileFormat trackFileFormat = trackFileFormats[i];
|
||||
choices[i] = getString(optionId, trackFileFormat.name(),
|
||||
FileUtils.getDirectoryDisplayName(trackFileFormat.getExtension()));
|
||||
}
|
||||
return new AlertDialog.Builder(getActivity()).setNegativeButton(R.string.generic_cancel, null)
|
||||
.setPositiveButton(R.string.generic_ok, new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
int position = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
|
||||
caller.onFileTypeDone(menuId, TrackFileFormat.values()[position]);
|
||||
}
|
||||
}).setSingleChoiceItems(choices, 0, null).setTitle(titleId).create();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,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.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.isMetricUnits(fragmentActivity);
|
||||
return StringUtils.getFrequencyOptions(fragmentActivity, metricUnits);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the list index for a frequency value. Returns 0 if the value is not on
|
||||
* the list.
|
||||
*/
|
||||
private int getListIndex(int frequencyValue) {
|
||||
String[] values = getResources().getStringArray(R.array.frequency_values);
|
||||
for (int i = 0; i < values.length; i++) {
|
||||
if (frequencyValue == Integer.parseInt(values[i])) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the frequency value from a list index.
|
||||
*
|
||||
* @param listIndex the list index
|
||||
*/
|
||||
private int getFrequencyValue(int listIndex) {
|
||||
String[] values = getResources().getStringArray(R.array.frequency_values);
|
||||
return Integer.parseInt(values[listIndex]);
|
||||
}
|
||||
}
|
||||
+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.track_detail_install_earth_title).create();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
/*
|
||||
* Copyright 2013 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
|
||||
package com.google.android.apps.mytracks.fragments;
|
||||
|
||||
import com.google.android.apps.mytracks.MarkerEditActivity;
|
||||
import com.google.android.apps.mytracks.TrackDetailActivity;
|
||||
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
|
||||
import com.google.android.apps.mytracks.content.Track;
|
||||
import com.google.android.apps.mytracks.content.Waypoint;
|
||||
import com.google.android.apps.mytracks.content.Waypoint.WaypointType;
|
||||
import com.google.android.apps.mytracks.util.IntentUtils;
|
||||
import com.google.android.apps.mytracks.util.PhotoUtils;
|
||||
import com.google.android.apps.mytracks.util.StatsUtils;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.app.FragmentActivity;
|
||||
import android.util.Log;
|
||||
import android.view.Display;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
/**
|
||||
* A fragment to show marker details.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class MarkerDetailFragment extends Fragment {
|
||||
|
||||
private static final String TAG = MarkerDetailFragment.class.getSimpleName();
|
||||
private static final String KEY_MARKER_ID = "markerId";
|
||||
|
||||
public static MarkerDetailFragment newInstance(long markerId) {
|
||||
MarkerDetailFragment fragment = new MarkerDetailFragment();
|
||||
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putLong(KEY_MARKER_ID, markerId);
|
||||
fragment.setArguments(bundle);
|
||||
|
||||
return fragment;
|
||||
}
|
||||
|
||||
private MyTracksProviderUtils myTracksProviderUtils;
|
||||
private Waypoint waypoint;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
long markerId = getArguments().getLong(KEY_MARKER_ID);
|
||||
if (markerId == -1L) {
|
||||
Log.d(TAG, "invalid marker id");
|
||||
getFragmentManager().popBackStack();
|
||||
return;
|
||||
}
|
||||
myTracksProviderUtils = MyTracksProviderUtils.Factory.get(getActivity());
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(
|
||||
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
setHasOptionsMenu(true);
|
||||
return inflater.inflate(R.layout.marker_detail_fragment, container, false);
|
||||
};
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
// Need to update the waypoint in case returning after an edit
|
||||
updateWaypoint(true);
|
||||
update();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
inflater.inflate(R.menu.marker_detail, menu);
|
||||
|
||||
updateWaypoint(false);
|
||||
|
||||
Track track = myTracksProviderUtils.getTrack(waypoint.getTrackId());
|
||||
boolean isSharedWithMe = track != null ? track.isSharedWithMe() : true;
|
||||
|
||||
menu.findItem(R.id.marker_detail_edit).setVisible(!isSharedWithMe);
|
||||
menu.findItem(R.id.marker_detail_delete).setVisible(!isSharedWithMe);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
long markerId = getArguments().getLong(KEY_MARKER_ID);
|
||||
FragmentActivity fragmentActivity = getActivity();
|
||||
Intent intent;
|
||||
|
||||
switch (item.getItemId()) {
|
||||
case R.id.marker_detail_show_on_map:
|
||||
intent = IntentUtils.newIntent(fragmentActivity, TrackDetailActivity.class)
|
||||
.putExtra(TrackDetailActivity.EXTRA_MARKER_ID, markerId);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
case R.id.marker_detail_edit:
|
||||
intent = IntentUtils.newIntent(fragmentActivity, MarkerEditActivity.class)
|
||||
.putExtra(MarkerEditActivity.EXTRA_MARKER_ID, markerId);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
case R.id.marker_detail_delete:
|
||||
DeleteMarkerDialogFragment.newInstance(new long[] { markerId })
|
||||
.show(getChildFragmentManager(), DeleteMarkerDialogFragment.DELETE_MARKER_DIALOG_TAG);
|
||||
return true;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the waypoint.
|
||||
*
|
||||
* @param refresh true to always update
|
||||
*/
|
||||
private void updateWaypoint(boolean refresh) {
|
||||
if (refresh || waypoint == null) {
|
||||
waypoint = myTracksProviderUtils.getWaypoint(getArguments().getLong(KEY_MARKER_ID));
|
||||
if (waypoint == null) {
|
||||
Log.d(TAG, "waypoint is null");
|
||||
getFragmentManager().popBackStack();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the UI.
|
||||
*/
|
||||
private void update() {
|
||||
View waypointSection = getView().findViewById(R.id.marker_detail_waypoint_section);
|
||||
View statisticsSection = getView().findViewById(R.id.marker_detail_statistics_section);
|
||||
|
||||
if (waypoint.getType() == WaypointType.WAYPOINT) {
|
||||
waypointSection.setVisibility(View.VISIBLE);
|
||||
statisticsSection.setVisibility(View.GONE);
|
||||
|
||||
ImageView imageView = (ImageView) getView().findViewById(R.id.marker_detail_waypoint_photo);
|
||||
String photoUrl = waypoint.getPhotoUrl();
|
||||
if (photoUrl == null || photoUrl.equals("")) {
|
||||
imageView.setVisibility(View.GONE);
|
||||
} else {
|
||||
imageView.setVisibility(View.VISIBLE);
|
||||
Display defaultDisplay = getActivity().getWindowManager().getDefaultDisplay();
|
||||
@SuppressWarnings("deprecation")
|
||||
int displayWidth = defaultDisplay.getWidth();
|
||||
@SuppressWarnings("deprecation")
|
||||
int displayHeight = defaultDisplay.getHeight();
|
||||
PhotoUtils.setImageVew(imageView, Uri.parse(photoUrl), displayWidth, displayHeight);
|
||||
}
|
||||
|
||||
TextView name = (TextView) getView().findViewById(R.id.marker_detail_waypoint_name);
|
||||
name.setText(getString(R.string.generic_name_line, waypoint.getName()));
|
||||
|
||||
TextView markerType = (TextView) getView()
|
||||
.findViewById(R.id.marker_detail_waypoint_marker_type);
|
||||
markerType.setText(
|
||||
getString(R.string.marker_detail_waypoint_marker_type, waypoint.getCategory()));
|
||||
|
||||
TextView description = (TextView) getView()
|
||||
.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);
|
||||
|
||||
TextView name = (TextView) getView().findViewById(R.id.marker_detail_statistics_name);
|
||||
name.setText(getString(R.string.generic_name_line, waypoint.getName()));
|
||||
|
||||
StatsUtils.setTripStatisticsValues(getActivity(), waypoint.getTripStatistics());
|
||||
StatsUtils.setLocationValues(getActivity(), waypoint.getLocation(), false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,760 @@
|
||||
/*
|
||||
* 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.view.ViewGroup.LayoutParams;
|
||||
import android.view.ViewTreeObserver.OnGlobalLayoutListener;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumSet;
|
||||
|
||||
/**
|
||||
* A fragment to display map to the user.
|
||||
*
|
||||
* @author Leif Hendrik Wilden
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
public class MyTracksMapFragment extends SupportMapFragment implements TrackDataListener {
|
||||
|
||||
public static final String MAP_FRAGMENT_TAG = "mapFragment";
|
||||
|
||||
private static final String CURRENT_LOCATION_KEY = "current_location_key";
|
||||
private static final String
|
||||
KEEP_CURRENT_LOCATION_VISIBLE_KEY = "keep_current_location_visible_key";
|
||||
|
||||
private static final float DEFAULT_ZOOM_LEVEL = 18f;
|
||||
|
||||
// Google's latitude and longitude
|
||||
private static final double DEFAULT_LATITUDE = 37.423;
|
||||
private static final double DEFAULT_LONGITUDE = -122.084;
|
||||
|
||||
private static final int MAP_VIEW_PADDING = 32;
|
||||
|
||||
// States from TrackDetailActivity, set in onResume
|
||||
private long trackId;
|
||||
private long markerId;
|
||||
private TrackDataHub trackDataHub;
|
||||
|
||||
// Current location
|
||||
private Location currentLocation;
|
||||
private Location lastTrackPoint;
|
||||
private int recordingGpsAccuracy = PreferencesUtils.RECORDING_GPS_ACCURACY_DEFAULT;
|
||||
|
||||
/**
|
||||
* True to continue keeping the current location visible on the screen.
|
||||
* <p>
|
||||
* Set to true when <br>
|
||||
* 1. user clicks on the my location button <br>
|
||||
* 2. first location during a recording <br>
|
||||
* Set to false when <br>
|
||||
* 1. showing a marker <br>
|
||||
* 2. user manually zooms/pans
|
||||
*/
|
||||
private boolean keepCurrentLocationVisible;
|
||||
|
||||
private OnLocationChangedListener onLocationChangedListener;
|
||||
|
||||
// Current track
|
||||
private Track currentTrack;
|
||||
|
||||
// Current paths
|
||||
private ArrayList<Polyline> paths = new ArrayList<Polyline>();
|
||||
boolean reloadPaths = true;
|
||||
|
||||
// UI elements
|
||||
private GoogleMap googleMap;
|
||||
private MapOverlay mapOverlay;
|
||||
private View mapView;
|
||||
private ImageButton myLocationImageButton;
|
||||
private TextView messageTextView;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle bundle) {
|
||||
super.onCreate(bundle);
|
||||
setHasOptionsMenu(true);
|
||||
mapOverlay = new MapOverlay(getActivity());
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(
|
||||
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
mapView = super.onCreateView(inflater, container, savedInstanceState);
|
||||
View layout = inflater.inflate(R.layout.map, container, false);
|
||||
RelativeLayout mapContainer = (RelativeLayout) layout.findViewById(R.id.map_container);
|
||||
mapContainer.addView(mapView, 0);
|
||||
|
||||
/*
|
||||
* For Froyo (2.2) and Gingerbread (2.3), need a transparent FrameLayout on
|
||||
* top for view pager to work correctly.
|
||||
*/
|
||||
FrameLayout frameLayout = new FrameLayout(getActivity());
|
||||
frameLayout.setBackgroundColor(getResources().getColor(android.R.color.transparent));
|
||||
mapContainer.addView(frameLayout,
|
||||
new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
|
||||
|
||||
myLocationImageButton = (ImageButton) layout.findViewById(R.id.map_my_location);
|
||||
myLocationImageButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
final MyTracksLocationManager myTracksLocationManager = new MyTracksLocationManager(
|
||||
getActivity(), Looper.myLooper(), true);
|
||||
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;
|
||||
setCurrentLocation(location);
|
||||
updateCurrentLocation(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
messageTextView = (TextView) layout.findViewById(R.id.map_message);
|
||||
return layout;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(Bundle savedInstanceState) {
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
if (savedInstanceState != null) {
|
||||
keepCurrentLocationVisible = savedInstanceState.getBoolean(
|
||||
KEEP_CURRENT_LOCATION_VISIBLE_KEY, false);
|
||||
if (keepCurrentLocationVisible) {
|
||||
Location location = (Location) savedInstanceState.getParcelable(CURRENT_LOCATION_KEY);
|
||||
if (location != null) {
|
||||
setCurrentLocation(location);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* At this point, after onCreateView, getMap will not return null and we can
|
||||
* initialize googleMap. However, onActivityCreated can be called multiple
|
||||
* times, e.g., when the user switches tabs. With
|
||||
* GoogleMapOptions.useViewLifecycleInFragment == false, googleMap lifecycle
|
||||
* is tied to the fragment lifecycle and the same googleMap object is
|
||||
* returned in getMap. Thus we only need to initialize googleMap once, when
|
||||
* it is null.
|
||||
*/
|
||||
if (googleMap == null) {
|
||||
googleMap = getMap();
|
||||
googleMap.setMyLocationEnabled(true);
|
||||
|
||||
/*
|
||||
* My Tracks needs to handle the onClick event when the my location button
|
||||
* is clicked. Currently, the API doesn't allow handling onClick event,
|
||||
* thus hiding the default my location button and providing our own.
|
||||
*/
|
||||
googleMap.getUiSettings().setMyLocationButtonEnabled(false);
|
||||
googleMap.setIndoorEnabled(true);
|
||||
googleMap.setOnMarkerClickListener(new OnMarkerClickListener() {
|
||||
|
||||
@Override
|
||||
public boolean onMarkerClick(Marker marker) {
|
||||
if (isResumed()) {
|
||||
String title = marker.getTitle();
|
||||
if (title != null && title.length() > 0) {
|
||||
long id = Long.valueOf(title);
|
||||
Context context = getActivity();
|
||||
Intent intent = IntentUtils.newIntent(context, MarkerDetailActivity.class)
|
||||
.putExtra(MarkerDetailActivity.EXTRA_MARKER_ID, id);
|
||||
context.startActivity(intent);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
googleMap.setLocationSource(new LocationSource() {
|
||||
|
||||
@Override
|
||||
public void activate(OnLocationChangedListener listener) {
|
||||
onLocationChangedListener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deactivate() {
|
||||
onLocationChangedListener = null;
|
||||
}
|
||||
});
|
||||
googleMap.setOnCameraChangeListener(new OnCameraChangeListener() {
|
||||
|
||||
@Override
|
||||
public void onCameraChange(CameraPosition cameraPosition) {
|
||||
if (isResumed() && keepCurrentLocationVisible && currentLocation != null
|
||||
&& !isLocationVisible(currentLocation)) {
|
||||
keepCurrentLocationVisible = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
// First obtain the states from TrackDetailActivity
|
||||
trackId = ((TrackDetailActivity) getActivity()).getTrackId();
|
||||
markerId = ((TrackDetailActivity) getActivity()).getMarkerId();
|
||||
resumeTrackDataHub();
|
||||
|
||||
MyTracksLocationManager myTracksLocationManager = new MyTracksLocationManager(
|
||||
getActivity(), Looper.myLooper(), false);
|
||||
boolean isGpsProviderEnabled = myTracksLocationManager.isGpsProviderEnabled();
|
||||
myTracksLocationManager.close();
|
||||
|
||||
if (googleMap != null) {
|
||||
|
||||
// Update map type
|
||||
int mapType = PreferencesUtils.getInt(
|
||||
getActivity(), R.string.map_type_key, PreferencesUtils.MAP_TYPE_DEFAUlT);
|
||||
googleMap.setMapType(mapType);
|
||||
ApiAdapterFactory.getApiAdapter().invalidMenu(getActivity());
|
||||
|
||||
// Disable my location if gps is disabled
|
||||
googleMap.setMyLocationEnabled(isGpsProviderEnabled);
|
||||
}
|
||||
|
||||
// setWarningMessage depends on resumeTrackDataHub being invoked beforehand
|
||||
setWarningMessage(isGpsProviderEnabled);
|
||||
|
||||
currentTrack = MyTracksProviderUtils.Factory.get(getActivity()).getTrack(trackId);
|
||||
mapOverlay.setShowEndMarker(!isSelectedTrackRecording());
|
||||
if (markerId != -1L) {
|
||||
showMarker(markerId);
|
||||
} else {
|
||||
if (keepCurrentLocationVisible && currentLocation != null && isSelectedTrackRecording()) {
|
||||
updateCurrentLocation(true);
|
||||
} else {
|
||||
showTrack();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
|
||||
outState.putBoolean(KEEP_CURRENT_LOCATION_VISIBLE_KEY, keepCurrentLocationVisible);
|
||||
if (currentLocation != null) {
|
||||
/*
|
||||
* currentLocation is a MyTracksLocation object, which cannot be
|
||||
* unmarshalled. Thus creating a Location object before placing it in the
|
||||
* bundle.
|
||||
*/
|
||||
outState.putParcelable(CURRENT_LOCATION_KEY, new Location(currentLocation));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
pauseTrackDataHub();
|
||||
}
|
||||
|
||||
@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 onTrackUpdated(Track track) {
|
||||
currentTrack = track;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearTrackPoints() {
|
||||
lastTrackPoint = null;
|
||||
if (isResumed()) {
|
||||
mapOverlay.clearPoints();
|
||||
reloadPaths = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSampledInTrackPoint(final Location location) {
|
||||
lastTrackPoint = location;
|
||||
if (isResumed()) {
|
||||
mapOverlay.addLocation(location);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSampledOutTrackPoint(Location location) {
|
||||
lastTrackPoint = location;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSegmentSplit(Location location) {
|
||||
if (isResumed()) {
|
||||
mapOverlay.addSegmentSplit();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNewTrackPointsDone() {
|
||||
if (isResumed()) {
|
||||
getActivity().runOnUiThread(new Runnable() {
|
||||
public void run() {
|
||||
if (isResumed() && googleMap != null && currentTrack != null) {
|
||||
boolean hasStartMarker = mapOverlay.update(
|
||||
googleMap, paths, currentTrack.getTripStatistics(), reloadPaths);
|
||||
|
||||
/*
|
||||
* If has the start marker, then don't need to reload the paths each
|
||||
* time
|
||||
*/
|
||||
if (hasStartMarker) {
|
||||
reloadPaths = false;
|
||||
}
|
||||
|
||||
if (lastTrackPoint != null && isSelectedTrackRecording()) {
|
||||
boolean firstLocation = setCurrentLocation(lastTrackPoint);
|
||||
if (firstLocation) {
|
||||
keepCurrentLocationVisible = true;
|
||||
}
|
||||
updateCurrentLocation(firstLocation);
|
||||
setWarningMessage(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearWaypoints() {
|
||||
if (isResumed()) {
|
||||
mapOverlay.clearWaypoints();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNewWaypoint(Waypoint waypoint) {
|
||||
if (isResumed() && waypoint != null && LocationUtils.isValidLocation(waypoint.getLocation())) {
|
||||
mapOverlay.addWaypoint(waypoint);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNewWaypointsDone() {
|
||||
if (isResumed()) {
|
||||
getActivity().runOnUiThread(new Runnable() {
|
||||
public void run() {
|
||||
if (isResumed() && googleMap != null && currentTrack != null) {
|
||||
mapOverlay.update(googleMap, paths, currentTrack.getTripStatistics(), true);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMetricUnitsChanged(boolean metric) {
|
||||
// We don't care.
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onReportSpeedChanged(boolean reportSpeed) {
|
||||
// We don't care.
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onRecordingGpsAccuracy(int newValue) {
|
||||
recordingGpsAccuracy = newValue;
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onRecordingDistanceIntervalChanged(int minRecordingDistance) {
|
||||
// We don't care.
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resumes the trackDataHub. Needs to be synchronized because the trackDataHub
|
||||
* can be accessed by multiple threads.
|
||||
*/
|
||||
private synchronized void resumeTrackDataHub() {
|
||||
trackDataHub = ((TrackDetailActivity) getActivity()).getTrackDataHub();
|
||||
trackDataHub.registerTrackDataListener(this, EnumSet.of(TrackDataType.TRACKS_TABLE,
|
||||
TrackDataType.WAYPOINTS_TABLE, TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE,
|
||||
TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE, TrackDataType.PREFERENCE));
|
||||
}
|
||||
|
||||
/**
|
||||
* Pauses the trackDataHub. Needs to be synchronized because the trackDataHub
|
||||
* can be accessed by multiple threads.
|
||||
*/
|
||||
private synchronized void pauseTrackDataHub() {
|
||||
if (trackDataHub != null) {
|
||||
trackDataHub.unregisterTrackDataListener(this);
|
||||
}
|
||||
trackDataHub = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the selected track is recording. Needs to be synchronized
|
||||
* because the trackDataHub can be accessed by multiple threads.
|
||||
*/
|
||||
private synchronized boolean isSelectedTrackRecording() {
|
||||
return trackDataHub != null && trackDataHub.isSelectedTrackRecording();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current location.
|
||||
*
|
||||
* @param location the location
|
||||
* @return true if this is the first location
|
||||
*/
|
||||
private boolean setCurrentLocation(Location location) {
|
||||
boolean firstLocation = false;
|
||||
if (currentLocation == null && location != null) {
|
||||
firstLocation = true;
|
||||
}
|
||||
currentLocation = location;
|
||||
return firstLocation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the current location.
|
||||
*
|
||||
* @param forceZoom true to force zoom to the current location regardless of
|
||||
* the keepCurrentLocationVisible policy
|
||||
*/
|
||||
private void updateCurrentLocation(final boolean forceZoom) {
|
||||
getActivity().runOnUiThread(new Runnable() {
|
||||
public void run() {
|
||||
if (!isResumed() || googleMap == null || onLocationChangedListener == null
|
||||
|| currentLocation == null) {
|
||||
return;
|
||||
}
|
||||
onLocationChangedListener.onLocationChanged(currentLocation);
|
||||
if (forceZoom || (keepCurrentLocationVisible && !isLocationVisible(currentLocation))) {
|
||||
LatLng latLng = new LatLng(currentLocation.getLatitude(), currentLocation.getLongitude());
|
||||
googleMap.animateCamera(CameraUpdateFactory.newLatLngZoom(latLng, DEFAULT_ZOOM_LEVEL));
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the current track by moving the camera over the track.
|
||||
*/
|
||||
private void showTrack() {
|
||||
if (googleMap == null || currentTrack == null) {
|
||||
return;
|
||||
}
|
||||
if (currentTrack.getNumberOfPoints() < 2) {
|
||||
googleMap.moveCamera(
|
||||
CameraUpdateFactory.newLatLngZoom(getDefaultLatLng(), googleMap.getMinZoomLevel()));
|
||||
return;
|
||||
}
|
||||
|
||||
if (mapView == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mapView.getWidth() == 0 || mapView.getHeight() == 0) {
|
||||
if (mapView.getViewTreeObserver().isAlive()) {
|
||||
mapView.getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
|
||||
@Override
|
||||
public void onGlobalLayout() {
|
||||
ApiAdapterFactory.getApiAdapter()
|
||||
.removeGlobalLayoutListener(mapView.getViewTreeObserver(), this);
|
||||
getActivity().runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (isResumed()) {
|
||||
moveCameraOverTrack();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
moveCameraOverTrack();
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the camera over the current track.
|
||||
*/
|
||||
private void moveCameraOverTrack() {
|
||||
/**
|
||||
* Check all the required variables.
|
||||
*/
|
||||
if (googleMap == null || currentTrack == null || currentTrack.getNumberOfPoints() < 2
|
||||
|| mapView == null || mapView.getWidth() == 0 || mapView.getHeight() == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
TripStatistics tripStatistics = currentTrack.getTripStatistics();
|
||||
int latitudeSpanE6 = tripStatistics.getTop() - tripStatistics.getBottom();
|
||||
int longitudeSpanE6 = tripStatistics.getRight() - tripStatistics.getLeft();
|
||||
if (latitudeSpanE6 > 0 && latitudeSpanE6 < 180E6 && longitudeSpanE6 > 0
|
||||
&& longitudeSpanE6 < 360E6) {
|
||||
LatLng southWest = new LatLng(
|
||||
tripStatistics.getBottomDegrees(), tripStatistics.getLeftDegrees());
|
||||
LatLng northEast = new LatLng(
|
||||
tripStatistics.getTopDegrees(), tripStatistics.getRightDegrees());
|
||||
LatLngBounds bounds = LatLngBounds.builder().include(southWest).include(northEast).build();
|
||||
|
||||
/**
|
||||
* Note cannot call CameraUpdateFactory.newLatLngBounds(LatLngBounds
|
||||
* bounds, int padding) if the map view has not undergone layout. Thus
|
||||
* calling CameraUpdateFactory.newLatLngBounds(LatLngBounds bounds, int
|
||||
* width, int height, int padding) after making sure that mapView is valid
|
||||
* in the above code.
|
||||
*/
|
||||
CameraUpdate cameraUpdate = CameraUpdateFactory.newLatLngBounds(
|
||||
bounds, mapView.getWidth(), mapView.getHeight(), MAP_VIEW_PADDING);
|
||||
googleMap.moveCamera(cameraUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a marker by moving the camera over the marker.
|
||||
*
|
||||
* @param id the marker id
|
||||
*/
|
||||
private void showMarker(final long id) {
|
||||
getActivity().runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (!isResumed() || googleMap == null) {
|
||||
return;
|
||||
}
|
||||
MyTracksProviderUtils MyTracksProviderUtils = Factory.get(getActivity());
|
||||
Waypoint waypoint = MyTracksProviderUtils.getWaypoint(id);
|
||||
if (waypoint != null && waypoint.getLocation() != null) {
|
||||
Location location = waypoint.getLocation();
|
||||
LatLng latLng = new LatLng(location.getLatitude(), location.getLongitude());
|
||||
keepCurrentLocationVisible = false;
|
||||
CameraUpdate cameraUpdate = CameraUpdateFactory.newLatLngZoom(latLng, DEFAULT_ZOOM_LEVEL);
|
||||
googleMap.moveCamera(cameraUpdate);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the default LatLng.
|
||||
*/
|
||||
private LatLng getDefaultLatLng() {
|
||||
MyTracksProviderUtils myTracksProviderUtils = MyTracksProviderUtils.Factory.get(getActivity());
|
||||
Location location = myTracksProviderUtils.getLastValidTrackPoint();
|
||||
if (location != null) {
|
||||
return new LatLng(location.getLatitude(), location.getLongitude());
|
||||
}
|
||||
return new LatLng(DEFAULT_LATITUDE, DEFAULT_LONGITUDE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the location is visible. Needs to run on the UI thread.
|
||||
*
|
||||
* @param location the location
|
||||
*/
|
||||
private boolean isLocationVisible(Location location) {
|
||||
if (location == null || googleMap == null) {
|
||||
return false;
|
||||
}
|
||||
LatLng latLng = new LatLng(location.getLatitude(), location.getLongitude());
|
||||
return googleMap.getProjection().getVisibleRegion().latLngBounds.contains(latLng);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the warning message.
|
||||
*
|
||||
* @param isGpsProviderEnabled true if gps provider is enabled
|
||||
*/
|
||||
private void setWarningMessage(boolean isGpsProviderEnabled) {
|
||||
String message;
|
||||
if (!isSelectedTrackRecording()) {
|
||||
message = null;
|
||||
} else {
|
||||
if (isGpsProviderEnabled) {
|
||||
boolean hasFix;
|
||||
boolean hasGoodFix;
|
||||
if (currentLocation == null) {
|
||||
hasFix = false;
|
||||
hasGoodFix = false;
|
||||
} else {
|
||||
hasFix = !LocationUtils.isLocationOld(currentLocation);
|
||||
hasGoodFix = currentLocation.hasAccuracy()
|
||||
&& currentLocation.getAccuracy() < recordingGpsAccuracy;
|
||||
}
|
||||
if (!hasFix) {
|
||||
message = getString(R.string.gps_wait_for_signal);
|
||||
} else if (!hasGoodFix) {
|
||||
message = getString(R.string.gps_wait_for_better_signal);
|
||||
} else {
|
||||
message = null;
|
||||
}
|
||||
} else {
|
||||
String setting = getString(
|
||||
GoogleLocationUtils.isAvailable(getActivity()) ? R.string.gps_google_location_settings
|
||||
: R.string.gps_location_access);
|
||||
message = getString(R.string.gps_disabled, setting);
|
||||
}
|
||||
}
|
||||
|
||||
if (message == null) {
|
||||
messageTextView.setVisibility(View.GONE);
|
||||
return;
|
||||
}
|
||||
messageTextView.setText(message);
|
||||
messageTextView.setVisibility(View.VISIBLE);
|
||||
if (isGpsProviderEnabled) {
|
||||
messageTextView.setOnClickListener(null);
|
||||
} else {
|
||||
Toast.makeText(getActivity(), R.string.gps_not_found, Toast.LENGTH_LONG).show();
|
||||
|
||||
// Click to show the location source settings
|
||||
messageTextView.setOnClickListener(new OnClickListener() {
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
+182
@@ -0,0 +1,182 @@
|
||||
/*
|
||||
* 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.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.CheckBox;
|
||||
import android.widget.CompoundButton;
|
||||
import android.widget.CompoundButton.OnCheckedChangeListener;
|
||||
import android.widget.FilterQueryProvider;
|
||||
import android.widget.MultiAutoCompleteTextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
/**
|
||||
* A DialogFragment to share a track.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class ShareTrackDialogFragment extends DialogFragment {
|
||||
|
||||
/**
|
||||
* Interface for caller of this dialog fragment.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public interface ShareTrackCaller {
|
||||
|
||||
/**
|
||||
* Called when share track is done.
|
||||
*
|
||||
* @param emails the added emails
|
||||
* @param makePublic true to make the track public
|
||||
*/
|
||||
public void onShareTrackDone(String emails, boolean makePublic);
|
||||
}
|
||||
|
||||
public static final String SHARE_TRACK_DIALOG_TAG = "shareTrackDialog";
|
||||
|
||||
private static final String KEY_TRACK_ID = "trackId";
|
||||
|
||||
public static ShareTrackDialogFragment newInstance(long trackId) {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putLong(KEY_TRACK_ID, trackId);
|
||||
|
||||
ShareTrackDialogFragment shareleTrackDialogFragment = new ShareTrackDialogFragment();
|
||||
shareleTrackDialogFragment.setArguments(bundle);
|
||||
return shareleTrackDialogFragment;
|
||||
}
|
||||
|
||||
private ShareTrackCaller caller;
|
||||
private FragmentActivity fragmentActivity;
|
||||
private MultiAutoCompleteTextView multiAutoCompleteTextView;
|
||||
private CheckBox publicCheckBox;
|
||||
private CheckBox inviteCheckBox;
|
||||
|
||||
@Override
|
||||
public void onAttach(Activity activity) {
|
||||
super.onAttach(activity);
|
||||
try {
|
||||
caller = (ShareTrackCaller) activity;
|
||||
} catch (ClassCastException e) {
|
||||
throw new ClassCastException(
|
||||
activity.toString() + " must implement " + ShareTrackCaller.class.getSimpleName());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
fragmentActivity = getActivity();
|
||||
View view = fragmentActivity.getLayoutInflater().inflate(R.layout.share_track, null);
|
||||
|
||||
multiAutoCompleteTextView = (MultiAutoCompleteTextView) view.findViewById(
|
||||
R.id.share_track_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);
|
||||
|
||||
publicCheckBox = (CheckBox) view.findViewById(R.id.share_track_public);
|
||||
publicCheckBox.setChecked(PreferencesUtils.getBoolean(
|
||||
fragmentActivity, R.string.share_track_public_key,
|
||||
PreferencesUtils.SHARE_TRACK_PUBLIC_DEFAULT));
|
||||
|
||||
inviteCheckBox = (CheckBox) view.findViewById(R.id.share_track_invite);
|
||||
inviteCheckBox.setOnCheckedChangeListener(new OnCheckedChangeListener() {
|
||||
@Override
|
||||
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
|
||||
multiAutoCompleteTextView.setVisibility(isChecked ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
});
|
||||
inviteCheckBox.setChecked(PreferencesUtils.getBoolean(
|
||||
fragmentActivity, R.string.share_track_invite_key,
|
||||
PreferencesUtils.SHARE_TRACK_INVITE_DEFAULT));
|
||||
|
||||
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) {
|
||||
if (!publicCheckBox.isChecked() && !inviteCheckBox.isChecked()) {
|
||||
Toast.makeText(fragmentActivity, R.string.share_track_no_selection, Toast.LENGTH_LONG)
|
||||
.show();
|
||||
return;
|
||||
}
|
||||
String acl = multiAutoCompleteTextView.getText().toString().trim();
|
||||
if (!publicCheckBox.isChecked() && acl.equals("")) {
|
||||
Toast.makeText(fragmentActivity, R.string.share_track_no_emails, Toast.LENGTH_LONG)
|
||||
.show();
|
||||
return;
|
||||
}
|
||||
PreferencesUtils.setBoolean(
|
||||
fragmentActivity, R.string.share_track_public_key, publicCheckBox.isChecked());
|
||||
PreferencesUtils.setBoolean(
|
||||
fragmentActivity, R.string.share_track_invite_key, inviteCheckBox.isChecked());
|
||||
caller.onShareTrackDone(acl, publicCheckBox.isChecked());
|
||||
}
|
||||
}).setTitle(R.string.share_track_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,265 @@
|
||||
/*
|
||||
* 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 int ONE_SECOND = 1000;
|
||||
|
||||
private TrackDataHub trackDataHub;
|
||||
private Handler handler;
|
||||
|
||||
private Location lastLocation = null;
|
||||
private TripStatistics lastTripStatistics = null;
|
||||
private int recordingGpsAccuracy = PreferencesUtils.RECORDING_GPS_ACCURACY_DEFAULT;
|
||||
|
||||
// A runnable to update the total time field.
|
||||
private final Runnable updateTotalTime = new Runnable() {
|
||||
public void run() {
|
||||
if (isResumed() && isSelectedTrackRecording()) {
|
||||
if (!isSelectedTrackPaused() && lastTripStatistics != null) {
|
||||
StatsUtils.setTotalTimeValue(getActivity(), System.currentTimeMillis()
|
||||
- lastTripStatistics.getStopTime() + lastTripStatistics.getTotalTime());
|
||||
}
|
||||
handler.postDelayed(this, ONE_SECOND);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public View onCreateView(
|
||||
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.stats, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(Bundle savedInstanceState) {
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
handler = new Handler();
|
||||
updateUi(getActivity());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
resumeTrackDataHub();
|
||||
if (isSelectedTrackRecording()) {
|
||||
handler.post(updateTotalTime);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
pauseTrackDataHub();
|
||||
handler.removeCallbacks(updateTotalTime);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTrackUpdated(final Track track) {
|
||||
if (isResumed()) {
|
||||
getActivity().runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (isResumed()) {
|
||||
lastTripStatistics = track != null ? track.getTripStatistics() : null;
|
||||
updateUi(getActivity());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearTrackPoints() {
|
||||
lastLocation = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSampledInTrackPoint(Location location) {
|
||||
lastLocation = location;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSampledOutTrackPoint(Location location) {
|
||||
lastLocation = location;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSegmentSplit(Location location) {
|
||||
// We don't care.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNewTrackPointsDone() {
|
||||
if (isResumed()) {
|
||||
getActivity().runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (isResumed()) {
|
||||
if (!isSelectedTrackRecording() || isSelectedTrackPaused()) {
|
||||
lastLocation = null;
|
||||
}
|
||||
|
||||
if (lastLocation != null) {
|
||||
boolean hasFix = !LocationUtils.isLocationOld(lastLocation);
|
||||
boolean hasGoodFix = lastLocation.hasAccuracy()
|
||||
&& lastLocation.getAccuracy() < recordingGpsAccuracy;
|
||||
|
||||
if (!hasFix || !hasGoodFix) {
|
||||
lastLocation = null;
|
||||
}
|
||||
}
|
||||
StatsUtils.setLocationValues(getActivity(), lastLocation, isSelectedTrackRecording());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearWaypoints() {
|
||||
// We don't care.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNewWaypoint(Waypoint wpt) {
|
||||
// We don't care.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNewWaypointsDone() {
|
||||
// We don't care.
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMetricUnitsChanged(final boolean metric) {
|
||||
if (isResumed()) {
|
||||
getActivity().runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (isResumed()) {
|
||||
updateUi(getActivity());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onReportSpeedChanged(final boolean speed) {
|
||||
if (isResumed()) {
|
||||
getActivity().runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (isResumed()) {
|
||||
updateUi(getActivity());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onRecordingGpsAccuracy(int newValue) {
|
||||
recordingGpsAccuracy = newValue;
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onRecordingDistanceIntervalChanged(int minRecordingDistance) {
|
||||
// We don't care.
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resumes the trackDataHub. Needs to be synchronized because trackDataHub can
|
||||
* be accessed by multiple threads.
|
||||
*/
|
||||
private synchronized void resumeTrackDataHub() {
|
||||
trackDataHub = ((TrackDetailActivity) getActivity()).getTrackDataHub();
|
||||
trackDataHub.registerTrackDataListener(this, EnumSet.of(TrackDataType.TRACKS_TABLE,
|
||||
TrackDataType.SAMPLED_IN_TRACK_POINTS_TABLE, TrackDataType.SAMPLED_OUT_TRACK_POINTS_TABLE,
|
||||
TrackDataType.PREFERENCE));
|
||||
}
|
||||
|
||||
/**
|
||||
* Pauses the trackDataHub. Needs to be synchronized because trackDataHub can
|
||||
* be accessed by multiple threads.
|
||||
*/
|
||||
private synchronized void pauseTrackDataHub() {
|
||||
trackDataHub.unregisterTrackDataListener(this);
|
||||
trackDataHub = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the selected track is recording. Needs to be synchronized
|
||||
* because trackDataHub can be accessed by multiple threads.
|
||||
*/
|
||||
private synchronized boolean isSelectedTrackRecording() {
|
||||
return trackDataHub != null && trackDataHub.isSelectedTrackRecording();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the selected track is paused. Needs to be synchronized
|
||||
* because trackDataHub can be accessed by multiple threads.
|
||||
*/
|
||||
private synchronized boolean isSelectedTrackPaused() {
|
||||
return trackDataHub != null && trackDataHub.isSelectedTrackPaused();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the UI.
|
||||
*/
|
||||
private void updateUi(FragmentActivity activity) {
|
||||
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,278 @@
|
||||
/*
|
||||
* 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 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 directoryPath = FileUtils.getDirectoryPath(FileUtils.BACKUPS_DIR);
|
||||
final File dir = new File(directoryPath);
|
||||
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,216 @@
|
||||
/*
|
||||
* 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.activity_recognition_type_key));
|
||||
doNotBackup.add(context.getString(R.string.ant_bike_cadence_sensor_id_key));
|
||||
doNotBackup.add(context.getString(R.string.ant_combined_bike_sensor_id_key));
|
||||
doNotBackup.add(context.getString(R.string.ant_heart_rate_monitor_id_key));
|
||||
doNotBackup.add(context.getString(R.string.ant_speed_distance_monitor_id_key));
|
||||
doNotBackup.add(context.getString(R.string.auto_resume_track_current_retry_key));
|
||||
doNotBackup.add(context.getString(R.string.bluetooth_sensor_key));
|
||||
doNotBackup.add(context.getString(R.string.confirm_play_earth_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.google_account_key));
|
||||
doNotBackup.add(context.getString(R.string.recording_track_id_key));
|
||||
doNotBackup.add(context.getString(R.string.recording_track_paused_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,130 @@
|
||||
/*
|
||||
* 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)) {
|
||||
showNoBackupMessage();
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
backupDates = externalFileBackup.getAvailableBackups();
|
||||
if (backupDates == null || backupDates.length == 0) {
|
||||
showNoBackupMessage();
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a no backup message.
|
||||
*/
|
||||
private void showNoBackupMessage() {
|
||||
String message = getString(R.string.settings_backup_restore_no_backups,
|
||||
FileUtils.getDirectoryDisplayName(FileUtils.BACKUPS_DIR));
|
||||
Toast.makeText(this, message, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright 2013 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
|
||||
package com.google.android.apps.mytracks.io.drive;
|
||||
|
||||
import com.google.android.apps.mytracks.io.fusiontables.SendFusionTablesActivity;
|
||||
import com.google.android.apps.mytracks.io.maps.SendMapsActivity;
|
||||
import com.google.android.apps.mytracks.io.sendtogoogle.AbstractSendActivity;
|
||||
import com.google.android.apps.mytracks.io.sendtogoogle.AbstractSendAsyncTask;
|
||||
import com.google.android.apps.mytracks.io.sendtogoogle.SendRequest;
|
||||
import com.google.android.apps.mytracks.io.sendtogoogle.UploadResultActivity;
|
||||
import com.google.android.apps.mytracks.io.spreadsheets.SendSpreadsheetsActivity;
|
||||
import com.google.android.apps.mytracks.util.IntentUtils;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.content.Intent;
|
||||
|
||||
/**
|
||||
* An activity to send a track to Google Drive.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class SendDriveActivity extends AbstractSendActivity {
|
||||
|
||||
@Override
|
||||
protected AbstractSendAsyncTask createAsyncTask() {
|
||||
return new SendDriveAsyncTask(this, sendRequest.getTrackId(), sendRequest.getAccount(),
|
||||
sendRequest.getDriveShareEmails(), sendRequest.isDriveSharePublic());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getServiceName() {
|
||||
return getString(R.string.export_google_drive);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void startNextActivity(boolean success, boolean isCancel) {
|
||||
sendRequest.setDriveSuccess(success);
|
||||
Class<?> next = getNextClass(isCancel);
|
||||
Intent intent = IntentUtils.newIntent(this, next)
|
||||
.putExtra(SendRequest.SEND_REQUEST_KEY, sendRequest);
|
||||
startActivity(intent);
|
||||
finish();
|
||||
}
|
||||
|
||||
private Class<?> getNextClass(boolean isCancel) {
|
||||
if (isCancel) {
|
||||
return UploadResultActivity.class;
|
||||
} else {
|
||||
if (sendRequest.isSendMaps()) {
|
||||
return SendMapsActivity.class;
|
||||
} else if (sendRequest.isSendFusionTables()) {
|
||||
return SendFusionTablesActivity.class;
|
||||
} else if (sendRequest.isSendSpreadsheets()) {
|
||||
return SendSpreadsheetsActivity.class;
|
||||
} else {
|
||||
return UploadResultActivity.class;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
/*
|
||||
* 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 com.google.api.services.drive.model.PermissionList;
|
||||
|
||||
import android.accounts.Account;
|
||||
import android.content.Context;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* AsyncTask to send a track to Google Drive.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class SendDriveAsyncTask extends AbstractSendAsyncTask {
|
||||
|
||||
private final long trackId;
|
||||
private final Account account;
|
||||
private final String[] acl;
|
||||
private final boolean isPublic;
|
||||
private final Context context;
|
||||
private final MyTracksProviderUtils myTracksProviderUtils;
|
||||
|
||||
public SendDriveAsyncTask(
|
||||
SendDriveActivity activity, long trackId, Account account, String acl, boolean isPublic) {
|
||||
super(activity);
|
||||
this.trackId = trackId;
|
||||
this.account = account;
|
||||
if (acl != null) {
|
||||
this.acl = acl.split(",");
|
||||
} else {
|
||||
this.acl = null;
|
||||
}
|
||||
this.isPublic = isPublic;
|
||||
|
||||
context = activity.getApplicationContext();
|
||||
myTracksProviderUtils = MyTracksProviderUtils.Factory.get(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void closeConnection() {}
|
||||
|
||||
@Override
|
||||
protected boolean performTask() {
|
||||
try {
|
||||
GoogleAccountCredential credential = SendToGoogleUtils.getGoogleAccountCredential(
|
||||
context, account.name, SendToGoogleUtils.DRIVE_SCOPE);
|
||||
if (credential == null) {
|
||||
return false;
|
||||
}
|
||||
Drive drive = SyncUtils.getDriveService(credential);
|
||||
File folder = SyncUtils.getMyTracksFolder(context, drive);
|
||||
if (folder == null) {
|
||||
return false;
|
||||
}
|
||||
String folderId = folder.getId();
|
||||
if (folderId == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Track track = myTracksProviderUtils.getTrack(trackId);
|
||||
String driveId = track.getDriveId();
|
||||
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, driveFile);
|
||||
return true;
|
||||
}
|
||||
SyncUtils.updateTrackWithDriveFileInfo(myTracksProviderUtils, track, null);
|
||||
}
|
||||
|
||||
File file = SyncUtils.insertDriveFile(
|
||||
drive, folderId, context, myTracksProviderUtils, track, false);
|
||||
if (file == null) {
|
||||
return false;
|
||||
}
|
||||
addPermission(drive, file);
|
||||
return true;
|
||||
} catch (UserRecoverableAuthException e) {
|
||||
SendToGoogleUtils.sendNotification(
|
||||
context, account.name, e.getIntent(), SendToGoogleUtils.DRIVE_NOTIFICATION_ID);
|
||||
return false;
|
||||
} catch (GoogleAuthException e) {
|
||||
return retryTask();
|
||||
} catch (UserRecoverableAuthIOException e) {
|
||||
SendToGoogleUtils.sendNotification(
|
||||
context, account.name, e.getIntent(), SendToGoogleUtils.DRIVE_NOTIFICATION_ID);
|
||||
return false;
|
||||
} catch (IOException e) {
|
||||
return retryTask();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void invalidateToken() {}
|
||||
|
||||
/**
|
||||
* Adds permission.
|
||||
*
|
||||
* @param drive the drive
|
||||
* @param file the drive file
|
||||
*/
|
||||
private void addPermission(Drive drive, File file) throws IOException {
|
||||
if (isPublic) {
|
||||
boolean hasPublic = false;
|
||||
PermissionList permissionList = drive.permissions().list(file.getId()).execute();
|
||||
for (Permission permission : permissionList.getItems()) {
|
||||
String role = permission.getRole();
|
||||
if (role.equals("reader") || role.equals("writer")) {
|
||||
if (permission.getType().equals("anyone")) {
|
||||
hasPublic = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!hasPublic) {
|
||||
Permission permission = new Permission();
|
||||
permission.setRole("reader");
|
||||
permission.setType("anyone");
|
||||
permission.setValue("");
|
||||
drive.permissions().insert(file.getId(), permission).execute();
|
||||
}
|
||||
shareUrl = file.getAlternateLink();
|
||||
}
|
||||
|
||||
if (acl != null) {
|
||||
for (String email : acl) {
|
||||
email = email.trim();
|
||||
if (!email.equals("")) {
|
||||
Permission permission = new Permission();
|
||||
permission.setRole("reader");
|
||||
permission.setType("user");
|
||||
permission.setValue(email);
|
||||
drive.permissions().insert(file.getId(), permission).execute();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,603 @@
|
||||
/*
|
||||
* 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 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 file track importers like {@link GpxFileTrackImporter} and
|
||||
* {@link KmlFileTrackImporter}.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
abstract class AbstractFileTrackImporter extends DefaultHandler implements TrackImporter {
|
||||
|
||||
/**
|
||||
* Data for the current track.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
private class TrackData {
|
||||
|
||||
// The current track
|
||||
Track track = new Track();
|
||||
|
||||
// The number of segments processed for the current track
|
||||
int numberOfSegments = 0;
|
||||
|
||||
/*
|
||||
* The last location in the current segment. Null if the current segment
|
||||
* doesn't have a last location.
|
||||
*/
|
||||
Location lastLocationInCurrentSegment;
|
||||
|
||||
// The number of locations processed for the current track
|
||||
int numberOfLocations = 0;
|
||||
|
||||
// The trip statistics updater for the current track
|
||||
TripStatisticsUpdater tripStatisticsUpdater;
|
||||
|
||||
// The import time of the track.
|
||||
long importTime = System.currentTimeMillis();
|
||||
|
||||
// The buffered locations
|
||||
Location[] bufferedLocations = new Location[MAX_BUFFERED_LOCATIONS];
|
||||
|
||||
// The number of buffered locations
|
||||
int numBufferedLocations = 0;
|
||||
}
|
||||
|
||||
private static final String TAG = AbstractFileTrackImporter.class.getSimpleName();
|
||||
|
||||
// The maximum number of buffered locations for bulk-insertion
|
||||
private static final int MAX_BUFFERED_LOCATIONS = 512;
|
||||
|
||||
private final Context context;
|
||||
private final long importTrackId;
|
||||
private final MyTracksProviderUtils myTracksProviderUtils;
|
||||
private final int recordingDistanceInterval;
|
||||
private final List<Long> trackIds;
|
||||
private final List<Waypoint> waypoints;
|
||||
|
||||
// The current track data
|
||||
private TrackData trackData;
|
||||
|
||||
// The SAX locator to get the current line information
|
||||
private Locator locator;
|
||||
|
||||
// The current element content
|
||||
protected String content;
|
||||
|
||||
protected String name;
|
||||
protected String description;
|
||||
protected String category;
|
||||
protected String latitude;
|
||||
protected String longitude;
|
||||
protected String altitude;
|
||||
protected String time;
|
||||
protected String waypointType;
|
||||
protected String photoUrl;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param context the context
|
||||
* @param importTrackId the track id to import to. -1L to import to a new
|
||||
* track.
|
||||
*/
|
||||
AbstractFileTrackImporter(
|
||||
Context context, long importTrackId, MyTracksProviderUtils myTracksProviderUtils) {
|
||||
this.context = context;
|
||||
this.importTrackId = importTrackId;
|
||||
this.myTracksProviderUtils = myTracksProviderUtils;
|
||||
this.recordingDistanceInterval = PreferencesUtils.getInt(context,
|
||||
R.string.recording_distance_interval_key,
|
||||
PreferencesUtils.RECORDING_DISTANCE_INTERVAL_DEFAULT);
|
||||
trackIds = new ArrayList<Long>();
|
||||
waypoints = new ArrayList<Waypoint>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setDocumentLocator(Locator locator) {
|
||||
this.locator = locator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void characters(char[] ch, int start, int length) throws SAXException {
|
||||
String newContent = new String(ch, start, length);
|
||||
if (content == null) {
|
||||
content = newContent;
|
||||
} else {
|
||||
/*
|
||||
* In 99% of the cases, a single call to this method will be made for each
|
||||
* sequence of characters we're interested in, so we'll rarely be
|
||||
* concatenating strings, thus not justifying the use of a StringBuilder.
|
||||
*/
|
||||
content += newContent;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long importFile(InputStream inputStream) {
|
||||
try {
|
||||
SAXParser saxParser = SAXParserFactory.newInstance().newSAXParser();
|
||||
long start = System.currentTimeMillis();
|
||||
|
||||
saxParser.parse(inputStream, this);
|
||||
Log.d(Constants.TAG, "Total import time: " + (System.currentTimeMillis() - start) + "ms");
|
||||
if (trackIds.size() != 1) {
|
||||
Log.d(TAG, trackIds.size() + " tracks imported");
|
||||
cleanImport();
|
||||
return -1L;
|
||||
}
|
||||
return trackIds.get(0);
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Unable to import file", e);
|
||||
cleanImport();
|
||||
return -1L;
|
||||
} catch (ParserConfigurationException e) {
|
||||
Log.e(TAG, "Unable to import file", e);
|
||||
cleanImport();
|
||||
return -1L;
|
||||
} catch (SAXException e) {
|
||||
Log.e(TAG, "Unable to import file", e);
|
||||
cleanImport();
|
||||
return -1L;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* On file end.
|
||||
*/
|
||||
protected void onFileEnd() {
|
||||
// Add waypoints to the last imported track
|
||||
int size = trackIds.size();
|
||||
if (size == 0) {
|
||||
return;
|
||||
}
|
||||
long trackId = trackIds.get(size - 1);
|
||||
Track track = myTracksProviderUtils.getTrack(trackId);
|
||||
if (track == null) {
|
||||
return;
|
||||
}
|
||||
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, recordingDistanceInterval,
|
||||
PreferencesUtils.getInt(context, R.string.stats_weight_key,
|
||||
PreferencesUtils.STATS_WEIGHT_DEFAULT));
|
||||
markerTripStatisticsUpdater.addLocation(trackPoint, recordingDistanceInterval,
|
||||
PreferencesUtils.getInt(context, R.string.stats_weight_key,
|
||||
PreferencesUtils.STATS_WEIGHT_DEFAULT));
|
||||
}
|
||||
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, waypoint.getPhotoUrl());
|
||||
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);
|
||||
|
||||
if (photoUrl != null) {
|
||||
waypoint.setPhotoUrl(photoUrl);
|
||||
}
|
||||
waypoints.add(waypoint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a track point.
|
||||
*/
|
||||
protected Location getTrackPoint() throws SAXException {
|
||||
Location location = createLocation();
|
||||
|
||||
// Calculate derived attributes from the previous point
|
||||
if (trackData.lastLocationInCurrentSegment != null
|
||||
&& trackData.lastLocationInCurrentSegment.getTime() != 0) {
|
||||
long timeDifference = location.getTime() - trackData.lastLocationInCurrentSegment.getTime();
|
||||
|
||||
// Check for negative time change
|
||||
if (timeDifference <= 0) {
|
||||
Log.w(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) {
|
||||
return null;
|
||||
}
|
||||
double latitudeValue;
|
||||
double longitudeValue;
|
||||
try {
|
||||
latitudeValue = Double.parseDouble(latitude);
|
||||
longitudeValue = Double.parseDouble(longitude);
|
||||
} catch (NumberFormatException e) {
|
||||
throw new SAXException(createErrorMessage(
|
||||
String.format("Unable to parse latitude longitude: %s %s", latitude, longitude)), e);
|
||||
}
|
||||
Double altitudeValue = null;
|
||||
if (altitude != null) {
|
||||
try {
|
||||
altitudeValue = Double.parseDouble(altitude);
|
||||
} catch (NumberFormatException e) {
|
||||
throw new SAXException(
|
||||
createErrorMessage(String.format("Unable to parse altitude: %s", altitude)), e);
|
||||
}
|
||||
}
|
||||
|
||||
long timeValue;
|
||||
if (time == null) {
|
||||
timeValue = trackData.importTime;
|
||||
} else {
|
||||
try {
|
||||
timeValue = StringUtils.getTime(time);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new SAXException(
|
||||
createErrorMessage(String.format("Unable to parse time: %s", time)), e);
|
||||
}
|
||||
}
|
||||
return createLocation(latitudeValue, longitudeValue, altitudeValue, timeValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a location.
|
||||
*
|
||||
* @param latitudeValue the latitude value
|
||||
* @param longitudeValue the longitude value
|
||||
* @param altitudeValue the altitude value
|
||||
* @param timeValue the time value
|
||||
*/
|
||||
private Location createLocation(
|
||||
double latitudeValue, double longitudeValue, Double altitudeValue, long timeValue) {
|
||||
Location location = new Location(LocationManager.GPS_PROVIDER);
|
||||
location.setLatitude(latitudeValue);
|
||||
location.setLongitude(longitudeValue);
|
||||
if (altitudeValue != null) {
|
||||
location.setAltitude(altitudeValue);
|
||||
} else {
|
||||
location.removeAltitude();
|
||||
}
|
||||
location.setTime(timeValue);
|
||||
location.removeAccuracy();
|
||||
location.removeBearing();
|
||||
location.removeSpeed();
|
||||
return location;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a location.
|
||||
*
|
||||
* @param location the location
|
||||
*/
|
||||
private void insertLocation(Location location) {
|
||||
if (trackData.tripStatisticsUpdater == null) {
|
||||
trackData.tripStatisticsUpdater = new TripStatisticsUpdater(
|
||||
location.getTime() != -1L ? location.getTime() : trackData.importTime);
|
||||
}
|
||||
trackData.tripStatisticsUpdater.addLocation(location, recordingDistanceInterval,
|
||||
PreferencesUtils.getInt(context, R.string.stats_weight_key,
|
||||
PreferencesUtils.STATS_WEIGHT_DEFAULT));
|
||||
|
||||
trackData.bufferedLocations[trackData.numBufferedLocations] = location;
|
||||
trackData.numBufferedLocations++;
|
||||
trackData.numberOfLocations++;
|
||||
|
||||
if (trackData.numBufferedLocations >= MAX_BUFFERED_LOCATIONS) {
|
||||
flushLocations(trackData);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Flushes the locations to the database.
|
||||
*
|
||||
* @param data the track data
|
||||
*/
|
||||
private void flushLocations(TrackData data) {
|
||||
if (data.numBufferedLocations <= 0) {
|
||||
return;
|
||||
}
|
||||
myTracksProviderUtils.bulkInsertTrackPoint(
|
||||
data.bufferedLocations, data.numBufferedLocations, data.track.getId());
|
||||
data.numBufferedLocations = 0;
|
||||
if (data.track.getStartId() == -1L) {
|
||||
data.track.setStartId(myTracksProviderUtils.getFirstTrackPointId(data.track.getId()));
|
||||
}
|
||||
data.track.setStopId(myTracksProviderUtils.getLastTrackPointId(data.track.getId()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts the first waypoint, the track statistics waypoint.
|
||||
*
|
||||
* @param track the track
|
||||
*/
|
||||
private void insertFirstWaypoint(Track track) {
|
||||
String waypointName = context.getString(R.string.marker_split_name_format, 0);
|
||||
String waypointCategory = "";
|
||||
TripStatisticsUpdater updater = new TripStatisticsUpdater(
|
||||
track.getTripStatistics().getStartTime());
|
||||
TripStatistics tripStatistics = updater.getTripStatistics();
|
||||
String waypointDescription = new DescriptionGeneratorImpl(context).generateWaypointDescription(
|
||||
tripStatistics);
|
||||
String icon = context.getString(R.string.marker_statistics_icon_url);
|
||||
double length = 0.0;
|
||||
long duration = 0L;
|
||||
Location waypointLocation = new Location("");
|
||||
waypointLocation.setLatitude(100);
|
||||
waypointLocation.setLongitude(180);
|
||||
Waypoint waypoint = new Waypoint(waypointName, waypointDescription, waypointCategory, icon,
|
||||
track.getId(), WaypointType.STATISTICS, length, duration, -1L, -1L, waypointLocation,
|
||||
tripStatistics, "");
|
||||
myTracksProviderUtils.insertWaypoint(waypoint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up import.
|
||||
*/
|
||||
private void cleanImport() {
|
||||
for (long trackId : trackIds) {
|
||||
myTracksProviderUtils.deleteTrack(trackId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
/*
|
||||
* 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 GpxFileTrackImporter extends AbstractFileTrackImporter {
|
||||
|
||||
private static final String TAG_DESCRIPTION = "desc";
|
||||
private static final String TAG_COMMENT = "cmt";
|
||||
private static final String TAG_ELEVATION = "ele";
|
||||
private static final String TAG_GPX = "gpx";
|
||||
private static final String TAG_NAME = "name";
|
||||
private static final String TAG_TIME = "time";
|
||||
private static final String TAG_TRACK = "trk";
|
||||
private static final String TAG_TRACK_POINT = "trkpt";
|
||||
private static final String TAG_TRACK_SEGMENT = "trkseg";
|
||||
private static final String TAG_TYPE = "type";
|
||||
private static final String TAG_WAYPOINT = "wpt";
|
||||
|
||||
private static final String ATTRIBUTE_LAT = "lat";
|
||||
private static final String ATTRIBUTE_LON = "lon";
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param context the context
|
||||
*/
|
||||
public GpxFileTrackImporter(Context context) {
|
||||
this(context, MyTracksProviderUtils.Factory.get(context));
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
GpxFileTrackImporter(Context context, MyTracksProviderUtils myTracksProviderUtils) {
|
||||
super(context, -1L, myTracksProviderUtils);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startElement(String uri, String localName, String tag, Attributes attributes)
|
||||
throws SAXException {
|
||||
if (tag.equals(TAG_WAYPOINT)) {
|
||||
onWaypointStart(attributes);
|
||||
} else if (tag.equals(TAG_TRACK)) {
|
||||
onTrackStart();
|
||||
} else if (tag.equals(TAG_TRACK_SEGMENT)) {
|
||||
onTrackSegmentStart();
|
||||
} else if (tag.equals(TAG_TRACK_POINT)) {
|
||||
onTrackPointStart(attributes);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void endElement(String uri, String localName, String tag) throws SAXException {
|
||||
if (tag.equals(TAG_GPX)) {
|
||||
onFileEnd();
|
||||
} else if (tag.equals(TAG_WAYPOINT)) {
|
||||
onWaypointEnd();
|
||||
} else if (tag.equals(TAG_TRACK)) {
|
||||
onTrackEnd();
|
||||
} else if (tag.equals(TAG_TRACK_POINT)) {
|
||||
onTrackPointEnd();
|
||||
} else if (tag.equals(TAG_NAME)) {
|
||||
if (content != null) {
|
||||
name = content.trim();
|
||||
}
|
||||
} else if (tag.equals(TAG_DESCRIPTION)) {
|
||||
if (content != null) {
|
||||
description = content.trim();
|
||||
}
|
||||
} else if (tag.equals(TAG_TYPE)) {
|
||||
if (content != null) {
|
||||
category = content.trim();
|
||||
}
|
||||
} else if (tag.equals(TAG_TIME)) {
|
||||
if (content != null) {
|
||||
time = content.trim();
|
||||
}
|
||||
} else if (tag.equals(TAG_ELEVATION)) {
|
||||
if (content != null) {
|
||||
altitude = content.trim();
|
||||
}
|
||||
} else if (tag.equals(TAG_COMMENT)) {
|
||||
if (content != null) {
|
||||
waypointType = content.trim();
|
||||
}
|
||||
}
|
||||
|
||||
// Reset element content
|
||||
content = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onTrackStart() throws SAXException {
|
||||
super.onTrackStart();
|
||||
name = null;
|
||||
description = null;
|
||||
category = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* On track point start.
|
||||
*
|
||||
* @param attributes the attributes
|
||||
*/
|
||||
private void onTrackPointStart(Attributes attributes) {
|
||||
latitude = attributes.getValue(ATTRIBUTE_LAT);
|
||||
longitude = attributes.getValue(ATTRIBUTE_LON);
|
||||
altitude = null;
|
||||
time = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* On track point end.
|
||||
*/
|
||||
private void onTrackPointEnd() throws SAXException {
|
||||
Location location = getTrackPoint();
|
||||
if (location == null) {
|
||||
return;
|
||||
}
|
||||
insertTrackPoint(location);
|
||||
}
|
||||
|
||||
/**
|
||||
* On waypoint start.
|
||||
*
|
||||
* @param attributes the attributes
|
||||
*/
|
||||
private void onWaypointStart(Attributes attributes) {
|
||||
name = null;
|
||||
description = null;
|
||||
category = null;
|
||||
latitude = attributes.getValue(ATTRIBUTE_LAT);
|
||||
longitude = attributes.getValue(ATTRIBUTE_LON);
|
||||
altitude = null;
|
||||
time = null;
|
||||
waypointType = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* On waypoint end.
|
||||
*/
|
||||
private void onWaypointEnd() throws SAXException {
|
||||
addWaypoint(WaypointType.STATISTICS.name().equals(waypointType) ? WaypointType.STATISTICS
|
||||
: WaypointType.WAYPOINT);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
/*
|
||||
* 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 directoryDisplayName;
|
||||
|
||||
// the number of files successfully imported
|
||||
private int successCount;
|
||||
|
||||
// the number of files to import
|
||||
private int totalCount;
|
||||
|
||||
// the last successfully imported track id
|
||||
private long trackId;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
Intent intent = getIntent();
|
||||
importAll = intent.getBooleanExtra(EXTRA_IMPORT_ALL, false);
|
||||
trackFileFormat = intent.getParcelableExtra(EXTRA_TRACK_FILE_FORMAT);
|
||||
if (trackFileFormat == null) {
|
||||
trackFileFormat = TrackFileFormat.GPX;
|
||||
}
|
||||
|
||||
if (!FileUtils.isExternalStorageAvailable()) {
|
||||
Toast.makeText(this, R.string.external_storage_not_available, Toast.LENGTH_LONG).show();
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
String directoryPath;
|
||||
if (importAll) {
|
||||
directoryDisplayName = FileUtils.getDirectoryDisplayName(
|
||||
trackFileFormat.getExtension());
|
||||
directoryPath = FileUtils.getDirectoryPath(trackFileFormat.getExtension());
|
||||
if (!FileUtils.isDirectory(new File(directoryPath))) {
|
||||
Toast.makeText(
|
||||
this, getString(R.string.import_no_directory, directoryDisplayName), Toast.LENGTH_LONG)
|
||||
.show();
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
String action = intent.getAction();
|
||||
if (!(Intent.ACTION_ATTACH_DATA.equals(action) || Intent.ACTION_VIEW.equals(action))) {
|
||||
Log.d(TAG, "Invalid action: " + intent);
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
Uri data = intent.getData();
|
||||
if (!UriUtils.isFileUri(data)) {
|
||||
Log.d(TAG, "Invalid data: " + intent);
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
directoryDisplayName = data.getPath();
|
||||
directoryPath = data.getPath();
|
||||
}
|
||||
|
||||
Object retained = getLastNonConfigurationInstance();
|
||||
if (retained instanceof ImportAsyncTask) {
|
||||
importAsyncTask = (ImportAsyncTask) retained;
|
||||
importAsyncTask.setActivity(this);
|
||||
} else {
|
||||
importAsyncTask = new ImportAsyncTask(this, importAll, trackFileFormat, directoryPath);
|
||||
importAsyncTask.execute();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object onRetainNonConfigurationInstance() {
|
||||
importAsyncTask.setActivity(null);
|
||||
return importAsyncTask;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Dialog onCreateDialog(int id) {
|
||||
switch (id) {
|
||||
case DIALOG_PROGRESS_ID:
|
||||
progressDialog = DialogUtils.createHorizontalProgressDialog(
|
||||
this, R.string.import_progress_message, new DialogInterface.OnCancelListener() {
|
||||
@Override
|
||||
public void onCancel(DialogInterface dialog) {
|
||||
importAsyncTask.cancel(true);
|
||||
dialog.dismiss();
|
||||
}
|
||||
}, directoryDisplayName);
|
||||
return progressDialog;
|
||||
case DIALOG_RESULT_ID:
|
||||
int iconId;
|
||||
int titleId;
|
||||
String message;
|
||||
String totalFiles = getResources()
|
||||
.getQuantityString(R.plurals.files, totalCount, totalCount);
|
||||
if (successCount == totalCount) {
|
||||
if (totalCount == 0) {
|
||||
iconId = android.R.drawable.ic_dialog_info;
|
||||
titleId = R.string.import_no_file_title;
|
||||
message = getString(R.string.import_no_file, directoryDisplayName);
|
||||
} else {
|
||||
iconId = R.drawable.ic_dialog_success;
|
||||
titleId = R.string.generic_success_title;
|
||||
message = getString(R.string.import_success, totalFiles, directoryDisplayName);
|
||||
}
|
||||
} else {
|
||||
iconId = android.R.drawable.ic_dialog_alert;
|
||||
titleId = R.string.generic_error_title;
|
||||
message = getString(
|
||||
R.string.import_error, successCount, totalFiles, directoryDisplayName);
|
||||
}
|
||||
return new AlertDialog.Builder(this).setCancelable(true).setIcon(iconId).setMessage(message)
|
||||
.setOnCancelListener(new DialogInterface.OnCancelListener() {
|
||||
@Override
|
||||
public void onCancel(DialogInterface dialog) {
|
||||
dialog.dismiss();
|
||||
finish();
|
||||
}
|
||||
}).setPositiveButton(R.string.generic_ok, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
if (successCount == totalCount && !importAll && trackId != -1L) {
|
||||
Intent intent = IntentUtils.newIntent(
|
||||
ImportActivity.this, TrackDetailActivity.class)
|
||||
.putExtra(TrackDetailActivity.EXTRA_TRACK_ID, trackId);
|
||||
TaskStackBuilder.create(ImportActivity.this)
|
||||
.addParentStack(TrackDetailActivity.class).addNextIntent(intent)
|
||||
.startActivities();
|
||||
}
|
||||
dialog.dismiss();
|
||||
finish();
|
||||
}
|
||||
}).setTitle(titleId).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,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.file;
|
||||
|
||||
import com.google.android.apps.mytracks.io.file.export.KmzTrackExporter;
|
||||
import com.google.android.apps.mytracks.util.FileUtils;
|
||||
import com.google.android.apps.mytracks.util.PreferencesUtils;
|
||||
import com.google.android.apps.mytracks.util.SystemUtils;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Environment;
|
||||
import android.os.PowerManager.WakeLock;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* AsyncTask to import files from the external storage.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class ImportAsyncTask extends AsyncTask<Void, Integer, Boolean> {
|
||||
|
||||
private static final String TAG = ImportAsyncTask.class.getSimpleName();
|
||||
|
||||
private ImportActivity importActivity;
|
||||
private final boolean importAll;
|
||||
private final TrackFileFormat trackFileFormat;
|
||||
private final String path;
|
||||
private final Context context;
|
||||
private WakeLock wakeLock;
|
||||
|
||||
// true if the AsyncTask has completed
|
||||
private boolean completed;
|
||||
|
||||
// the number of files successfully imported
|
||||
private int successCount;
|
||||
|
||||
// the number of files to import
|
||||
private int totalCount;
|
||||
|
||||
// the last successfully imported track id
|
||||
private long trackId;
|
||||
|
||||
/**
|
||||
* Creates an AsyncTask.
|
||||
*
|
||||
* @param importActivity the activity currently associated with this AsyncTask
|
||||
* @param importAll true to import all GPX files
|
||||
* @param trackFileFormat the track file format
|
||||
* @param path path to import GPX files
|
||||
*/
|
||||
public ImportAsyncTask(ImportActivity importActivity, boolean importAll,
|
||||
TrackFileFormat trackFileFormat, String path) {
|
||||
this.importActivity = importActivity;
|
||||
this.importAll = importAll;
|
||||
this.trackFileFormat = trackFileFormat;
|
||||
this.path = path;
|
||||
context = importActivity.getApplicationContext();
|
||||
|
||||
completed = false;
|
||||
successCount = 0;
|
||||
totalCount = 0;
|
||||
trackId = -1L;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current {@link ImportActivity} associated with this AyncTask.
|
||||
*
|
||||
* @param importActivity the current {@link ImportActivity}, can be null
|
||||
*/
|
||||
public void setActivity(ImportActivity importActivity) {
|
||||
this.importActivity = importActivity;
|
||||
if (completed && importActivity != null) {
|
||||
importActivity.onAsyncTaskCompleted(successCount, totalCount, trackId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
if (importActivity != null) {
|
||||
importActivity.showProgressDialog();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Boolean doInBackground(Void... params) {
|
||||
try {
|
||||
// Get the wake lock if not recording or paused
|
||||
boolean isRecording =
|
||||
PreferencesUtils.getLong(importActivity, R.string.recording_track_id_key)
|
||||
!= PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
|
||||
boolean isPaused = PreferencesUtils.getBoolean(importActivity,
|
||||
R.string.recording_track_paused_key, PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT);
|
||||
if (!isRecording || isPaused) {
|
||||
wakeLock = SystemUtils.acquireWakeLock(importActivity, wakeLock);
|
||||
}
|
||||
|
||||
List<File> files = getFiles();
|
||||
totalCount = files.size();
|
||||
if (totalCount == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (int i = 0; i < totalCount; i++) {
|
||||
if (isCancelled()) {
|
||||
// If cancelled, return true to show the number of files imported
|
||||
return true;
|
||||
}
|
||||
if (importFile(files.get(i))) {
|
||||
successCount++;
|
||||
}
|
||||
publishProgress(i + 1, totalCount);
|
||||
}
|
||||
return true;
|
||||
} finally {
|
||||
if (wakeLock != null && wakeLock.isHeld()) {
|
||||
wakeLock.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onProgressUpdate(Integer... values) {
|
||||
if (importActivity != null) {
|
||||
importActivity.setProgressDialogValue(values[0], values[1]);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Boolean result) {
|
||||
completed = true;
|
||||
if (importActivity != null) {
|
||||
importActivity.onAsyncTaskCompleted(successCount, totalCount, trackId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCancelled() {
|
||||
completed = true;
|
||||
if (importActivity != null) {
|
||||
importActivity.onAsyncTaskCompleted(successCount, totalCount, trackId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports a file.
|
||||
*
|
||||
* @param file the file
|
||||
*/
|
||||
private boolean importFile(final File file) {
|
||||
FileInputStream fileInputStream = null;
|
||||
try {
|
||||
TrackImporter trackImporter;
|
||||
if (trackFileFormat == TrackFileFormat.KML) {
|
||||
String name = file.getName();
|
||||
if (name.endsWith("." + TrackFileFormat.KML.getExtension())) {
|
||||
trackImporter = new KmlFileTrackImporter(context, -1L, null);
|
||||
} else {
|
||||
File dir = new File(
|
||||
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES),
|
||||
FileUtils.SDCARD_TOP_DIR);
|
||||
FileUtils.ensureDirectoryExists(dir);
|
||||
dir = new File(dir, name.substring(0, name.lastIndexOf('.')));
|
||||
FileUtils.ensureDirectoryExists(dir);
|
||||
trackImporter = new KmzTrackImporter(context, dir.getPath());
|
||||
}
|
||||
} else {
|
||||
trackImporter = new GpxFileTrackImporter(context);
|
||||
}
|
||||
fileInputStream = new FileInputStream(file);
|
||||
trackId = trackImporter.importFile(fileInputStream);
|
||||
return trackId != -1L;
|
||||
} catch (FileNotFoundException e) {
|
||||
Log.e(TAG, "Unable to import file", e);
|
||||
return false;
|
||||
} finally {
|
||||
if (fileInputStream != null) {
|
||||
try {
|
||||
fileInputStream.close();
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Unable to close file input stream", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of files. If importAll is true, returns a list of the files
|
||||
* under the path directory. If importAll is false, returns a list containing
|
||||
* just the path file.
|
||||
*/
|
||||
private List<File> getFiles() {
|
||||
List<File> files = new ArrayList<File>();
|
||||
File file = new File(path);
|
||||
if (importAll) {
|
||||
File[] candidates = file.listFiles();
|
||||
if (candidates != null) {
|
||||
for (File candidate : candidates) {
|
||||
if (!candidate.isDirectory()) {
|
||||
String name = candidate.getName();
|
||||
if (trackFileFormat == TrackFileFormat.KML && (
|
||||
name.endsWith("." + TrackFileFormat.KML.getExtension())
|
||||
|| name.endsWith("." + KmzTrackExporter.KMZ_EXTENSION))) {
|
||||
files.add(candidate);
|
||||
} else if (trackFileFormat == TrackFileFormat.GPX
|
||||
&& name.endsWith("." + TrackFileFormat.GPX.getExtension())) {
|
||||
files.add(candidate);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
files.add(file);
|
||||
}
|
||||
return files;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,306 @@
|
||||
/*
|
||||
* 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 android.net.Uri;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import org.xml.sax.Attributes;
|
||||
import org.xml.sax.SAXException;
|
||||
|
||||
/**
|
||||
* Imports a KML file.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class KmlFileTrackImporter extends AbstractFileTrackImporter {
|
||||
|
||||
private static final String CADENCE = "cadence";
|
||||
private static final String HEART_RATE = "heart_rate";
|
||||
private static final String POWER = "power";
|
||||
|
||||
private static final String STATISTICS_STYLE = "#statistics";
|
||||
private static final String WAYPOINT_STYLE = "#waypoint";
|
||||
|
||||
private static final String TAG_COORDINATES = "coordinates";
|
||||
private static final String TAG_DESCRIPTION = "description";
|
||||
private static final String TAG_GX_COORD = "gx:coord";
|
||||
private static final String TAG_GX_MULTI_TRACK = "gx:MultiTrack";
|
||||
private static final String TAG_GX_SIMPLE_ARRAY_DATA = "gx:SimpleArrayData";
|
||||
private static final String TAG_GX_TRACK = "gx:Track";
|
||||
private static final String TAG_GX_VALUE = "gx:value";
|
||||
private static final String TAG_HREF = "href";
|
||||
private static final String TAG_KML = "kml";
|
||||
private static final String TAG_NAME = "name";
|
||||
private static final String TAG_PHOTO_OVERLAY = "PhotoOverlay";
|
||||
private static final String TAG_PLACEMARK = "Placemark";
|
||||
private static final String TAG_STYLE_URL = "styleUrl";
|
||||
private static final String TAG_VALUE = "value";
|
||||
private static final String TAG_WHEN = "when";
|
||||
|
||||
private static final String ATTRIBUTE_NAME = "name";
|
||||
|
||||
private final String photoPath;
|
||||
private boolean trackStarted = false;
|
||||
private String sensorName;
|
||||
private ArrayList<Location> locationList;
|
||||
private ArrayList<Integer> cadenceList;
|
||||
private ArrayList<Integer> heartRateList;
|
||||
private ArrayList<Integer> powerList;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param context the context
|
||||
* @param importTrackId track id to import to. -1L to import to a new track.
|
||||
*/
|
||||
public KmlFileTrackImporter(Context context, long importTrackId, String photoPath) {
|
||||
this(context, importTrackId, photoPath, MyTracksProviderUtils.Factory.get(context));
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public KmlFileTrackImporter(Context context, long importTrackId, String photoPath,
|
||||
MyTracksProviderUtils myTracksProviderUtils) {
|
||||
super(context, importTrackId, myTracksProviderUtils);
|
||||
this.photoPath = photoPath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startElement(String uri, String localName, String tag, Attributes attributes)
|
||||
throws SAXException {
|
||||
if (tag.equals(TAG_PLACEMARK) || tag.equals(TAG_PHOTO_OVERLAY)) {
|
||||
onWaypointStart();
|
||||
} else if (tag.equals(TAG_GX_MULTI_TRACK)) {
|
||||
trackStarted = true;
|
||||
onTrackStart();
|
||||
} else if (tag.equals(TAG_GX_TRACK)) {
|
||||
if (!trackStarted) {
|
||||
throw new SAXException("No " + TAG_GX_MULTI_TRACK);
|
||||
}
|
||||
onTrackSegmentStart();
|
||||
} else if (tag.equals(TAG_GX_SIMPLE_ARRAY_DATA)) {
|
||||
onSensorDataStart(attributes);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void endElement(String uri, String localName, String tag) throws SAXException {
|
||||
if (tag.equals(TAG_KML)) {
|
||||
onFileEnd();
|
||||
} else if (tag.equals(TAG_PLACEMARK) || tag.equals(TAG_PHOTO_OVERLAY)) {
|
||||
onWaypointEnd();
|
||||
} else if (localName.equals(TAG_COORDINATES)) {
|
||||
onWaypointLocationEnd();
|
||||
} else if (tag.equals(TAG_GX_MULTI_TRACK)) {
|
||||
onTrackEnd();
|
||||
} else if (tag.equals(TAG_GX_TRACK)) {
|
||||
onTrackSegmentEnd();
|
||||
} else if (tag.equals(TAG_GX_COORD)) {
|
||||
onTrackPointEnd();
|
||||
} else if (tag.equals(TAG_GX_VALUE)) {
|
||||
onSensorValueEnd();
|
||||
} else if (tag.equals(TAG_NAME)) {
|
||||
if (content != null) {
|
||||
name = content.trim();
|
||||
}
|
||||
} else if (localName.equals(TAG_DESCRIPTION)) {
|
||||
if (content != null) {
|
||||
description = content.trim();
|
||||
}
|
||||
} else if (localName.equals(TAG_VALUE)) {
|
||||
if (content != null) {
|
||||
category = content.trim();
|
||||
}
|
||||
} else if (localName.equals(TAG_WHEN)) {
|
||||
if (content != null) {
|
||||
time = content.trim();
|
||||
}
|
||||
} else if (localName.equals(TAG_STYLE_URL)) {
|
||||
if (content != null) {
|
||||
waypointType = content.trim();
|
||||
}
|
||||
} else if (localName.equals(TAG_HREF)) {
|
||||
if (content != null) {
|
||||
photoUrl = content.trim();
|
||||
}
|
||||
}
|
||||
|
||||
// Reset element content
|
||||
content = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* On waypoint start.
|
||||
*/
|
||||
private void onWaypointStart() {
|
||||
// Reset all Placemark variables
|
||||
name = null;
|
||||
description = null;
|
||||
category = null;
|
||||
latitude = null;
|
||||
longitude = null;
|
||||
altitude = null;
|
||||
time = null;
|
||||
waypointType = null;
|
||||
photoUrl = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* On waypoint end.
|
||||
*/
|
||||
private void onWaypointEnd() throws SAXException {
|
||||
// Add a waypoint if the waypointType matches
|
||||
WaypointType type = null;
|
||||
if (WAYPOINT_STYLE.equals(waypointType)) {
|
||||
type = WaypointType.WAYPOINT;
|
||||
} else if (STATISTICS_STYLE.equals(waypointType)) {
|
||||
type = WaypointType.STATISTICS;
|
||||
}
|
||||
if (type == null) {
|
||||
return;
|
||||
}
|
||||
if (photoUrl != null && photoPath != null) {
|
||||
Uri uri = Uri.parse(photoUrl);
|
||||
photoUrl = photoPath + File.separatorChar + uri.getLastPathSegment();
|
||||
}
|
||||
addWaypoint(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* On waypoint location end.
|
||||
*/
|
||||
private void onWaypointLocationEnd() {
|
||||
if (content != null) {
|
||||
String parts[] = content.trim().split(",");
|
||||
if (parts.length != 2 && parts.length != 3) {
|
||||
return;
|
||||
}
|
||||
longitude = parts[0];
|
||||
latitude = parts[1];
|
||||
altitude = parts.length == 3 ? parts[2] : null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onTrackSegmentStart() {
|
||||
super.onTrackSegmentStart();
|
||||
locationList = new ArrayList<Location>();
|
||||
powerList = new ArrayList<Integer>();
|
||||
cadenceList = new ArrayList<Integer>();
|
||||
heartRateList = new ArrayList<Integer>();
|
||||
}
|
||||
|
||||
/**
|
||||
* On track segment end.
|
||||
*/
|
||||
private void onTrackSegmentEnd() {
|
||||
// Close a track segment by inserting the segment locations
|
||||
boolean hasPower = powerList.size() == locationList.size();
|
||||
boolean hasCadence = cadenceList.size() == locationList.size();
|
||||
boolean hasHeartRate = heartRateList.size() == locationList.size();
|
||||
|
||||
for (int i = 0; i < locationList.size(); i++) {
|
||||
Location location = locationList.get(i);
|
||||
|
||||
if (!hasPower && !hasCadence && !hasHeartRate) {
|
||||
insertTrackPoint(location);
|
||||
} else {
|
||||
SensorDataSet.Builder builder = Sensor.SensorDataSet.newBuilder();
|
||||
if (hasPower) {
|
||||
builder.setPower(Sensor.SensorData.newBuilder()
|
||||
.setValue(powerList.get(i)).setState(Sensor.SensorState.SENDING));
|
||||
}
|
||||
if (hasCadence) {
|
||||
builder.setCadence(Sensor.SensorData.newBuilder()
|
||||
.setValue(cadenceList.get(i)).setState(Sensor.SensorState.SENDING));
|
||||
}
|
||||
if (hasHeartRate) {
|
||||
builder.setHeartRate(Sensor.SensorData.newBuilder()
|
||||
.setValue(heartRateList.get(i)).setState(Sensor.SensorState.SENDING));
|
||||
}
|
||||
SensorDataSet sensorDataSet = builder.setCreationTime(location.getTime()).build();
|
||||
MyTracksLocation myTracksLocation = new MyTracksLocation(location, sensorDataSet);
|
||||
insertTrackPoint(myTracksLocation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* On track point end. gx:coord end tag.
|
||||
*/
|
||||
private void onTrackPointEnd() throws SAXException {
|
||||
// Add location to locationList
|
||||
if (content == null) {
|
||||
return;
|
||||
}
|
||||
String parts[] = content.trim().split(" ");
|
||||
if (parts.length != 2 && parts.length != 3) {
|
||||
return;
|
||||
}
|
||||
longitude = parts[0];
|
||||
latitude = parts[1];
|
||||
altitude = parts.length == 3 ? parts[2] : null;
|
||||
|
||||
Location location = getTrackPoint();
|
||||
if (location == null) {
|
||||
return;
|
||||
}
|
||||
locationList.add(location);
|
||||
time = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* On sensor data start. gx:SimpleArrayData start tag.
|
||||
*
|
||||
* @param attributes
|
||||
*/
|
||||
private void onSensorDataStart(Attributes attributes) {
|
||||
sensorName = attributes.getValue(ATTRIBUTE_NAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* On sensor value end. gx:value end tag.
|
||||
*/
|
||||
private void onSensorValueEnd() throws SAXException {
|
||||
if (content != null) {
|
||||
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,181 @@
|
||||
/*
|
||||
* Copyright 2013 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
|
||||
package com.google.android.apps.mytracks.io.file;
|
||||
|
||||
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
|
||||
import com.google.android.apps.mytracks.io.file.export.KmzTrackExporter;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
|
||||
/**
|
||||
* Imports a KMZ file.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class KmzTrackImporter implements TrackImporter {
|
||||
|
||||
private static final String TAG = KmzTrackImporter.class.getSimpleName();
|
||||
private static final int BUFFER_SIZE = 4096;
|
||||
|
||||
private final Context context;
|
||||
private final String photoPath;
|
||||
|
||||
public KmzTrackImporter(Context context, String photoPath) {
|
||||
this.context = context;
|
||||
this.photoPath = photoPath;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long importFile(InputStream inputStream) {
|
||||
ZipInputStream zipInputStream = null;
|
||||
long importedTrackId = -1L;
|
||||
try {
|
||||
ZipEntry zipEntry;
|
||||
|
||||
zipInputStream = new ZipInputStream(inputStream);
|
||||
while ((zipEntry = zipInputStream.getNextEntry()) != null) {
|
||||
if (Thread.interrupted()) {
|
||||
Log.d(TAG, "Thread interrupted");
|
||||
cleanImport(importedTrackId);
|
||||
return -1L;
|
||||
}
|
||||
String fileName = zipEntry.getName();
|
||||
if (fileName.equals(KmzTrackExporter.KMZ_KML_FILE)) {
|
||||
importedTrackId = parseKml(zipInputStream);
|
||||
if (importedTrackId == -1L) {
|
||||
Log.d(TAG, "Unable to parse kml in kmz");
|
||||
cleanImport(importedTrackId);
|
||||
return -1L;
|
||||
}
|
||||
} else {
|
||||
String prefix = KmzTrackExporter.KMZ_IMAGES_DIR + File.separatorChar;
|
||||
if (fileName.startsWith(prefix)) {
|
||||
readImageFile(zipInputStream, fileName.substring(prefix.length()));
|
||||
}
|
||||
}
|
||||
zipInputStream.closeEntry();
|
||||
}
|
||||
return importedTrackId;
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Unable to import file", e);
|
||||
cleanImport(importedTrackId);
|
||||
return -1L;
|
||||
} finally {
|
||||
if (zipInputStream != null) {
|
||||
try {
|
||||
zipInputStream.close();
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Unable to close zip input stream", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up import.
|
||||
*
|
||||
* @param importedTrackId the imported track id
|
||||
*/
|
||||
private void cleanImport(long importedTrackId) {
|
||||
if (importedTrackId != -1L) {
|
||||
MyTracksProviderUtils myTracksProviderUtils = MyTracksProviderUtils.Factory.get(context);
|
||||
myTracksProviderUtils.deleteTrack(importedTrackId);
|
||||
}
|
||||
if (photoPath != null) {
|
||||
File dir = new File(photoPath);
|
||||
if (dir.exists() && dir.isDirectory()) {
|
||||
for (File file : dir.listFiles()) {
|
||||
file.delete();
|
||||
}
|
||||
dir.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses kml
|
||||
*
|
||||
* @param zipInputStream the zip input stream
|
||||
* @return the imported track id or -1L
|
||||
*/
|
||||
private long parseKml(ZipInputStream zipInputStream) throws IOException {
|
||||
ByteArrayInputStream byteArrayInputStream = null;
|
||||
try {
|
||||
KmlFileTrackImporter kmlFileTrackImporter = new KmlFileTrackImporter(context, -1L, photoPath);
|
||||
byteArrayInputStream = new ByteArrayInputStream(getKml(zipInputStream));
|
||||
return kmlFileTrackImporter.importFile(byteArrayInputStream);
|
||||
} finally {
|
||||
if (byteArrayInputStream != null) {
|
||||
byteArrayInputStream.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the kml as byte array.
|
||||
*
|
||||
* @param zipInputStream the zip input stream
|
||||
*/
|
||||
private byte[] getKml(ZipInputStream zipInputStream) throws IOException {
|
||||
ByteArrayOutputStream byteArrayOutputStream = null;
|
||||
try {
|
||||
byteArrayOutputStream = new ByteArrayOutputStream();
|
||||
byte[] buffer = new byte[BUFFER_SIZE];
|
||||
int count;
|
||||
while ((count = zipInputStream.read(buffer)) != -1) {
|
||||
byteArrayOutputStream.write(buffer, 0, count);
|
||||
}
|
||||
return byteArrayOutputStream.toByteArray();
|
||||
} finally {
|
||||
if (byteArrayOutputStream != null) {
|
||||
byteArrayOutputStream.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads an image file.
|
||||
*
|
||||
* @param zipInputStream the zip input stream
|
||||
* @param fileName the file name
|
||||
*/
|
||||
private void readImageFile(ZipInputStream zipInputStream, String fileName) throws IOException {
|
||||
FileOutputStream fileOutputStream = null;
|
||||
try {
|
||||
fileOutputStream = new FileOutputStream(photoPath + File.separatorChar + fileName);
|
||||
byte[] buffer = new byte[BUFFER_SIZE];
|
||||
int count;
|
||||
while ((count = zipInputStream.read(buffer)) != -1) {
|
||||
fileOutputStream.write(buffer, 0, count);
|
||||
}
|
||||
} finally {
|
||||
if (fileOutputStream != null) {
|
||||
fileOutputStream.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package com.google.android.apps.mytracks.io.file;
|
||||
|
||||
import com.google.android.apps.mytracks.io.file.export.CsvTrackWriter;
|
||||
import com.google.android.apps.mytracks.io.file.export.GpxTrackWriter;
|
||||
import com.google.android.apps.mytracks.io.file.export.KmlTrackWriter;
|
||||
import com.google.android.apps.mytracks.io.file.export.TcxTrackWriter;
|
||||
import com.google.android.apps.mytracks.io.file.export.TrackWriter;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Definition of all possible track formats.
|
||||
*/
|
||||
public enum TrackFileFormat implements Parcelable {
|
||||
KML {
|
||||
@Override
|
||||
public TrackWriter newTrackWriter(Context context, boolean inZip) {
|
||||
return new KmlTrackWriter(context, inZip);
|
||||
}
|
||||
},
|
||||
GPX {
|
||||
@Override
|
||||
public TrackWriter newTrackWriter(Context context, boolean inZip) {
|
||||
return new GpxTrackWriter(context);
|
||||
}
|
||||
},
|
||||
CSV {
|
||||
@Override
|
||||
public TrackWriter newTrackWriter(Context context, boolean inZip) {
|
||||
return new CsvTrackWriter(context);
|
||||
}
|
||||
},
|
||||
TCX {
|
||||
@Override
|
||||
public TrackWriter newTrackWriter(Context context, boolean inZip) {
|
||||
return new TcxTrackWriter(context);
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(final Parcel dest, final int flags) {
|
||||
dest.writeInt(ordinal());
|
||||
}
|
||||
|
||||
public static final Creator<TrackFileFormat> CREATOR = new Creator<TrackFileFormat>() {
|
||||
@Override
|
||||
public TrackFileFormat createFromParcel(final Parcel source) {
|
||||
return TrackFileFormat.values()[source.readInt()];
|
||||
}
|
||||
|
||||
@Override
|
||||
public TrackFileFormat[] newArray(final int size) {
|
||||
return new TrackFileFormat[size];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a new track writer for the format.
|
||||
*/
|
||||
public abstract TrackWriter newTrackWriter(Context context, boolean inZip);
|
||||
|
||||
/**
|
||||
* 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,34 @@
|
||||
/*
|
||||
* Copyright 2013 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package com.google.android.apps.mytracks.io.file;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* Interface for a track importer.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public interface TrackImporter {
|
||||
|
||||
/**
|
||||
* Import a file.
|
||||
*
|
||||
* @param inputStream the file's input stream
|
||||
* @return the imported track id or -1L.
|
||||
*/
|
||||
public long importFile(InputStream inputStream);
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Copyright 2013 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
|
||||
package com.google.android.apps.mytracks.io.file.export;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
/**
|
||||
* Abstract class for {@link TrackExporter}.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
abstract class AbstractTrackExporter implements TrackExporter {
|
||||
|
||||
private static final String TAG = AbstractTrackExporter.class.getSimpleName();
|
||||
|
||||
private Thread writeThread;
|
||||
private boolean success = false;
|
||||
|
||||
@Override
|
||||
public void writeTrack(final OutputStream outputStream) {
|
||||
writeThread = new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
performWrite(outputStream);
|
||||
success = true;
|
||||
} catch (InterruptedException e) {
|
||||
Log.e(TAG, "Unable to perform write", e);
|
||||
success = false;
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Unable to perform write", e);
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
writeThread.start();
|
||||
try {
|
||||
writeThread.join();
|
||||
} catch (InterruptedException e) {
|
||||
Log.e(TAG, "Interrupted while waiting for write thread to finish", e);
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSuccess() {
|
||||
return success;
|
||||
}
|
||||
|
||||
abstract void performWrite(OutputStream outputStream) throws InterruptedException, IOException;
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
/*
|
||||
* 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.export;
|
||||
|
||||
import com.google.android.apps.mytracks.content.MyTracksLocation;
|
||||
import com.google.android.apps.mytracks.content.Sensor;
|
||||
import com.google.android.apps.mytracks.content.Sensor.SensorData;
|
||||
import com.google.android.apps.mytracks.content.Sensor.SensorDataSet;
|
||||
import com.google.android.apps.mytracks.content.Track;
|
||||
import com.google.android.apps.mytracks.content.Waypoint;
|
||||
import com.google.android.apps.mytracks.io.file.TrackFileFormat;
|
||||
import com.google.android.apps.mytracks.util.StringUtils;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.content.Context;
|
||||
import android.location.Location;
|
||||
|
||||
import java.io.OutputStream;
|
||||
import java.io.PrintWriter;
|
||||
import java.text.NumberFormat;
|
||||
|
||||
/**
|
||||
* Write track as CSV to a file. See RFC 4180 for info on CSV. Output three
|
||||
* tables.<br>
|
||||
* The first table contains the track info. Its columns are:<br>
|
||||
* "Track name","Activity type","Track description" <br>
|
||||
* <br>
|
||||
* The second table contains the markers. Its columns are:<br>
|
||||
* "Marker name","Marker type","Marker description","Latitude (deg)","Longitude
|
||||
* (deg)","Altitude (m)","Bearing (deg)","Accuracy (m)","Speed (m/s)","Time"<br>
|
||||
* <br>
|
||||
* The thrid table contains the points. Its columns are:<br>
|
||||
* "Segment","Point","Latitude (deg)","Longitude (deg)","Altitude (m)","Bearing
|
||||
* (deg)","Accuracy (m)","Speed (m/s)","Time","Power (W)","Cadence (rpm)","Heart
|
||||
* rate (bpm)","Battery level (%)"<br>
|
||||
*
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
public class CsvTrackWriter implements TrackWriter {
|
||||
|
||||
private static final NumberFormat SHORT_FORMAT = NumberFormat.getInstance();
|
||||
|
||||
static {
|
||||
SHORT_FORMAT.setMaximumFractionDigits(4);
|
||||
}
|
||||
|
||||
private final Context context;
|
||||
private PrintWriter printWriter;
|
||||
private int segmentIndex;
|
||||
private int pointIndex;
|
||||
|
||||
public CsvTrackWriter(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getExtension() {
|
||||
return TrackFileFormat.CSV.getExtension();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void prepare(OutputStream outputStream) {
|
||||
printWriter = new PrintWriter(outputStream);
|
||||
segmentIndex = 0;
|
||||
pointIndex = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (printWriter != null) {
|
||||
printWriter.flush();
|
||||
printWriter = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeHeader(Track 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()),
|
||||
getAltitude(location),
|
||||
getBearing(location),
|
||||
getAccuracy(location),
|
||||
getSpeed(location),
|
||||
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()),
|
||||
getAltitude(location),
|
||||
getBearing(location),
|
||||
getAccuracy(location),
|
||||
getSpeed(location),
|
||||
StringUtils.formatDateTimeIso8601(location.getTime()),
|
||||
power,
|
||||
cadence,
|
||||
heartRate);
|
||||
}
|
||||
|
||||
private String getAltitude(Location location) {
|
||||
return location.hasAltitude() ? Double.toString(location.getAltitude()) : null;
|
||||
}
|
||||
|
||||
private String getBearing(Location location) {
|
||||
return location.hasBearing() ? Double.toString(location.getBearing()) : null;
|
||||
}
|
||||
|
||||
private String getAccuracy(Location location) {
|
||||
return location.hasAccuracy() ? SHORT_FORMAT.format(location.getAccuracy()) : null;
|
||||
}
|
||||
|
||||
private String getSpeed(Location location) {
|
||||
return location.hasSpeed() ? SHORT_FORMAT.format(location.getSpeed()) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a single line of a CSV file.
|
||||
*
|
||||
* @param values the values to be written as CSV
|
||||
*/
|
||||
private void writeCommaSeparatedLine(String... values) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
boolean isFirst = true;
|
||||
for (String value : values) {
|
||||
if (!isFirst) {
|
||||
builder.append(',');
|
||||
}
|
||||
isFirst = false;
|
||||
|
||||
builder.append('"');
|
||||
if (value != null) {
|
||||
builder.append(value.replaceAll("\"", "\"\""));
|
||||
}
|
||||
builder.append('"');
|
||||
}
|
||||
printWriter.println(builder.toString());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
/*
|
||||
* 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.export;
|
||||
|
||||
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.io.file.TrackFileFormat;
|
||||
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 java.io.OutputStream;
|
||||
|
||||
/**
|
||||
* Track Writer for writing tracks to an {@link OutputStream}.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
public class FileTrackExporter extends AbstractTrackExporter {
|
||||
|
||||
private final MyTracksProviderUtils myTracksProviderUtils;
|
||||
private final Track[] tracks;
|
||||
private final TrackWriter trackWriter;
|
||||
private final TrackExporterListener trackExporterListener;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param context the context
|
||||
* @param myTracksProviderUtils the my tracks provider utils
|
||||
* @param tracks the tracks
|
||||
* @param trackFileFormat the track file format
|
||||
* @param trackExporterListener the track export listener
|
||||
*/
|
||||
public FileTrackExporter(MyTracksProviderUtils myTracksProviderUtils,
|
||||
Track[] tracks, TrackFileFormat trackFileFormat, Context context, boolean inZip,
|
||||
TrackExporterListener trackExporterListener) {
|
||||
this(myTracksProviderUtils, tracks, trackFileFormat.newTrackWriter(context, inZip),
|
||||
trackExporterListener);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public FileTrackExporter(MyTracksProviderUtils myTracksProviderUtils, Track[] tracks,
|
||||
TrackWriter trackWriter, TrackExporterListener trackExporterListener) {
|
||||
this.myTracksProviderUtils = myTracksProviderUtils;
|
||||
this.tracks = tracks;
|
||||
this.trackWriter = trackWriter;
|
||||
this.trackExporterListener = trackExporterListener;
|
||||
}
|
||||
|
||||
@Override
|
||||
void performWrite(OutputStream outputStream) throws InterruptedException {
|
||||
trackWriter.prepare(outputStream);
|
||||
trackWriter.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);
|
||||
}
|
||||
trackWriter.writeFooter();
|
||||
trackWriter.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the waypoints.
|
||||
*/
|
||||
private void writeWaypoints(Track track) throws InterruptedException {
|
||||
/*
|
||||
* TODO: Stream through the waypoints in chunks. I am leaving the number of
|
||||
* waypoints very high which should not be a problem because we don't try to
|
||||
* load them into objects all at the same time.
|
||||
*/
|
||||
boolean hasWaypoints = false;
|
||||
Cursor cursor = null;
|
||||
try {
|
||||
cursor = myTracksProviderUtils.getWaypointCursor(
|
||||
track.getId(), -1L, Constants.MAX_LOADED_WAYPOINTS_POINTS);
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
/*
|
||||
* Yes, this will skip the first waypoint and that is intentional as the
|
||||
* first waypoint holds the stats for the track.
|
||||
*/
|
||||
while (cursor.moveToNext()) {
|
||||
if (Thread.interrupted()) {
|
||||
throw new InterruptedException();
|
||||
}
|
||||
if (!hasWaypoints) {
|
||||
trackWriter.writeBeginWaypoints();
|
||||
hasWaypoints = true;
|
||||
}
|
||||
Waypoint waypoint = myTracksProviderUtils.createWaypoint(cursor);
|
||||
trackWriter.writeWaypoint(waypoint);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
if (hasWaypoints) {
|
||||
trackWriter.writeEndWaypoints();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the locations.
|
||||
*/
|
||||
private void writeLocations(Track track, long offset) throws InterruptedException {
|
||||
boolean wroteTrack = false;
|
||||
boolean wroteSegment = false;
|
||||
boolean isLastLocationValid = false;
|
||||
TrackWriterLocationFactory locationFactory = new TrackWriterLocationFactory();
|
||||
LocationIterator iterator = myTracksProviderUtils.getTrackPointLocationIterator(
|
||||
track.getId(), -1L, false, locationFactory);
|
||||
|
||||
try {
|
||||
int locationNumber = 0;
|
||||
while (iterator.hasNext()) {
|
||||
if (Thread.interrupted()) {
|
||||
throw new InterruptedException();
|
||||
}
|
||||
Location location = iterator.next();
|
||||
|
||||
setLocationTime(location, offset);
|
||||
locationNumber++;
|
||||
|
||||
boolean isLocationValid = LocationUtils.isValidLocation(location);
|
||||
boolean isSegmentValid = isLocationValid && isLastLocationValid;
|
||||
if (!wroteTrack && isSegmentValid) {
|
||||
// Found the first two consecutive locations that are valid
|
||||
trackWriter.writeBeginTrack(track, locationFactory.lastLocation);
|
||||
wroteTrack = true;
|
||||
}
|
||||
|
||||
if (isSegmentValid) {
|
||||
if (!wroteSegment) {
|
||||
// Start a segment
|
||||
trackWriter.writeOpenSegment();
|
||||
wroteSegment = true;
|
||||
|
||||
// Write the previous location, which we had previously skipped
|
||||
trackWriter.writeLocation(locationFactory.lastLocation);
|
||||
}
|
||||
|
||||
// Write the current location
|
||||
trackWriter.writeLocation(location);
|
||||
if (trackExporterListener != null) {
|
||||
trackExporterListener.onProgressUpdate(locationNumber, track.getNumberOfPoints());
|
||||
}
|
||||
} else {
|
||||
if (wroteSegment) {
|
||||
trackWriter.writeCloseSegment();
|
||||
wroteSegment = false;
|
||||
}
|
||||
}
|
||||
locationFactory.swapLocations();
|
||||
isLastLocationValid = isLocationValid;
|
||||
}
|
||||
|
||||
if (wroteSegment) {
|
||||
trackWriter.writeCloseSegment();
|
||||
wroteSegment = false;
|
||||
}
|
||||
if (wroteTrack) {
|
||||
Location lastValidTrackPoint = myTracksProviderUtils.getLastValidTrackPoint(track.getId());
|
||||
setLocationTime(lastValidTrackPoint, offset);
|
||||
trackWriter.writeEndTrack(track, lastValidTrackPoint);
|
||||
} else {
|
||||
// Write an empty track
|
||||
trackWriter.writeBeginTrack(track, null);
|
||||
trackWriter.writeEndTrack(track, null);
|
||||
}
|
||||
} finally {
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
/*
|
||||
* 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.export;
|
||||
|
||||
import com.google.android.apps.mytracks.content.Track;
|
||||
import com.google.android.apps.mytracks.content.Waypoint;
|
||||
import com.google.android.apps.mytracks.io.file.TrackFileFormat;
|
||||
import com.google.android.apps.mytracks.util.StringUtils;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.content.Context;
|
||||
import android.location.Location;
|
||||
|
||||
import java.io.OutputStream;
|
||||
import java.io.PrintWriter;
|
||||
import java.text.NumberFormat;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Write track as GPX to a file.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class GpxTrackWriter implements TrackWriter {
|
||||
|
||||
private static final NumberFormat ELEVATION_FORMAT = NumberFormat.getInstance(Locale.US);
|
||||
private static final NumberFormat COORDINATE_FORMAT = NumberFormat.getInstance(Locale.US);
|
||||
static {
|
||||
/*
|
||||
* GPX readers expect to see fractional numbers with US-style punctuation.
|
||||
* That is, they want periods for decimal points, rather than commas.
|
||||
*/
|
||||
ELEVATION_FORMAT.setMaximumFractionDigits(1);
|
||||
ELEVATION_FORMAT.setGroupingUsed(false);
|
||||
|
||||
COORDINATE_FORMAT.setMaximumFractionDigits(6);
|
||||
COORDINATE_FORMAT.setMaximumIntegerDigits(3);
|
||||
COORDINATE_FORMAT.setGroupingUsed(false);
|
||||
}
|
||||
|
||||
private final Context context;
|
||||
private PrintWriter printWriter;
|
||||
|
||||
public GpxTrackWriter(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getExtension() {
|
||||
return TrackFileFormat.GPX.getExtension();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void prepare(OutputStream outputStream) {
|
||||
this.printWriter = new PrintWriter(outputStream);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (printWriter != null) {
|
||||
printWriter.flush();
|
||||
printWriter = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeHeader(Track 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) + ">");
|
||||
if (location.hasAltitude()) {
|
||||
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) + ">");
|
||||
if (location.hasAltitude()) {
|
||||
printWriter.println("<ele>" + ELEVATION_FORMAT.format(location.getAltitude()) + "</ele>");
|
||||
}
|
||||
printWriter.println(
|
||||
"<time>" + StringUtils.formatDateTimeIso8601(location.getTime()) + "</time>");
|
||||
printWriter.println("<name>" + StringUtils.formatCData(waypoint.getName()) + "</name>");
|
||||
printWriter.println("<cmt>" + StringUtils.formatCData(waypoint.getType().name()) + "</cmt>");
|
||||
printWriter.println(
|
||||
"<desc>" + StringUtils.formatCData(waypoint.getDescription()) + "</desc>");
|
||||
printWriter.println("<type>" + StringUtils.formatCData(waypoint.getCategory()) + "</type>");
|
||||
printWriter.println("</wpt>");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,471 @@
|
||||
/*
|
||||
* 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.export;
|
||||
|
||||
import com.google.android.apps.mytracks.content.DescriptionGenerator;
|
||||
import com.google.android.apps.mytracks.content.DescriptionGeneratorImpl;
|
||||
import com.google.android.apps.mytracks.content.MyTracksLocation;
|
||||
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
|
||||
import com.google.android.apps.mytracks.content.Sensor;
|
||||
import com.google.android.apps.mytracks.content.Sensor.SensorData;
|
||||
import com.google.android.apps.mytracks.content.Sensor.SensorDataSet;
|
||||
import com.google.android.apps.mytracks.content.Track;
|
||||
import com.google.android.apps.mytracks.content.Waypoint;
|
||||
import com.google.android.apps.mytracks.content.Waypoint.WaypointType;
|
||||
import com.google.android.apps.mytracks.io.file.TrackFileFormat;
|
||||
import com.google.android.apps.mytracks.util.GoogleEarthUtils;
|
||||
import com.google.android.apps.mytracks.util.StringUtils;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.location.Location;
|
||||
import android.net.Uri;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.OutputStream;
|
||||
import java.io.PrintWriter;
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* Write track as KML to a file.
|
||||
*
|
||||
* @author Leif Hendrik Wilden
|
||||
*/
|
||||
public class KmlTrackWriter implements TrackWriter {
|
||||
|
||||
private static final String WAYPOINT_STYLE = "waypoint";
|
||||
private static final String STATISTICS_STYLE = "statistics";
|
||||
private static final String START_STYLE = "start";
|
||||
private static final String END_STYLE = "end";
|
||||
private static final String TRACK_STYLE = "track";
|
||||
private static final String SCHEMA_ID = "schema";
|
||||
private static final String CADENCE = "cadence";
|
||||
private static final String HEART_RATE = "heart_rate";
|
||||
private static final String POWER = "power";
|
||||
|
||||
private static final String
|
||||
WAYPOINT_ICON = "http://maps.google.com/mapfiles/kml/pushpin/blue-pushpin.png";
|
||||
private static final String
|
||||
STATISTICS_ICON = "http://maps.google.com/mapfiles/kml/pushpin/ylw-pushpin.png";
|
||||
private static final String
|
||||
START_ICON = "http://maps.google.com/mapfiles/kml/paddle/grn-circle.png";
|
||||
private static final String
|
||||
END_ICON = "http://maps.google.com/mapfiles/kml/paddle/red-circle.png";
|
||||
private static final String
|
||||
TRACK_ICON = "http://earth.google.com/images/kml-icons/track-directional/track-0.png";
|
||||
|
||||
private final Context context;
|
||||
private final boolean inKmz;
|
||||
private final DescriptionGenerator descriptionGenerator;
|
||||
private final MyTracksProviderUtils myTracksProviderUtils;
|
||||
|
||||
private PrintWriter printWriter;
|
||||
private ArrayList<Integer> powerList = new ArrayList<Integer>();
|
||||
private ArrayList<Integer> cadenceList = new ArrayList<Integer>();
|
||||
private ArrayList<Integer> heartRateList = new ArrayList<Integer>();
|
||||
private boolean hasPower;
|
||||
private boolean hasCadence;
|
||||
private boolean hasHeartRate;
|
||||
|
||||
public KmlTrackWriter(Context context, boolean inKmz) {
|
||||
this(context, inKmz, new DescriptionGeneratorImpl(context));
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
KmlTrackWriter(Context context, boolean inKmz, DescriptionGenerator descriptionGenerator) {
|
||||
this.context = context;
|
||||
this.inKmz = inKmz;
|
||||
this.descriptionGenerator = descriptionGenerator;
|
||||
this.myTracksProviderUtils = MyTracksProviderUtils.Factory.get(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getExtension() {
|
||||
return TrackFileFormat.KML.getExtension();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void prepare(OutputStream outputStream) {
|
||||
this.printWriter = new PrintWriter(outputStream);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (printWriter != null) {
|
||||
printWriter.flush();
|
||||
printWriter = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeHeader(Track 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;
|
||||
String photoUrl = waypoint.getPhotoUrl();
|
||||
if (photoUrl != null && !photoUrl.equals("")) {
|
||||
float heading = getHeading(waypoint.getTrackId(), waypoint.getLocation());
|
||||
writePhotoOverlay(waypoint.getName(), waypoint.getCategory(), waypoint.getDescription(),
|
||||
styleName, waypoint.getLocation(), photoUrl, heading);
|
||||
} else {
|
||||
writePlacemark(waypoint.getName(), waypoint.getCategory(), waypoint.getDescription(),
|
||||
styleName, waypoint.getLocation());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void 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>" + getCoordinates(location, " ") + "</gx:coord>");
|
||||
if (location instanceof MyTracksLocation) {
|
||||
SensorDataSet sensorDataSet = ((MyTracksLocation) location).getSensorDataSet();
|
||||
int power = -1;
|
||||
int cadence = -1;
|
||||
int heartRate = -1;
|
||||
|
||||
if (sensorDataSet != null) {
|
||||
if (sensorDataSet.hasPower()) {
|
||||
SensorData sensorData = sensorDataSet.getPower();
|
||||
if (sensorData.hasValue() && sensorData.getState() == Sensor.SensorState.SENDING) {
|
||||
hasPower = true;
|
||||
power = sensorData.getValue();
|
||||
}
|
||||
}
|
||||
if (sensorDataSet.hasCadence()) {
|
||||
SensorData sensorData = sensorDataSet.getCadence();
|
||||
if (sensorData.hasValue() && sensorData.getState() == Sensor.SensorState.SENDING) {
|
||||
hasCadence = true;
|
||||
cadence = sensorData.getValue();
|
||||
}
|
||||
}
|
||||
if (sensorDataSet.hasHeartRate()) {
|
||||
SensorData sensorData = sensorDataSet.getHeartRate();
|
||||
if (sensorData.hasValue() && sensorData.getState() == Sensor.SensorState.SENDING) {
|
||||
hasHeartRate = true;
|
||||
heartRate = sensorData.getValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
powerList.add(power);
|
||||
cadenceList.add(cadence);
|
||||
heartRateList.add(heartRate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the sensor data.
|
||||
*
|
||||
* @param list a list of sensor data
|
||||
* @param name the name of the sensor data
|
||||
*/
|
||||
private void writeSensorData(ArrayList<Integer> list, String name) {
|
||||
printWriter.println("<gx:SimpleArrayData name=\"" + name + "\">");
|
||||
for (int i = 0; i < list.size(); i++) {
|
||||
printWriter.println("<gx:value>" + list.get(i) + "</gx:value>");
|
||||
}
|
||||
printWriter.println("</gx:SimpleArrayData>");
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a placemark.
|
||||
*
|
||||
* @param name the name
|
||||
* @param category the category
|
||||
* @param description the description
|
||||
* @param styleName the style name
|
||||
* @param location the location
|
||||
*/
|
||||
private void writePlacemark(
|
||||
String name, String category, String description, String styleName, Location location) {
|
||||
if (location != null) {
|
||||
printWriter.println("<Placemark>");
|
||||
printWriter.println("<name>" + StringUtils.formatCData(name) + "</name>");
|
||||
printWriter.println(
|
||||
"<description>" + StringUtils.formatCData(description) + "</description>");
|
||||
printWriter.println("<TimeStamp><when>"
|
||||
+ StringUtils.formatDateTimeIso8601(location.getTime()) + "</when></TimeStamp>");
|
||||
printWriter.println("<styleUrl>#" + styleName + "</styleUrl>");
|
||||
writeCategory(category);
|
||||
printWriter.println("<Point>");
|
||||
printWriter.println("<coordinates>" + getCoordinates(location, ",") + "</coordinates>");
|
||||
printWriter.println("</Point>");
|
||||
printWriter.println("</Placemark>");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a photo overlay.
|
||||
*
|
||||
* @param name the name
|
||||
* @param category the category
|
||||
* @param description the description
|
||||
* @param styleName the style name
|
||||
* @param location the location
|
||||
* @param photoUrl the photo url
|
||||
* @param heading the heading
|
||||
*/
|
||||
private void writePhotoOverlay(String name, String category, String description, String styleName,
|
||||
Location location, String photoUrl, float heading) {
|
||||
if (location != null) {
|
||||
printWriter.println("<PhotoOverlay>");
|
||||
printWriter.println("<name>" + StringUtils.formatCData(name) + "</name>");
|
||||
printWriter.println(
|
||||
"<description>" + StringUtils.formatCData(description) + "</description>");
|
||||
printWriter.print("<Camera>");
|
||||
printWriter.print("<longitude>" + location.getLongitude() + "</longitude>");
|
||||
printWriter.print("<latitude>" + location.getLatitude() + "</latitude>");
|
||||
printWriter.print("<altitude>20</altitude>");
|
||||
printWriter.print("<heading>" + heading + "</heading>");
|
||||
printWriter.print("<tilt>90</tilt>");
|
||||
printWriter.println("</Camera>");
|
||||
printWriter.println("<TimeStamp><when>"
|
||||
+ StringUtils.formatDateTimeIso8601(location.getTime()) + "</when></TimeStamp>");
|
||||
printWriter.println("<styleUrl>#" + styleName + "</styleUrl>");
|
||||
writeCategory(category);
|
||||
if (inKmz) {
|
||||
Uri uri = Uri.parse(photoUrl);
|
||||
printWriter.println("<Icon><href>" + KmzTrackExporter.KMZ_IMAGES_DIR + File.separatorChar
|
||||
+ uri.getLastPathSegment() + "</href></Icon>");
|
||||
} else {
|
||||
printWriter.println("<Icon><href>" + Uri.decode(photoUrl) + "</href></Icon>");
|
||||
}
|
||||
|
||||
printWriter.print("<ViewVolume>");
|
||||
printWriter.print("<near>10</near>");
|
||||
printWriter.print("<leftFov>-60</leftFov>");
|
||||
printWriter.print("<rightFov>60</rightFov>");
|
||||
printWriter.print("<bottomFov>-45</bottomFov>");
|
||||
printWriter.print("<topFov>45</topFov>");
|
||||
printWriter.println("</ViewVolume>");
|
||||
printWriter.println("<Point>");
|
||||
printWriter.println("<coordinates>" + getCoordinates(location, ",") + "</coordinates>");
|
||||
printWriter.println("</Point>");
|
||||
printWriter.println("</PhotoOverlay>");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the heading to a location.
|
||||
*
|
||||
* @param trackId the track id containing the location
|
||||
* @param location the location
|
||||
*/
|
||||
private float getHeading(long trackId, Location location) {
|
||||
long trackPointId = myTracksProviderUtils.getTrackPointId(trackId, location);
|
||||
if (trackPointId == -1L) {
|
||||
return location.getBearing();
|
||||
}
|
||||
Cursor cursor = null;
|
||||
Location viewLocation;
|
||||
try {
|
||||
cursor = myTracksProviderUtils.getTrackPointCursor(trackId, trackPointId, 10, true);
|
||||
if (cursor == null || cursor.getCount() == 0) {
|
||||
return location.getBearing();
|
||||
}
|
||||
cursor.moveToPosition(cursor.getCount() - 1);
|
||||
viewLocation = myTracksProviderUtils.createTrackPoint(cursor);
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
return viewLocation.bearingTo(location);
|
||||
}
|
||||
|
||||
private String getCoordinates(Location location, String separator) {
|
||||
StringBuffer buffer = new StringBuffer();
|
||||
buffer.append(location.getLongitude() + separator + location.getLatitude());
|
||||
if (location.hasAltitude()) {
|
||||
buffer.append(separator + location.getAltitude());
|
||||
}
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the category.
|
||||
*
|
||||
* @param category the category
|
||||
*/
|
||||
private void writeCategory(String category) {
|
||||
if (category == null || category.equals("")) {
|
||||
return;
|
||||
}
|
||||
printWriter.println("<ExtendedData>");
|
||||
printWriter.println(
|
||||
"<Data name=\"type\"><value>" + StringUtils.formatCData(category) + "</value></Data>");
|
||||
printWriter.println("</ExtendedData>");
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the track style.
|
||||
*/
|
||||
private void writeTrackStyle() {
|
||||
printWriter.println("<Style id=\"" + TRACK_STYLE + "\">");
|
||||
printWriter.println("<LineStyle><color>7f0000ff</color><width>4</width></LineStyle>");
|
||||
printWriter.println("<IconStyle>");
|
||||
printWriter.println("<scale>1.3</scale>");
|
||||
printWriter.println("<Icon><href>" + TRACK_ICON + "</href></Icon>");
|
||||
printWriter.println("</IconStyle>");
|
||||
printWriter.println("</Style>");
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a placemarker style.
|
||||
*
|
||||
* @param name the name of the style
|
||||
* @param url the url of the style icon
|
||||
* @param x the x position of the hotspot
|
||||
* @param y the y position of the hotspot
|
||||
*/
|
||||
private void writePlacemarkerStyle(String name, String url, int x, int y) {
|
||||
printWriter.println("<Style id=\"" + name + "\"><IconStyle>");
|
||||
printWriter.println("<scale>1.3</scale>");
|
||||
printWriter.println("<Icon><href>" + url + "</href></Icon>");
|
||||
printWriter.println(
|
||||
"<hotSpot x=\"" + x + "\" y=\"" + y + "\" xunits=\"pixels\" yunits=\"pixels\"/>");
|
||||
printWriter.println("</IconStyle></Style>");
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a sensor style.
|
||||
*
|
||||
* @param name the name of the sesnor
|
||||
* @param displayName the sensor display name
|
||||
*/
|
||||
private void writeSensorStyle(String name, String displayName) {
|
||||
printWriter.println("<gx:SimpleArrayField name=\"" + name + "\" type=\"int\">");
|
||||
printWriter.println("<displayName>" + StringUtils.formatCData(displayName) + "</displayName>");
|
||||
printWriter.println("</gx:SimpleArrayField>");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,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.io.file.export;
|
||||
|
||||
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 android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
/**
|
||||
* KMZ track exporter.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class KmzTrackExporter extends AbstractTrackExporter {
|
||||
|
||||
public static final String KMZ_EXTENSION = "kmz";
|
||||
public static final String KMZ_IMAGES_DIR = "images";
|
||||
public static final String KMZ_KML_FILE = "doc.kml";
|
||||
private static final int BUFFER_SIZE = 4096;
|
||||
|
||||
private final MyTracksProviderUtils myTracksProviderUtils;
|
||||
private final FileTrackExporter fileTrackExporter;
|
||||
private final Track[] tracks;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param myTracksProviderUtils the my tracks provider utils
|
||||
* @param fileTrackExporter the file track exporter
|
||||
* @param tracks the tracks to export
|
||||
*/
|
||||
public KmzTrackExporter(MyTracksProviderUtils myTracksProviderUtils,
|
||||
FileTrackExporter fileTrackExporter, Track[] tracks) {
|
||||
this.myTracksProviderUtils = myTracksProviderUtils;
|
||||
this.fileTrackExporter = fileTrackExporter;
|
||||
this.tracks = tracks;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSuccess() {
|
||||
return fileTrackExporter.isSuccess() && super.isSuccess();
|
||||
}
|
||||
|
||||
@Override
|
||||
void performWrite(OutputStream outputStream) throws InterruptedException, IOException {
|
||||
ZipOutputStream zipOutputStream = null;
|
||||
try {
|
||||
zipOutputStream = new ZipOutputStream(outputStream);
|
||||
|
||||
// Add kml file
|
||||
ZipEntry zipEntry = new ZipEntry(KMZ_KML_FILE);
|
||||
zipOutputStream.putNextEntry(zipEntry);
|
||||
|
||||
fileTrackExporter.writeTrack(zipOutputStream);
|
||||
zipOutputStream.closeEntry();
|
||||
if (!fileTrackExporter.isSuccess()) {
|
||||
throw new IOException();
|
||||
}
|
||||
|
||||
// Add photos
|
||||
addImages(zipOutputStream);
|
||||
} finally {
|
||||
if (zipOutputStream != null) {
|
||||
zipOutputStream.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void addImages(ZipOutputStream zipOutputStream) throws InterruptedException, IOException {
|
||||
for (Track track : tracks) {
|
||||
Cursor cursor = null;
|
||||
try {
|
||||
cursor = myTracksProviderUtils.getWaypointCursor(track.getId(), -1L, -1);
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
/*
|
||||
* Yes, this will skip the first waypoint and that is intentional as
|
||||
* the first waypoint holds the stats for the track.
|
||||
*/
|
||||
while (cursor.moveToNext()) {
|
||||
if (Thread.interrupted()) {
|
||||
throw new InterruptedException();
|
||||
}
|
||||
Waypoint waypoint = myTracksProviderUtils.createWaypoint(cursor);
|
||||
String photoUrl = waypoint.getPhotoUrl();
|
||||
if (photoUrl != null && !photoUrl.equals("")) {
|
||||
addImage(zipOutputStream, photoUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void addImage(ZipOutputStream zipOutputStream, String photoUrl) throws IOException {
|
||||
Uri uri = Uri.parse(photoUrl);
|
||||
ZipEntry zipEntry = new ZipEntry(
|
||||
KMZ_IMAGES_DIR + File.separatorChar + uri.getLastPathSegment());
|
||||
zipOutputStream.putNextEntry(zipEntry);
|
||||
|
||||
FileInputStream fileInputStream = null;
|
||||
try {
|
||||
fileInputStream = new FileInputStream(new File(uri.getPath()));
|
||||
byte[] buffer = new byte[BUFFER_SIZE];
|
||||
int byteCount = 0;
|
||||
while ((byteCount = fileInputStream.read(buffer)) != -1) {
|
||||
zipOutputStream.write(buffer, 0, byteCount);
|
||||
}
|
||||
} finally {
|
||||
if (fileInputStream != null) {
|
||||
fileInputStream.close();
|
||||
}
|
||||
}
|
||||
zipOutputStream.closeEntry();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
/*
|
||||
* 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.export;
|
||||
|
||||
import com.google.android.apps.mytracks.io.file.TrackFileFormat;
|
||||
import com.google.android.apps.mytracks.util.DialogUtils;
|
||||
import com.google.android.apps.mytracks.util.FileUtils;
|
||||
import com.google.android.apps.mytracks.util.GoogleEarthUtils;
|
||||
import com.google.android.apps.mytracks.util.IntentUtils;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.app.Dialog;
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.widget.Toast;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* An activity for saving tracks to the external storage. If saving a specific
|
||||
* track, option to save it to a temp directory and play the track afterward.
|
||||
*
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
public class SaveActivity extends Activity {
|
||||
|
||||
public static final String EXTRA_TRACK_FILE_FORMAT = "track_file_format";
|
||||
public static final String EXTRA_TRACK_IDS = "track_ids";
|
||||
public static final String EXTRA_PLAY_TRACK = "play_track";
|
||||
|
||||
private static final int DIALOG_PROGRESS_ID = 0;
|
||||
private static final int DIALOG_RESULT_ID = 1;
|
||||
|
||||
private TrackFileFormat trackFileFormat;
|
||||
private long[] trackIds;
|
||||
private boolean playTrack;
|
||||
private String directoryDisplayName;
|
||||
|
||||
private SaveAsyncTask saveAsyncTask;
|
||||
private ProgressDialog progressDialog;
|
||||
|
||||
// the number of tracks successfully saved
|
||||
private int successCount;
|
||||
|
||||
// the number of tracks to save
|
||||
private int totalCount;
|
||||
|
||||
// the last successfully saved path
|
||||
private String savedPath;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
Intent intent = getIntent();
|
||||
trackFileFormat = intent.getParcelableExtra(EXTRA_TRACK_FILE_FORMAT);
|
||||
trackIds = intent.getLongArrayExtra(EXTRA_TRACK_IDS);
|
||||
playTrack = intent.getBooleanExtra(EXTRA_PLAY_TRACK, false);
|
||||
|
||||
if (!FileUtils.isExternalStorageWriteable()) {
|
||||
Toast.makeText(this, R.string.external_storage_not_writable, Toast.LENGTH_LONG).show();
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
directoryDisplayName = playTrack ? FileUtils.getDirectoryDisplayName(
|
||||
trackFileFormat.getExtension(), FileUtils.TEMP_DIR)
|
||||
: FileUtils.getDirectoryDisplayName(trackFileFormat.getExtension());
|
||||
|
||||
String directoryPath = playTrack ? FileUtils.getDirectoryPath(
|
||||
trackFileFormat.getExtension(), FileUtils.TEMP_DIR)
|
||||
: FileUtils.getDirectoryPath(trackFileFormat.getExtension());
|
||||
File directory = new File(directoryPath);
|
||||
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, playTrack);
|
||||
saveAsyncTask.execute();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object onRetainNonConfigurationInstance() {
|
||||
saveAsyncTask.setActivity(null);
|
||||
return saveAsyncTask;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Dialog onCreateDialog(int id) {
|
||||
switch (id) {
|
||||
case DIALOG_PROGRESS_ID:
|
||||
progressDialog = DialogUtils.createHorizontalProgressDialog(this,
|
||||
R.string.export_external_storage_progress_message,
|
||||
new DialogInterface.OnCancelListener() {
|
||||
@Override
|
||||
public void onCancel(DialogInterface dialog) {
|
||||
saveAsyncTask.cancel(true);
|
||||
dialog.dismiss();
|
||||
}
|
||||
}, directoryDisplayName);
|
||||
return progressDialog;
|
||||
case DIALOG_RESULT_ID:
|
||||
int iconId;
|
||||
int titleId;
|
||||
String message;
|
||||
String totalTracks = getResources()
|
||||
.getQuantityString(R.plurals.tracks, totalCount, totalCount);
|
||||
if (successCount == totalCount) {
|
||||
if (totalCount == 0) {
|
||||
iconId = android.R.drawable.ic_dialog_info;
|
||||
titleId = R.string.export_external_storage_no_track_title;
|
||||
message = getString(R.string.export_external_storage_no_track);
|
||||
} else {
|
||||
iconId = R.drawable.ic_dialog_success;
|
||||
titleId = R.string.generic_success_title;
|
||||
message = getString(
|
||||
R.string.export_external_storage_success, totalTracks, directoryDisplayName);
|
||||
}
|
||||
} else {
|
||||
iconId = android.R.drawable.ic_dialog_alert;
|
||||
titleId = R.string.generic_error_title;
|
||||
message = getString(R.string.export_external_storage_error, successCount, totalTracks,
|
||||
directoryDisplayName);
|
||||
}
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(this).setCancelable(true)
|
||||
.setIcon(iconId).setMessage(message)
|
||||
.setOnCancelListener(new DialogInterface.OnCancelListener() {
|
||||
@Override
|
||||
public void onCancel(DialogInterface dialog) {
|
||||
dialog.dismiss();
|
||||
finish();
|
||||
}
|
||||
}).setPositiveButton(R.string.generic_ok, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int arg1) {
|
||||
dialog.dismiss();
|
||||
finish();
|
||||
}
|
||||
}).setTitle(titleId);
|
||||
if (!playTrack && trackIds.length == 1 && trackIds[0] != -1L && successCount == totalCount
|
||||
&& savedPath != null) {
|
||||
builder.setNegativeButton(
|
||||
R.string.share_track_share_file, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
Intent intent = IntentUtils.newShareFileIntent(
|
||||
SaveActivity.this, trackIds[0], savedPath, trackFileFormat);
|
||||
startActivity(
|
||||
Intent.createChooser(intent, getString(R.string.share_track_picker_title)));
|
||||
finish();
|
||||
}
|
||||
});
|
||||
}
|
||||
return builder.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 aSavedPath the last successfully saved path
|
||||
*/
|
||||
public void onAsyncTaskCompleted(int aSuccessCount, int aTotalCount, String aSavedPath) {
|
||||
successCount = aSuccessCount;
|
||||
totalCount = aTotalCount;
|
||||
savedPath = aSavedPath;
|
||||
removeDialog(DIALOG_PROGRESS_ID);
|
||||
if (playTrack && successCount == 1 && totalCount == 1 && savedPath != null) {
|
||||
startActivity(GoogleEarthUtils.getPlayInEarthIntent(savedPath));
|
||||
finish();
|
||||
} else {
|
||||
showDialog(DIALOG_RESULT_ID);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the progress dialog.
|
||||
*/
|
||||
public void showProgressDialog() {
|
||||
showDialog(DIALOG_PROGRESS_ID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the progress dialog value.
|
||||
*
|
||||
* @param number the number of points saved
|
||||
* @param max the maximum number of points
|
||||
*/
|
||||
public void setProgressDialogValue(int number, int max) {
|
||||
if (progressDialog != null) {
|
||||
progressDialog.setIndeterminate(false);
|
||||
progressDialog.setMax(max);
|
||||
progressDialog.setProgress(Math.min(number, max));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
/*
|
||||
* 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.export;
|
||||
|
||||
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
|
||||
import com.google.android.apps.mytracks.content.Track;
|
||||
import com.google.android.apps.mytracks.content.TracksColumns;
|
||||
import com.google.android.apps.mytracks.io.file.TrackFileFormat;
|
||||
import com.google.android.apps.mytracks.util.FileUtils;
|
||||
import com.google.android.apps.mytracks.util.PreferencesUtils;
|
||||
import com.google.android.apps.mytracks.util.SystemUtils;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.PowerManager.WakeLock;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Async Task to save tracks to the external storage.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class SaveAsyncTask extends AsyncTask<Void, Integer, Boolean> {
|
||||
|
||||
private static final String TAG = SaveAsyncTask.class.getSimpleName();
|
||||
|
||||
private SaveActivity saveActivity;
|
||||
private final TrackFileFormat trackFileFormat;
|
||||
private final long[] trackIds;
|
||||
private final File directory;
|
||||
private final boolean playTrack;
|
||||
private final Context context;
|
||||
private final MyTracksProviderUtils myTracksProviderUtils;
|
||||
|
||||
private WakeLock wakeLock;
|
||||
private TrackExporter trackExporter;
|
||||
|
||||
// true if the AsyncTask has completed
|
||||
private boolean completed;
|
||||
|
||||
// the number of tracks successfully saved
|
||||
private int successCount;
|
||||
|
||||
// the number of tracks to save
|
||||
private int totalCount;
|
||||
|
||||
// the last successfully saved path
|
||||
private String savedPath;
|
||||
|
||||
/**
|
||||
* Creates an AsyncTask.
|
||||
*
|
||||
* @param saveActivity the activity currently associated with this task
|
||||
* @param trackFileFormat the track file format
|
||||
* @param trackIds the track ids to save. To save all, set to size 1 with
|
||||
* trackIds[0] == -1L
|
||||
* @param directory the directory to save to
|
||||
* @param playTrack true to play track
|
||||
*/
|
||||
public SaveAsyncTask(SaveActivity saveActivity, TrackFileFormat trackFileFormat, long[] trackIds,
|
||||
File directory, boolean playTrack) {
|
||||
this.saveActivity = saveActivity;
|
||||
this.trackFileFormat = trackFileFormat;
|
||||
this.trackIds = trackIds;
|
||||
this.directory = directory;
|
||||
this.playTrack = playTrack;
|
||||
context = saveActivity.getApplicationContext();
|
||||
myTracksProviderUtils = MyTracksProviderUtils.Factory.get(context);
|
||||
|
||||
completed = false;
|
||||
successCount = 0;
|
||||
totalCount = 0;
|
||||
savedPath = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current activity associated with this AyncTask.
|
||||
*
|
||||
* @param saveActivity the current activity, can be null
|
||||
*/
|
||||
public void setActivity(SaveActivity saveActivity) {
|
||||
this.saveActivity = saveActivity;
|
||||
if (completed && saveActivity != null) {
|
||||
saveActivity.onAsyncTaskCompleted(successCount, totalCount, savedPath);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
if (saveActivity != null) {
|
||||
saveActivity.showProgressDialog();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Boolean doInBackground(Void... params) {
|
||||
try {
|
||||
boolean isRecording = PreferencesUtils.getLong(saveActivity, R.string.recording_track_id_key)
|
||||
!= PreferencesUtils.RECORDING_TRACK_ID_DEFAULT;
|
||||
boolean isPaused = PreferencesUtils.getBoolean(saveActivity,
|
||||
R.string.recording_track_paused_key, PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT);
|
||||
// Get the wake lock if not recording or paused
|
||||
if (!isRecording || isPaused) {
|
||||
wakeLock = SystemUtils.acquireWakeLock(saveActivity, wakeLock);
|
||||
}
|
||||
if (trackIds.length == 1 && trackIds[0] == -1L) {
|
||||
return saveAllTracks();
|
||||
} else {
|
||||
totalCount = 1;
|
||||
Track[] tracks = new Track[trackIds.length];
|
||||
for (int i = 0; i < trackIds.length; i++) {
|
||||
tracks[i] = myTracksProviderUtils.getTrack(trackIds[i]);
|
||||
if (tracks[i] == null) {
|
||||
Log.d(TAG, "No track for " + trackIds[i]);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (saveTracks(tracks)) {
|
||||
successCount = 1;
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (wakeLock != null && wakeLock.isHeld()) {
|
||||
wakeLock.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onProgressUpdate(Integer... values) {
|
||||
if (saveActivity != null) {
|
||||
saveActivity.setProgressDialogValue(values[0], values[1]);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Boolean result) {
|
||||
completed = true;
|
||||
if (saveActivity != null) {
|
||||
saveActivity.onAsyncTaskCompleted(successCount, totalCount, savedPath);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCancelled() {
|
||||
completed = true;
|
||||
if (saveActivity != null) {
|
||||
saveActivity.onAsyncTaskCompleted(successCount, totalCount, null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves tracks to one file.
|
||||
*
|
||||
* @param tracks the tracks
|
||||
*/
|
||||
private Boolean saveTracks(Track[] tracks) {
|
||||
if (tracks.length == 0) {
|
||||
return false;
|
||||
}
|
||||
Track track = tracks[0];
|
||||
boolean useKmz = !playTrack && trackFileFormat == TrackFileFormat.KML;
|
||||
String extension = useKmz ? KmzTrackExporter.KMZ_EXTENSION : trackFileFormat.getExtension();
|
||||
|
||||
// Make sure the file doesn't exist yet (possibly by changing the filename)
|
||||
String fileName = FileUtils.buildUniqueFileName(directory, track.getName(), extension);
|
||||
if (fileName == null) {
|
||||
Log.d(TAG, "Unable to get a unique filename for " + track.getName());
|
||||
return false;
|
||||
}
|
||||
|
||||
FileTrackExporter fileTrackExporter = new FileTrackExporter(myTracksProviderUtils, tracks,
|
||||
trackFileFormat, context, useKmz, new TrackExporterListener() {
|
||||
|
||||
@Override
|
||||
public void onProgressUpdate(int number, int max) {
|
||||
/*
|
||||
* If only saving one track, update the progress dialog once every
|
||||
* 500 points
|
||||
*/
|
||||
if (trackIds.length == 1 && trackIds[0] != -1L && number % 500 == 0) {
|
||||
publishProgress(number, max);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
trackExporter = useKmz ? new KmzTrackExporter(myTracksProviderUtils, fileTrackExporter, tracks)
|
||||
: fileTrackExporter;
|
||||
|
||||
File file = new File(directory, fileName);
|
||||
FileOutputStream fileOutputStream = null;
|
||||
try {
|
||||
fileOutputStream = new FileOutputStream(file);
|
||||
trackExporter.writeTrack(fileOutputStream);
|
||||
|
||||
if (trackExporter.isSuccess()) {
|
||||
savedPath = file.getAbsolutePath();
|
||||
return true;
|
||||
} else {
|
||||
if (!file.delete()) {
|
||||
Log.w(TAG, "Failed to delete file " + file.getAbsolutePath());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
} catch (FileNotFoundException e) {
|
||||
Log.e(TAG, "Unable to open file " + file.getName(), e);
|
||||
return false;
|
||||
} finally {
|
||||
if (fileOutputStream != null) {
|
||||
try {
|
||||
fileOutputStream.close();
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Unable to close file output stream", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves all the tracks.
|
||||
*/
|
||||
private Boolean saveAllTracks() {
|
||||
Cursor cursor = null;
|
||||
try {
|
||||
cursor = myTracksProviderUtils.getTrackCursor(null, null, TracksColumns._ID);
|
||||
if (cursor == null) {
|
||||
return false;
|
||||
}
|
||||
totalCount = cursor.getCount();
|
||||
for (int i = 0; i < totalCount; i++) {
|
||||
if (isCancelled()) {
|
||||
return false;
|
||||
}
|
||||
cursor.moveToPosition(i);
|
||||
Track track = myTracksProviderUtils.createTrack(cursor);
|
||||
if (track != null && saveTracks(new Track[] { track })) {
|
||||
successCount++;
|
||||
}
|
||||
publishProgress(i + 1, totalCount);
|
||||
}
|
||||
return true;
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,330 @@
|
||||
/*
|
||||
* 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.export;
|
||||
|
||||
import com.google.android.apps.mytracks.content.MyTracksLocation;
|
||||
import com.google.android.apps.mytracks.content.Sensor;
|
||||
import com.google.android.apps.mytracks.content.Sensor.SensorDataSet;
|
||||
import com.google.android.apps.mytracks.content.Track;
|
||||
import com.google.android.apps.mytracks.content.Waypoint;
|
||||
import com.google.android.apps.mytracks.io.file.TrackFileFormat;
|
||||
import com.google.android.apps.mytracks.util.StringUtils;
|
||||
import com.google.android.apps.mytracks.util.SystemUtils;
|
||||
import com.google.android.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 TrackWriter {
|
||||
|
||||
/**
|
||||
* TCX sport type. See the TCX spec.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
private enum SportType {
|
||||
RUNNING("Running"),
|
||||
BIKING("Biking"),
|
||||
OTHER("Other");
|
||||
|
||||
private final String name;
|
||||
|
||||
private SportType(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the name of the sport type
|
||||
*/
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
// My Tracks categories that are considered as TCX biking sport type.
|
||||
private static final int TCX_SPORT_BIKING_IDS[] = {
|
||||
R.string.activity_type_cycling,
|
||||
R.string.activity_type_dirt_bike,
|
||||
R.string.activity_type_mountain_biking,
|
||||
R.string.activity_type_road_biking,
|
||||
R.string.activity_type_track_cycling };
|
||||
|
||||
// My Tracks categories that are considered as TCX running sport type.
|
||||
private static final int TCX_SPORT_RUNNING_IDS[] = {
|
||||
R.string.activity_type_running,
|
||||
R.string.activity_type_speed_walking,
|
||||
R.string.activity_type_street_running,
|
||||
R.string.activity_type_track_running,
|
||||
R.string.activity_type_trail_running,
|
||||
R.string.activity_type_walking };
|
||||
|
||||
private final Context context;
|
||||
private PrintWriter printWriter;
|
||||
private SportType sportType;
|
||||
|
||||
public TcxTrackWriter(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void prepare(OutputStream outputStream) {
|
||||
this.printWriter = new PrintWriter(outputStream);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (printWriter != null) {
|
||||
printWriter.flush();
|
||||
printWriter = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getExtension() {
|
||||
return TrackFileFormat.TCX.getExtension();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeHeader(Track 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>");
|
||||
if (location.hasAltitude()) {
|
||||
printWriter.println("<AltitudeMeters>" + location.getAltitude() + "</AltitudeMeters>");
|
||||
}
|
||||
|
||||
if (location instanceof MyTracksLocation) {
|
||||
SensorDataSet sensorDataSet = ((MyTracksLocation) location).getSensorDataSet();
|
||||
if (sensorDataSet != null) {
|
||||
boolean heartRateAvailable = sensorDataSet.hasHeartRate()
|
||||
&& sensorDataSet.getHeartRate().hasValue()
|
||||
&& sensorDataSet.getHeartRate().getState() == Sensor.SensorState.SENDING;
|
||||
boolean cadenceAvailable = sensorDataSet.hasCadence()
|
||||
&& sensorDataSet.getCadence().hasValue()
|
||||
&& sensorDataSet.getCadence().getState() == Sensor.SensorState.SENDING;
|
||||
boolean powerAvailable = sensorDataSet.hasPower()
|
||||
&& sensorDataSet.getPower().hasValue()
|
||||
&& sensorDataSet.getPower().getState() == Sensor.SensorState.SENDING;
|
||||
|
||||
if (heartRateAvailable) {
|
||||
printWriter.println("<HeartRateBpm>");
|
||||
printWriter.println("<Value>" + sensorDataSet.getHeartRate().getValue() + "</Value>");
|
||||
printWriter.println("</HeartRateBpm>");
|
||||
}
|
||||
|
||||
// <Cadence> needs to be put before <Extensions>.
|
||||
// According to the TCX spec, <Cadence> is only for the biking sport
|
||||
// type. For others, use <RunCadence> in <Extensions>.
|
||||
if (cadenceAvailable && sportType == SportType.BIKING) {
|
||||
// The spec requires the max value be 254.
|
||||
printWriter.println(
|
||||
"<Cadence>" + Math.min(254, sensorDataSet.getCadence().getValue()) + "</Cadence>");
|
||||
}
|
||||
|
||||
if ((cadenceAvailable && sportType != SportType.BIKING) || powerAvailable) {
|
||||
printWriter.println("<Extensions>");
|
||||
printWriter.println(
|
||||
"<TPX xmlns=\"http://www.garmin.com/xmlschemas/ActivityExtension/v2\">");
|
||||
|
||||
// <RunCadence> needs to be put before <Watts>.
|
||||
if (cadenceAvailable && sportType != SportType.BIKING) {
|
||||
// The spec requires the max value to be 254.
|
||||
printWriter.println("<RunCadence>"
|
||||
+ Math.min(254, sensorDataSet.getCadence().getValue()) + "</RunCadence>");
|
||||
}
|
||||
|
||||
if (powerAvailable) {
|
||||
printWriter.println("<Watts>" + sensorDataSet.getPower().getValue() + "</Watts>");
|
||||
}
|
||||
printWriter.println("</TPX>");
|
||||
printWriter.println("</Extensions>");
|
||||
}
|
||||
}
|
||||
}
|
||||
printWriter.println("</Trackpoint>");
|
||||
}
|
||||
}
|
||||
|
||||
@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,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.file.export;
|
||||
|
||||
import java.io.OutputStream;
|
||||
|
||||
/**
|
||||
* Track exporting for exporting track to an {@link OutputStream}.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public interface TrackExporter {
|
||||
|
||||
/**
|
||||
* Write track to an output stream
|
||||
*
|
||||
* @param outputStream the output stream
|
||||
*/
|
||||
void writeTrack(OutputStream outputStream);
|
||||
|
||||
/**
|
||||
* Returns true if {@link #writeTrack(OutputStream)} completed successfully.
|
||||
*/
|
||||
boolean isSuccess();
|
||||
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright 2010 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package com.google.android.apps.mytracks.io.file.export;
|
||||
|
||||
/**
|
||||
* Listener for {@link TrackExporter} progress.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public interface TrackExporterListener {
|
||||
|
||||
/**
|
||||
* Called to update progress.
|
||||
*
|
||||
* @param number the number of locations written
|
||||
* @param max the maximum number of locations in a track, for calculation of
|
||||
* completion percentage
|
||||
*/
|
||||
public void onProgressUpdate(int number, int max);
|
||||
}
|
||||
@@ -0,0 +1,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.export;
|
||||
|
||||
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 TrackWriter {
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
+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.export_google_fusion_tables);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void startNextActivity(boolean success, boolean isCancel) {
|
||||
sendRequest.setFusionTablesSuccess(success);
|
||||
Class<?> next = getNextClass(sendRequest, isCancel);
|
||||
Intent intent = IntentUtils.newIntent(this, next)
|
||||
.putExtra(SendRequest.SEND_REQUEST_KEY, sendRequest);
|
||||
startActivity(intent);
|
||||
finish();
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
Class<?> getNextClass(SendRequest request, boolean isCancel) {
|
||||
if (isCancel) {
|
||||
return UploadResultActivity.class;
|
||||
} else {
|
||||
if (request.isSendSpreadsheets()) {
|
||||
return SendSpreadsheetsActivity.class;
|
||||
} else {
|
||||
return UploadResultActivity.class;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+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 boolean performTask() {
|
||||
try {
|
||||
// Reset the per upload states
|
||||
currentSegment = 1;
|
||||
|
||||
GoogleAccountCredential credential = SendToGoogleUtils.getGoogleAccountCredential(
|
||||
context, account.name, SendToGoogleUtils.FUSION_TABLES_SCOPE);
|
||||
if (credential == null) {
|
||||
return false;
|
||||
}
|
||||
Fusiontables fusiontables = new Fusiontables.Builder(
|
||||
AndroidHttp.newCompatibleTransport(), new GsonFactory(), credential).build();
|
||||
|
||||
Track track = myTracksProviderUtils.getTrack(trackId);
|
||||
if (track == null) {
|
||||
Log.d(TAG, "No track for " + trackId);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create a new table
|
||||
publishProgress(PROGRESS_CREATE_TABLE);
|
||||
String tableId = createNewTable(fusiontables, track);
|
||||
if (tableId == null) {
|
||||
return retryTask();
|
||||
}
|
||||
|
||||
publishProgress(PROGRESS_SET_STYLE);
|
||||
setStyle(fusiontables, tableId);
|
||||
setTemplate(fusiontables, tableId);
|
||||
if (!setPermission(track, tableId)) {
|
||||
Log.d(TAG, "Cannot set permission for table " + tableId);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Upload all the track points plus the start and end markers
|
||||
publishProgress(PROGRESS_UPLOAD_DATA_MIN);
|
||||
if (!uploadAllTrackPoints(fusiontables, tableId, track)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Upload all the waypoints
|
||||
publishProgress(PROGRESS_UPLOAD_WAYPOINTS);
|
||||
if (!uploadWaypoints(fusiontables, tableId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
publishProgress(PROGRESS_COMPLETE);
|
||||
return true;
|
||||
|
||||
} catch (UserRecoverableAuthException e) {
|
||||
SendToGoogleUtils.sendNotification(
|
||||
context, account.name, e.getIntent(), SendToGoogleUtils.FUSION_TABLES_NOTIFICATION_ID);
|
||||
return false;
|
||||
} catch (GoogleAuthException e) {
|
||||
return retryTask();
|
||||
} catch (UserRecoverableAuthIOException e) {
|
||||
SendToGoogleUtils.sendNotification(
|
||||
context, account.name, e.getIntent(), SendToGoogleUtils.FUSION_TABLES_NOTIFICATION_ID);
|
||||
return false;
|
||||
} catch (IOException e) {
|
||||
return retryTask();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void invalidateToken() {}
|
||||
|
||||
/**
|
||||
* Creates a new table.
|
||||
*
|
||||
* @param fusiontables fusion tables
|
||||
* @param track the track
|
||||
* @return the table id if success.
|
||||
*/
|
||||
private String createNewTable(Fusiontables fusiontables, Track track) throws IOException {
|
||||
Table table = new Table();
|
||||
table.setName(track.getName());
|
||||
table.setDescription(track.getDescription());
|
||||
table.setIsExportable(true);
|
||||
table.setColumns(Arrays.asList(new Column().setName("name").setType("STRING"),
|
||||
new Column().setName("description").setType("STRING"),
|
||||
new Column().setName("geometry").setType("LOCATION"),
|
||||
new Column().setName("icon").setType("STRING")));
|
||||
return fusiontables.table().insert(table).execute().getTableId();
|
||||
}
|
||||
|
||||
private void setStyle(Fusiontables fusiontables, String tableId) throws IOException {
|
||||
StyleFunction styleFunction = new StyleFunction();
|
||||
styleFunction.setColumnName("icon");
|
||||
PointStyle pointStyle = new PointStyle();
|
||||
pointStyle.setIconStyler(styleFunction);
|
||||
StyleSetting styleSetting = new StyleSetting();
|
||||
styleSetting.setTableId(tableId);
|
||||
styleSetting.setMarkerOptions(pointStyle);
|
||||
|
||||
fusiontables.style().insert(tableId, styleSetting).execute();
|
||||
}
|
||||
|
||||
private void setTemplate(Fusiontables fusiontables, String tableId) throws IOException {
|
||||
Template template = new Template();
|
||||
template.setTableId(tableId);
|
||||
template.setAutomaticColumnNames(Arrays.asList("name", "description"));
|
||||
fusiontables.template().insert(tableId, template).execute();
|
||||
}
|
||||
|
||||
private boolean setPermission(Track track, String tableId) throws IOException, GoogleAuthException {
|
||||
boolean defaultTablePublic = PreferencesUtils.getBoolean(context,
|
||||
R.string.export_google_fusion_tables_public_key,
|
||||
PreferencesUtils.EXPORT_GOOGLE_FUSION_TABLES_PUBLIC_DEFAULT);
|
||||
if (!defaultTablePublic) {
|
||||
return true;
|
||||
}
|
||||
GoogleAccountCredential driveCredential = SendToGoogleUtils.getGoogleAccountCredential(
|
||||
context, account.name, SendToGoogleUtils.DRIVE_SCOPE);
|
||||
if (driveCredential == null) {
|
||||
return false;
|
||||
}
|
||||
Drive drive = SyncUtils.getDriveService(driveCredential);
|
||||
Permission permission = new Permission();
|
||||
permission.setRole("reader");
|
||||
permission.setType("anyone");
|
||||
permission.setValue("");
|
||||
drive.permissions().insert(tableId, permission).execute();
|
||||
|
||||
shareUrl = SendFusionTablesUtils.getMapUrl(track, tableId);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads all the points in a track. *
|
||||
*
|
||||
* @param fusiontables fusion tables
|
||||
* @param tableId the table id
|
||||
* @param track the track
|
||||
* @return true if success.
|
||||
*/
|
||||
private boolean uploadAllTrackPoints(Fusiontables fusiontables, String tableId, Track track)
|
||||
throws IOException {
|
||||
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 lastValidLocation = null;
|
||||
boolean sentStartMarker = false;
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
cursor.moveToPosition(i);
|
||||
|
||||
Location location = myTracksProviderUtils.createTrackPoint(cursor);
|
||||
locations.add(location);
|
||||
|
||||
if (LocationUtils.isValidLocation(location)) {
|
||||
lastValidLocation = location;
|
||||
}
|
||||
|
||||
if (!sentStartMarker && lastValidLocation != null) {
|
||||
// Create a start marker
|
||||
String name = context.getString(R.string.marker_label_start, track.getName());
|
||||
createNewPoint(fusiontables, tableId, name, "", lastValidLocation, MARKER_TYPE_START);
|
||||
sentStartMarker = true;
|
||||
}
|
||||
|
||||
// Upload periodically
|
||||
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 (lastValidLocation != null) {
|
||||
String name = context.getString(R.string.marker_label_end, track.getName());
|
||||
DescriptionGenerator descriptionGenerator = new DescriptionGeneratorImpl(context);
|
||||
String description = descriptionGenerator.generateTrackDescription(track, null, null, true);
|
||||
createNewPoint(
|
||||
fusiontables, tableId, name, description, lastValidLocation, MARKER_TYPE_END);
|
||||
}
|
||||
return true;
|
||||
} finally {
|
||||
if (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);
|
||||
}
|
||||
}
|
||||
+155
@@ -0,0 +1,155 @@
|
||||
/*
|
||||
* Copyright 2012 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package com.google.android.apps.mytracks.io.fusiontables;
|
||||
|
||||
import com.google.android.apps.mytracks.content.Track;
|
||||
import com.google.android.apps.mytracks.stats.TripStatistics;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
|
||||
import android.location.Location;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Utilities for sending a track to Google Fusion Tables.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class SendFusionTablesUtils {
|
||||
|
||||
private static final String MAP_URL = "https://www.google.com/fusiontables/embedviz?"
|
||||
+ "viz=MAP&q=select+col0,+col1,+col2,+col3+from+%s+&h=false&lat=%f&lng=%f&z=%d&t=1&l=col2";
|
||||
private static final String TAG = SendFusionTablesUtils.class.getSimpleName();
|
||||
|
||||
private SendFusionTablesUtils() {}
|
||||
|
||||
/**
|
||||
* Gets the url to visualize a fusion table on a map.
|
||||
*
|
||||
* @param track the track
|
||||
* @param tableId the table id
|
||||
* @return the url.
|
||||
*/
|
||||
public static String getMapUrl(Track track, String tableId) {
|
||||
if (track == null || track.getTripStatistics() == null) {
|
||||
Log.e(TAG, "Invalid track");
|
||||
return null;
|
||||
}
|
||||
double latE6;
|
||||
double lonE6;
|
||||
int z;
|
||||
if (track.getNumberOfPoints() < 2) {
|
||||
// Use Google's latitude and longitude
|
||||
latE6 = 37.423 * 1.E6;
|
||||
lonE6 = -122.084 * 1.E6;
|
||||
z = 2;
|
||||
} else {
|
||||
TripStatistics stats = track.getTripStatistics();
|
||||
latE6 = stats.getBottom() + (stats.getTop() - stats.getBottom()) / 2;
|
||||
lonE6 = stats.getLeft() + (stats.getRight() - stats.getLeft()) / 2;
|
||||
z = 15;
|
||||
}
|
||||
// We explicitly format with Locale.US because we need the latitude and
|
||||
// longitude to be formatted in a locale-independent manner. Specifically,
|
||||
// we need the decimal separator to be a period rather than a comma.
|
||||
return String.format(Locale.US, MAP_URL, tableId, latE6 / 1.E6, lonE6 / 1.E6, z);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats an array of values as a SQL VALUES like
|
||||
* ('value1','value2',...,'value_n'). Escapes single quotes with two single
|
||||
* quotes.
|
||||
*
|
||||
* @param values an array of values to format
|
||||
* @return the formated SQL VALUES.
|
||||
*/
|
||||
public static String formatSqlValues(String... values) {
|
||||
StringBuilder builder = new StringBuilder("(");
|
||||
for (int i = 0; i < values.length; i++) {
|
||||
if (i > 0) {
|
||||
builder.append(',');
|
||||
}
|
||||
builder.append('\'');
|
||||
builder.append(escapeSqlString(values[i]));
|
||||
builder.append('\'');
|
||||
}
|
||||
builder.append(")");
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes a SQL string. Escapes single quotes with two single quotes.
|
||||
*
|
||||
* @param string the string
|
||||
* @return the escaped string.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
static String escapeSqlString(String string) {
|
||||
return string.replaceAll("'", "''");
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a KML Point value representing a location.
|
||||
*
|
||||
* @param location the location
|
||||
* @return the KML Point value.
|
||||
*/
|
||||
public static String getKmlPoint(Location location) {
|
||||
StringBuilder builder = new StringBuilder("<Point><coordinates>");
|
||||
if (location != null) {
|
||||
appendLocation(location, builder);
|
||||
}
|
||||
builder.append("</coordinates></Point>");
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a KML LineString value representing an array of locations.
|
||||
*
|
||||
* @param locations the locations.
|
||||
* @return the KML LineString value.
|
||||
*/
|
||||
public static String getKmlLineString(ArrayList<Location> locations) {
|
||||
StringBuilder builder = new StringBuilder("<LineString><coordinates>");
|
||||
if (locations != null) {
|
||||
for (int i = 0; i < locations.size(); i++) {
|
||||
if (i != 0) {
|
||||
builder.append(' ');
|
||||
}
|
||||
appendLocation(locations.get(i), builder);
|
||||
}
|
||||
}
|
||||
builder.append("</coordinates></LineString>");
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends a location to a string builder using "longitude,latitude[,altitude]" format.
|
||||
*
|
||||
* @param location the location
|
||||
* @param builder the string builder
|
||||
*/
|
||||
@VisibleForTesting
|
||||
static void appendLocation(Location location, StringBuilder builder) {
|
||||
builder.append(location.getLongitude()).append(",").append(location.getLatitude());
|
||||
if (location.hasAltitude()) {
|
||||
builder.append(",");
|
||||
builder.append(location.getAltitude());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,457 @@
|
||||
/*
|
||||
* Copyright 2010 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package com.google.android.apps.mytracks.io.gdata;
|
||||
|
||||
import com.google.wireless.gdata.client.GDataClient;
|
||||
import com.google.wireless.gdata.client.HttpException;
|
||||
import com.google.wireless.gdata.client.QueryParams;
|
||||
import com.google.wireless.gdata.data.StringUtils;
|
||||
import com.google.wireless.gdata.parser.ParseException;
|
||||
import com.google.wireless.gdata.serializer.GDataSerializer;
|
||||
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URLEncoder;
|
||||
import java.util.zip.GZIPInputStream;
|
||||
|
||||
import org.apache.http.Header;
|
||||
import org.apache.http.HttpEntity;
|
||||
import org.apache.http.HttpResponse;
|
||||
import org.apache.http.StatusLine;
|
||||
import org.apache.http.client.HttpClient;
|
||||
import org.apache.http.client.methods.HttpGet;
|
||||
import org.apache.http.client.methods.HttpPost;
|
||||
import org.apache.http.client.methods.HttpUriRequest;
|
||||
import org.apache.http.entity.AbstractHttpEntity;
|
||||
import org.apache.http.entity.ByteArrayEntity;
|
||||
import org.apache.http.entity.InputStreamEntity;
|
||||
import org.apache.http.impl.client.DefaultHttpClient;
|
||||
|
||||
/**
|
||||
* Implementation of a GDataClient using GoogleHttpClient to make HTTP requests.
|
||||
* Always issues GETs and POSTs, using the X-HTTP-Method-Override header when a
|
||||
* PUT or DELETE is desired, to avoid issues with firewalls, etc., that do not
|
||||
* allow methods other than GET or POST.
|
||||
*/
|
||||
public class AndroidGDataClient implements GDataClient {
|
||||
|
||||
private static final String TAG = "GDataClient";
|
||||
private static final boolean DEBUG = false;
|
||||
|
||||
private static final String X_HTTP_METHOD_OVERRIDE = "X-HTTP-Method-Override";
|
||||
|
||||
private static final int MAX_REDIRECTS = 10;
|
||||
|
||||
private final HttpClient httpClient;
|
||||
|
||||
/**
|
||||
* Interface for creating HTTP requests. Used by
|
||||
* {@link AndroidGDataClient#createAndExecuteMethod}, since HttpUriRequest
|
||||
* does not allow for changing the URI after creation, e.g., when you want to
|
||||
* follow a redirect.
|
||||
*/
|
||||
private interface HttpRequestCreator {
|
||||
HttpUriRequest createRequest(URI uri);
|
||||
}
|
||||
|
||||
private static class GetRequestCreator implements HttpRequestCreator {
|
||||
public HttpUriRequest createRequest(URI uri) {
|
||||
return new HttpGet(uri);
|
||||
}
|
||||
}
|
||||
|
||||
private static class PostRequestCreator implements HttpRequestCreator {
|
||||
private final String mMethodOverride;
|
||||
private final HttpEntity mEntity;
|
||||
|
||||
public PostRequestCreator(String methodOverride, HttpEntity entity) {
|
||||
mMethodOverride = methodOverride;
|
||||
mEntity = entity;
|
||||
}
|
||||
|
||||
public HttpUriRequest createRequest(URI uri) {
|
||||
HttpPost post = new HttpPost(uri);
|
||||
if (mMethodOverride != null) {
|
||||
post.addHeader(X_HTTP_METHOD_OVERRIDE, mMethodOverride);
|
||||
}
|
||||
post.setEntity(mEntity);
|
||||
return post;
|
||||
}
|
||||
}
|
||||
|
||||
// MAJOR TODO: make this work across redirects (if we can reset the
|
||||
// InputStream).
|
||||
// OR, read the bits into a local buffer (yuck, the media could be large).
|
||||
private static class MediaPutRequestCreator implements HttpRequestCreator {
|
||||
private final InputStream mMediaInputStream;
|
||||
private final String mContentType;
|
||||
|
||||
public MediaPutRequestCreator(InputStream mediaInputStream,
|
||||
String contentType) {
|
||||
mMediaInputStream = mediaInputStream;
|
||||
mContentType = contentType;
|
||||
}
|
||||
|
||||
public HttpUriRequest createRequest(URI uri) {
|
||||
HttpPost post = new HttpPost(uri);
|
||||
post.addHeader(X_HTTP_METHOD_OVERRIDE, "PUT");
|
||||
InputStreamEntity entity =
|
||||
new InputStreamEntity(mMediaInputStream, -1 /* read until EOF */);
|
||||
entity.setContentType(mContentType);
|
||||
post.setEntity(entity);
|
||||
return post;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new AndroidGDataClient.
|
||||
*/
|
||||
public AndroidGDataClient() {
|
||||
httpClient = new DefaultHttpClient();
|
||||
}
|
||||
|
||||
public void close() {
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
*
|
||||
* @see GDataClient#encodeUri(java.lang.String)
|
||||
*/
|
||||
public String encodeUri(String uri) {
|
||||
String encodedUri = null;
|
||||
try {
|
||||
encodedUri = URLEncoder.encode(uri, "UTF-8");
|
||||
} catch (UnsupportedEncodingException uee) {
|
||||
// Should not happen
|
||||
throw new IllegalStateException("Cannot encode " + uri, uee);
|
||||
}
|
||||
return encodedUri;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
*
|
||||
* @see com.google.wireless.gdata.client.GDataClient#createQueryParams()
|
||||
*/
|
||||
public QueryParams createQueryParams() {
|
||||
return new QueryParamsImpl();
|
||||
}
|
||||
|
||||
// follows redirects
|
||||
private InputStream createAndExecuteMethod(HttpRequestCreator creator,
|
||||
String uriString, String authToken) throws HttpException, IOException {
|
||||
|
||||
HttpResponse response = null;
|
||||
int status = 500;
|
||||
int redirectsLeft = MAX_REDIRECTS;
|
||||
|
||||
URI uri;
|
||||
try {
|
||||
uri = new URI(uriString);
|
||||
} catch (URISyntaxException use) {
|
||||
Log.w(TAG, "Unable to parse " + uriString + " as URI.", use);
|
||||
throw new IOException("Unable to parse " + uriString + " as URI: "
|
||||
+ use.getMessage());
|
||||
}
|
||||
|
||||
// we follow redirects ourselves, since we want to follow redirects even on
|
||||
// POSTs, which
|
||||
// the HTTP library does not do. following redirects ourselves also allows
|
||||
// us to log
|
||||
// the redirects using our own logging.
|
||||
while (redirectsLeft > 0) {
|
||||
|
||||
HttpUriRequest request = creator.createRequest(uri);
|
||||
request.addHeader("User-Agent", "Android-GData");
|
||||
request.addHeader("Accept-Encoding", "gzip");
|
||||
|
||||
// only add the auth token if not null (to allow for GData feeds that do
|
||||
// not require
|
||||
// authentication.)
|
||||
if (!TextUtils.isEmpty(authToken)) {
|
||||
request.addHeader("Authorization", "GoogleLogin auth=" + authToken);
|
||||
}
|
||||
if (DEBUG) {
|
||||
for (Header h : request.getAllHeaders()) {
|
||||
Log.v(TAG, h.getName() + ": " + h.getValue());
|
||||
}
|
||||
Log.d(TAG, "Executing " + request.getRequestLine().toString());
|
||||
}
|
||||
|
||||
response = null;
|
||||
|
||||
try {
|
||||
response = httpClient.execute(request);
|
||||
} catch (IOException ioe) {
|
||||
Log.w(TAG, "Unable to execute HTTP request." + ioe);
|
||||
throw ioe;
|
||||
}
|
||||
|
||||
StatusLine statusLine = response.getStatusLine();
|
||||
if (statusLine == null) {
|
||||
Log.w(TAG, "StatusLine is null.");
|
||||
throw new NullPointerException(
|
||||
"StatusLine is null -- should not happen.");
|
||||
}
|
||||
|
||||
if (Log.isLoggable(TAG, Log.DEBUG)) {
|
||||
Log.d(TAG, response.getStatusLine().toString());
|
||||
for (Header h : response.getAllHeaders()) {
|
||||
Log.d(TAG, h.getName() + ": " + h.getValue());
|
||||
}
|
||||
}
|
||||
status = statusLine.getStatusCode();
|
||||
|
||||
HttpEntity entity = response.getEntity();
|
||||
|
||||
if ((status >= 200) && (status < 300) && entity != null) {
|
||||
return getUngzippedContent(entity);
|
||||
}
|
||||
|
||||
// TODO: handle 301, 307?
|
||||
// TODO: let the http client handle the redirects, if we can be sure we'll
|
||||
// never get a
|
||||
// redirect on POST.
|
||||
if (status == 302) {
|
||||
// consume the content, so the connection can be closed.
|
||||
entity.consumeContent();
|
||||
Header location = response.getFirstHeader("Location");
|
||||
if (location == null) {
|
||||
if (Log.isLoggable(TAG, Log.DEBUG)) {
|
||||
Log.d(TAG, "Redirect requested but no Location " + "specified.");
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (Log.isLoggable(TAG, Log.DEBUG)) {
|
||||
Log.d(TAG, "Following redirect to " + location.getValue());
|
||||
}
|
||||
try {
|
||||
uri = new URI(location.getValue());
|
||||
} catch (URISyntaxException use) {
|
||||
if (Log.isLoggable(TAG, Log.DEBUG)) {
|
||||
Log.d(TAG, "Unable to parse " + location.getValue() + " as URI.",
|
||||
use);
|
||||
throw new IOException("Unable to parse " + location.getValue()
|
||||
+ " as URI.");
|
||||
}
|
||||
break;
|
||||
}
|
||||
--redirectsLeft;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (Log.isLoggable(TAG, Log.VERBOSE)) {
|
||||
Log.v(TAG, "Received " + status + " status code.");
|
||||
}
|
||||
String errorMessage = null;
|
||||
HttpEntity entity = response.getEntity();
|
||||
try {
|
||||
if (entity != null) {
|
||||
InputStream in = entity.getContent();
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
byte[] buf = new byte[8192];
|
||||
int bytesRead = -1;
|
||||
while ((bytesRead = in.read(buf)) != -1) {
|
||||
baos.write(buf, 0, bytesRead);
|
||||
}
|
||||
// TODO: use appropriate encoding, picked up from Content-Type.
|
||||
errorMessage = new String(baos.toByteArray());
|
||||
if (Log.isLoggable(TAG, Log.VERBOSE)) {
|
||||
Log.v(TAG, errorMessage);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (entity != null) {
|
||||
entity.consumeContent();
|
||||
}
|
||||
}
|
||||
String exceptionMessage = "Received " + status + " status code";
|
||||
if (errorMessage != null) {
|
||||
exceptionMessage += (": " + errorMessage);
|
||||
}
|
||||
throw new HttpException(exceptionMessage, status, null /* InputStream */);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the input stream from a response entity. If the entity is gzipped
|
||||
* then this will get a stream over the uncompressed data.
|
||||
*
|
||||
* @param entity the entity whose content should be read
|
||||
* @return the input stream to read from
|
||||
* @throws IOException
|
||||
*/
|
||||
private static InputStream getUngzippedContent(HttpEntity entity)
|
||||
throws IOException {
|
||||
InputStream responseStream = entity.getContent();
|
||||
if (responseStream == null) {
|
||||
return responseStream;
|
||||
}
|
||||
Header header = entity.getContentEncoding();
|
||||
if (header == null) {
|
||||
return responseStream;
|
||||
}
|
||||
String contentEncoding = header.getValue();
|
||||
if (contentEncoding == null) {
|
||||
return responseStream;
|
||||
}
|
||||
if (contentEncoding.contains("gzip")){
|
||||
responseStream = new GZIPInputStream(responseStream);
|
||||
}
|
||||
return responseStream;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
*
|
||||
* @see GDataClient#getFeedAsStream(java.lang.String, java.lang.String)
|
||||
*/
|
||||
public InputStream getFeedAsStream(String feedUrl, String authToken)
|
||||
throws HttpException, IOException {
|
||||
|
||||
InputStream in =
|
||||
createAndExecuteMethod(new GetRequestCreator(), feedUrl, authToken);
|
||||
if (in != null) {
|
||||
return in;
|
||||
}
|
||||
throw new IOException("Unable to access feed.");
|
||||
}
|
||||
|
||||
public InputStream getMediaEntryAsStream(String mediaEntryUrl,
|
||||
String authToken) throws HttpException, IOException {
|
||||
|
||||
InputStream in =
|
||||
createAndExecuteMethod(new GetRequestCreator(), mediaEntryUrl,
|
||||
authToken);
|
||||
|
||||
if (in != null) {
|
||||
return in;
|
||||
}
|
||||
throw new IOException("Unable to access media entry.");
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
*
|
||||
* @see GDataClient#createEntry
|
||||
*/
|
||||
public InputStream createEntry(String feedUrl, String authToken,
|
||||
GDataSerializer entry) throws HttpException, IOException {
|
||||
|
||||
HttpEntity entity =
|
||||
createEntityForEntry(entry, GDataSerializer.FORMAT_CREATE);
|
||||
InputStream in =
|
||||
createAndExecuteMethod(new PostRequestCreator(null /* override */,
|
||||
entity), feedUrl, authToken);
|
||||
if (in != null) {
|
||||
return in;
|
||||
}
|
||||
throw new IOException("Unable to create entry.");
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
*
|
||||
* @see GDataClient#updateEntry
|
||||
*/
|
||||
public InputStream updateEntry(String editUri, String authToken,
|
||||
GDataSerializer entry) throws HttpException, IOException {
|
||||
HttpEntity entity =
|
||||
createEntityForEntry(entry, GDataSerializer.FORMAT_UPDATE);
|
||||
InputStream in =
|
||||
createAndExecuteMethod(new PostRequestCreator("PUT", entity), editUri,
|
||||
authToken);
|
||||
if (in != null) {
|
||||
return in;
|
||||
}
|
||||
throw new IOException("Unable to update entry.");
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
*
|
||||
* @see GDataClient#deleteEntry
|
||||
*/
|
||||
public void deleteEntry(String editUri, String authToken)
|
||||
throws HttpException, IOException {
|
||||
if (StringUtils.isEmpty(editUri)) {
|
||||
throw new IllegalArgumentException(
|
||||
"you must specify an non-empty edit url");
|
||||
}
|
||||
InputStream in =
|
||||
createAndExecuteMethod(
|
||||
new PostRequestCreator("DELETE", null /* entity */), editUri,
|
||||
authToken);
|
||||
if (in == null) {
|
||||
throw new IOException("Unable to delete entry.");
|
||||
}
|
||||
try {
|
||||
in.close();
|
||||
} catch (IOException ioe) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
public InputStream updateMediaEntry(String editUri, String authToken,
|
||||
InputStream mediaEntryInputStream, String contentType)
|
||||
throws HttpException, IOException {
|
||||
InputStream in =
|
||||
createAndExecuteMethod(new MediaPutRequestCreator(
|
||||
mediaEntryInputStream, contentType), editUri, authToken);
|
||||
if (in != null) {
|
||||
return in;
|
||||
}
|
||||
throw new IOException("Unable to write media entry.");
|
||||
}
|
||||
|
||||
private HttpEntity createEntityForEntry(GDataSerializer entry, int format)
|
||||
throws IOException {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
try {
|
||||
entry.serialize(baos, format);
|
||||
} catch (IOException ioe) {
|
||||
Log.e(TAG, "Unable to serialize entry.", ioe);
|
||||
throw ioe;
|
||||
} catch (ParseException pe) {
|
||||
Log.e(TAG, "Unable to serialize entry.", pe);
|
||||
throw new IOException("Unable to serialize entry: " + pe.getMessage());
|
||||
}
|
||||
|
||||
byte[] entryBytes = baos.toByteArray();
|
||||
|
||||
if (entryBytes != null && Log.isLoggable(TAG, Log.DEBUG)) {
|
||||
try {
|
||||
Log.d(TAG, "Serialized entry: " + new String(entryBytes, "UTF-8"));
|
||||
} catch (UnsupportedEncodingException uee) {
|
||||
// should not happen
|
||||
throw new IllegalStateException("UTF-8 should be supported!", uee);
|
||||
}
|
||||
}
|
||||
|
||||
AbstractHttpEntity entity = new ByteArrayEntity(entryBytes);
|
||||
entity.setContentType(entry.getContentType());
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,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() { }
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user