Fix for ANT+ Sensor Manager Events Order Issue

This commit is contained in:
Umran Abdulla
2011-11-01 16:04:46 +11:00
commit 324d3f6ef1
501 changed files with 63866 additions and 0 deletions
+268
View File
@@ -0,0 +1,268 @@
/*
* Copyright 2010 Dynastream Innovations Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.dsi.ant;
/**
* The Android Ant API is not finalized, and *will* change. Use at your
* own risk.
*
* Public API for controlling the Ant Service.
* AntDefines contains definitions commonly used in ANT messaging.
*
* @hide
*/
public class AntDefine {
//////////////////////////////////////////////
// Valid Configuration Values
//////////////////////////////////////////////
public static final byte MIN_BIN = 0;
public static final byte MAX_BIN = 10;
public static final short MIN_DEVICE_ID = 0;
public static final short MAX_DEVICE_ID = (short)65535;
public static final short MIN_BUFFER_THRESHOLD = 0;
public static final short MAX_BUFFER_THRESHOLD = 996;
//////////////////////////////////////////////
// ANT Message Payload Size
//////////////////////////////////////////////
public static final byte ANT_STANDARD_DATA_PAYLOAD_SIZE =((byte)8);
//////////////////////////////////////////////
// ANT LIBRARY Extended Data Message Fields
// NOTE: You must check the extended message
// bitfield first to find out which fields
// are present before accessing them!
//////////////////////////////////////////////
public static final byte ANT_EXT_MESG_DEVICE_ID_FIELD_SIZE =((byte)4);
//public static final byte ANT_EXT_STRING_SIZE =((byte)19); // this is the additional buffer space required required for setting USB Descriptor strings
public static final byte ANT_EXT_STRING_SIZE =((byte)0); // changed to 0 as we will not be dealing with ANT USB parts.
//////////////////////////////////////////////
// ANT Extended Data Message Bifield Definitions
//////////////////////////////////////////////
public static final byte ANT_EXT_MESG_BITFIELD_DEVICE_ID =((byte)0x80); // first field after bitfield
public static final byte ANT_EXT_MESG_BITFIELD_RSSI =((byte)0x40); // next field after ID, if there is one
public static final byte ANT_EXT_MESG_BITFIELD_TIME_STAMP =((byte)0x20); // next field after RSSI, if there is one
// 4 bits free reserved set to 0
public static final byte ANT_EXT_MESG_BIFIELD_EXTENSION =((byte)0x01);
// extended message input bitfield defines
public static final byte ANT_EXT_MESG_BITFIELD_OVERWRITE_SHARED_ADR =((byte)0x10);
public static final byte ANT_EXT_MESG_BITFIELD_TRANSMISSION_TYPE =((byte)0x08);
//////////////////////////////////////////////
// ID Definitions
//////////////////////////////////////////////
public static final byte ANT_ID_SIZE =((byte)4);
public static final byte ANT_ID_TRANS_TYPE_OFFSET =((byte)3);
public static final byte ANT_ID_DEVICE_TYPE_OFFSET =((byte)2);
public static final byte ANT_ID_DEVICE_NUMBER_HIGH_OFFSET =((byte)1);
public static final byte ANT_ID_DEVICE_NUMBER_LOW_OFFSET =((byte)0);
public static final byte ANT_ID_DEVICE_TYPE_PAIRING_FLAG =((byte)0x80);
public static final byte ANT_TRANS_TYPE_SHARED_ADDR_MASK =((byte)0x03);
public static final byte ANT_TRANS_TYPE_1_BYTE_SHARED_ADDRESS =((byte)0x02);
public static final byte ANT_TRANS_TYPE_2_BYTE_SHARED_ADDRESS =((byte)0x03);
//////////////////////////////////////////////
// Assign Channel Parameters
//////////////////////////////////////////////
public static final byte PARAMETER_RX_NOT_TX =((byte)0x00);
public static final byte PARAMETER_TX_NOT_RX =((byte)0x10);
public static final byte PARAMETER_SHARED_CHANNEL =((byte)0x20);
public static final byte PARAMETER_NO_TX_GUARD_BAND =((byte)0x40);
public static final byte PARAMETER_ALWAYS_RX_WILD_CARD_SEARCH_ID =((byte)0x40); //Pre-AP2
public static final byte PARAMETER_RX_ONLY =((byte)0x40);
//////////////////////////////////////////////
// Ext. Assign Channel Parameters
//////////////////////////////////////////////
public static final byte EXT_PARAM_ALWAYS_SEARCH =((byte)0x01);
public static final byte EXT_PARAM_FREQUENCY_AGILITY =((byte)0x04);
//////////////////////////////////////////////
// Radio TX Power Definitions
//////////////////////////////////////////////
public static final byte RADIO_TX_POWER_LVL_MASK =((byte)0x03);
public static final byte RADIO_TX_POWER_LVL_0 =((byte)0x00); //(formerly: RADIO_TX_POWER_MINUS20DB); lowest
public static final byte RADIO_TX_POWER_LVL_1 =((byte)0x01); //(formerly: RADIO_TX_POWER_MINUS10DB);
public static final byte RADIO_TX_POWER_LVL_2 =((byte)0x02); //(formerly: RADIO_TX_POWER_MINUS5DB);
public static final byte RADIO_TX_POWER_LVL_3 =((byte)0x03); //(formerly: RADIO_TX_POWER_0DB); highest
//////////////////////////////////////////////
// Channel Status
//////////////////////////////////////////////
public static final byte STATUS_CHANNEL_STATE_MASK =((byte)0x03);
public static final byte STATUS_UNASSIGNED_CHANNEL =((byte)0x00);
public static final byte STATUS_ASSIGNED_CHANNEL =((byte)0x01);
public static final byte STATUS_SEARCHING_CHANNEL =((byte)0x02);
public static final byte STATUS_TRACKING_CHANNEL =((byte)0x03);
//////////////////////////////////////////////
// Standard capabilities defines
//////////////////////////////////////////////
public static final byte CAPABILITIES_NO_RX_CHANNELS =((byte)0x01);
public static final byte CAPABILITIES_NO_TX_CHANNELS =((byte)0x02);
public static final byte CAPABILITIES_NO_RX_MESSAGES =((byte)0x04);
public static final byte CAPABILITIES_NO_TX_MESSAGES =((byte)0x08);
public static final byte CAPABILITIES_NO_ACKD_MESSAGES =((byte)0x10);
public static final byte CAPABILITIES_NO_BURST_TRANSFER =((byte)0x20);
//////////////////////////////////////////////
// Advanced capabilities defines
//////////////////////////////////////////////
public static final byte CAPABILITIES_OVERUN_UNDERRUN =((byte)0x01); // Support for this functionality has been dropped
public static final byte CAPABILITIES_NETWORK_ENABLED =((byte)0x02);
public static final byte CAPABILITIES_AP1_VERSION_2 =((byte)0x04); // This Version of the AP1 does not support transmit and only had a limited release
public static final byte CAPABILITIES_SERIAL_NUMBER_ENABLED =((byte)0x08);
public static final byte CAPABILITIES_PER_CHANNEL_TX_POWER_ENABLED =((byte)0x10);
public static final byte CAPABILITIES_LOW_PRIORITY_SEARCH_ENABLED =((byte)0x20);
public static final byte CAPABILITIES_SCRIPT_ENABLED =((byte)0x40);
public static final byte CAPABILITIES_SEARCH_LIST_ENABLED =((byte)0x80);
//////////////////////////////////////////////
// Advanced capabilities 2 defines
//////////////////////////////////////////////
public static final byte CAPABILITIES_LED_ENABLED =((byte)0x01);
public static final byte CAPABILITIES_EXT_MESSAGE_ENABLED =((byte)0x02);
public static final byte CAPABILITIES_SCAN_MODE_ENABLED =((byte)0x04);
public static final byte CAPABILITIES_RESERVED =((byte)0x08);
public static final byte CAPABILITIES_PROX_SEARCH_ENABLED =((byte)0x10);
public static final byte CAPABILITIES_EXT_ASSIGN_ENABLED =((byte)0x20);
public static final byte CAPABILITIES_FREE_1 =((byte)0x40);
public static final byte CAPABILITIES_FIT1_ENABLED =((byte)0x80);
//////////////////////////////////////////////
// Advanced capabilities 3 defines
//////////////////////////////////////////////
public static final byte CAPABILITIES_SENSRCORE_ENABLED =((byte)0x01);
public static final byte CAPABILITIES_RESERVED_1 =((byte)0x02);
public static final byte CAPABILITIES_RESERVED_2 =((byte)0x04);
public static final byte CAPABILITIES_RESERVED_3 =((byte)0x08);
//////////////////////////////////////////////
// Burst Message Sequence
//////////////////////////////////////////////
public static final byte CHANNEL_NUMBER_MASK =((byte)0x1F);
public static final byte SEQUENCE_NUMBER_MASK =((byte)0xE0);
public static final byte SEQUENCE_NUMBER_ROLLOVER =((byte)0x60);
public static final byte SEQUENCE_FIRST_MESSAGE =((byte)0x00);
public static final byte SEQUENCE_LAST_MESSAGE =((byte)0x80);
public static final byte SEQUENCE_NUMBER_INC =((byte)0x20);
//////////////////////////////////////////////
// Control Message Flags
//////////////////////////////////////////////
public static final byte BROADCAST_CONTROL_BYTE =((byte)0x00);
public static final byte ACKNOWLEDGED_CONTROL_BYTE =((byte)0xA0);
//////////////////////////////////////////////
// Response / Event Codes
//////////////////////////////////////////////
public static final byte RESPONSE_NO_ERROR =((byte)0x00);
public static final byte NO_EVENT =((byte)0x00);
public static final byte EVENT_RX_SEARCH_TIMEOUT =((byte)0x01);
public static final byte EVENT_RX_FAIL =((byte)0x02);
public static final byte EVENT_TX =((byte)0x03);
public static final byte EVENT_TRANSFER_RX_FAILED =((byte)0x04);
public static final byte EVENT_TRANSFER_TX_COMPLETED =((byte)0x05);
public static final byte EVENT_TRANSFER_TX_FAILED =((byte)0x06);
public static final byte EVENT_CHANNEL_CLOSED =((byte)0x07);
public static final byte EVENT_RX_FAIL_GO_TO_SEARCH =((byte)0x08);
public static final byte EVENT_CHANNEL_COLLISION =((byte)0x09);
public static final byte EVENT_TRANSFER_TX_START =((byte)0x0A); // a pending transmit transfer has begun
public static final byte EVENT_CHANNEL_ACTIVE =((byte)0x0F);
public static final byte EVENT_TRANSFER_TX_NEXT_MESSAGE =((byte)0x11); // only enabled in FIT1
public static final byte CHANNEL_IN_WRONG_STATE =((byte)0x15); // returned on attempt to perform an action from the wrong channel state
public static final byte CHANNEL_NOT_OPENED =((byte)0x16); // returned on attempt to communicate on a channel that is not open
public static final byte CHANNEL_ID_NOT_SET =((byte)0x18); // returned on attempt to open a channel without setting the channel ID
public static final byte CLOSE_ALL_CHANNELS =((byte)0x19); // returned when attempting to start scanning mode, when channels are still open
public static final byte TRANSFER_IN_PROGRESS =((byte)0x1F); // returned on attempt to communicate on a channel with a TX transfer in progress
public static final byte TRANSFER_SEQUENCE_NUMBER_ERROR =((byte)0x20); // returned when sequence number is out of order on a Burst transfer
public static final byte TRANSFER_IN_ERROR =((byte)0x21);
public static final byte TRANSFER_BUSY =((byte)0x22);
public static final byte INVALID_MESSAGE_CRC =((byte)0x26); // returned if there is a framing error on an incomming message
public static final byte MESSAGE_SIZE_EXCEEDS_LIMIT =((byte)0x27); // returned if a data message is provided that is too large
public static final byte INVALID_MESSAGE =((byte)0x28); // returned when the message has an invalid parameter
public static final byte INVALID_NETWORK_NUMBER =((byte)0x29); // returned when an invalid network number is provided
public static final byte INVALID_LIST_ID =((byte)0x30); // returned when the provided list ID or size exceeds the limit
public static final byte INVALID_SCAN_TX_CHANNEL =((byte)0x31); // returned when attempting to transmit on channel 0 when in scan mode
public static final byte INVALID_PARAMETER_PROVIDED =((byte)0x33); // returned when an invalid parameter is specified in a configuration message
public static final byte EVENT_SERIAL_QUE_OVERFLOW =((byte)0x34);
public static final byte EVENT_QUE_OVERFLOW =((byte)0x35); // ANT event que has overflowed and lost 1 or more events
public static final byte EVENT_CLK_ERROR =((byte)0x36); // debug event for XOSC16M on LE1
public static final byte SCRIPT_FULL_ERROR =((byte)0x40); // error writing to script, memory is full
public static final byte SCRIPT_WRITE_ERROR =((byte)0x41); // error writing to script, bytes not written correctly
public static final byte SCRIPT_INVALID_PAGE_ERROR =((byte)0x42); // error accessing script page
public static final byte SCRIPT_LOCKED_ERROR =((byte)0x43); // the scripts are locked and can't be dumped
public static final byte NO_RESPONSE_MESSAGE =((byte)0x50); // returned to the Command_SerialMessageProcess function, so no reply message is generated
public static final byte RETURN_TO_MFG =((byte)0x51); // default return to any mesg when the module determines that the mfg procedure has not been fully completed
public static final byte FIT_ACTIVE_SEARCH_TIMEOUT =((byte)0x60); // Fit1 only event added for timeout of the pairing state after the Fit module becomes active
public static final byte FIT_WATCH_PAIR =((byte)0x61); // Fit1 only
public static final byte FIT_WATCH_UNPAIR =((byte)0x62); // Fit1 only
public static final byte USB_STRING_WRITE_FAIL =((byte)0x70);
// Internal only events below this point
public static final byte INTERNAL_ONLY_EVENTS =((byte)0x80);
public static final byte EVENT_RX =((byte)0x80); // INTERNAL: Event for a receive message
public static final byte EVENT_NEW_CHANNEL =((byte)0x81); // INTERNAL: EVENT for a new active channel
public static final byte EVENT_PASS_THRU =((byte)0x82); // INTERNAL: Event to allow an upper stack events to pass through lower stacks
public static final byte EVENT_BLOCKED =((byte)0xFF); // INTERNAL: Event to replace any event we do not wish to go out, will also zero the size of the Tx message
///////////////////////////////////////////////////////
// Script Command Codes
///////////////////////////////////////////////////////
public static final byte SCRIPT_CMD_FORMAT =((byte)0x00);
public static final byte SCRIPT_CMD_DUMP =((byte)0x01);
public static final byte SCRIPT_CMD_SET_DEFAULT_SECTOR =((byte)0x02);
public static final byte SCRIPT_CMD_END_SECTOR =((byte)0x03);
public static final byte SCRIPT_CMD_END_DUMP =((byte)0x04);
public static final byte SCRIPT_CMD_LOCK =((byte)0x05);
///////////////////////////////////////////////////////
// Reset Mesg Codes
///////////////////////////////////////////////////////
public static final byte RESET_FLAGS_MASK =((byte)0xE0);
public static final byte RESET_SUSPEND =((byte)0x80); // this must follow bitfield def
public static final byte RESET_SYNC =((byte)0x40); // this must follow bitfield def
public static final byte RESET_CMD =((byte)0x20); // this must follow bitfield def
public static final byte RESET_WDT =((byte)0x02);
public static final byte RESET_RST =((byte)0x01);
public static final byte RESET_POR =((byte)0x00);
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,37 @@
/*
* Copyright 2010 Dynastream Innovations Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.dsi.ant;
/**
* Manages the ANT Interface
*
* @hide
*/
public interface AntInterfaceIntent {
public static final String STATUS = "com.dsi.ant.rx.intent.STATUS";
public static final String ANT_MESSAGE = "com.dsi.ant.intent.ANT_MESSAGE";
public static final String ANT_INTERFACE_CLAIMED_PID = "com.dsi.ant.intent.ANT_INTERFACE_CLAIMED_PID";
public static final String ANT_ENABLED_ACTION = "com.dsi.ant.intent.action.ANT_ENABLED";
public static final String ANT_DISABLED_ACTION = "com.dsi.ant.intent.action.ANT_DISABLED";
public static final String ANT_RESET_ACTION = "com.dsi.ant.intent.action.ANT_RESET";
public static final String ANT_INTERFACE_CLAIMED_ACTION = "com.dsi.ant.intent.action.ANT_INTERFACE_CLAIMED_ACTION";
public static final String ANT_RX_MESSAGE_ACTION = "com.dsi.ant.intent.action.ANT_RX_MESSAGE_ACTION";
}
+279
View File
@@ -0,0 +1,279 @@
/*
* Copyright 2010 Dynastream Innovations Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.dsi.ant;
/**
* Public API for controlling the Ant Service.
* AntMesg contains definitions for ANT message IDs
*
* @hide
*/
public class AntMesg {
/////////////////////////////////////////////////////////////////////////////
// HCI VS Message Format
// Messages are in the format:
//
// Outgoing ANT messages (host -> ANT chip)
// 01 D1 FD XX YY YY II JJ ------
// ^ ^ ^ ^
// | HCI framing | | ANT Mesg |
//
// where: 01 is the 1 byte HCI packet Identifier (HCI Command packet)
// D1 FD is the 2 byte HCI op code (0xFDD1 stored in little endian)
// XX is the 1 byte Length of all parameters in bytes (number of bytes in the HCI packet after this byte)
// YY YY is the 2 byte Parameter describing the length of the entire ANT message (II JJ ------) stored in little endian
// II is the 1 byte size of the ANT message (0-249)
// JJ is the 1 byte ID of the ANT message (1-255, 0 is invalid)
// ------ is the data of the ANT message (0-249 bytes of data)
//
// Incoming HCI Command Complete for ANT message command (host <- ANT chip)
// 04 0E 04 01 D1 FD ZZ
//
// where: 04 is the 1 byte HCI packet Identifier (HCI Event packet)
// 0E is the 1 byte HCI event (Command Complete)
// 04 is the 1 byte Length of all parameters in bytes (there are 4 bytes)
// 01 is the 1 byte Number of parameters in the packet (there is 1 parameter)
// D1 FD is the 2 byte HCI Op code of the command (0xFDD1 stored in little endian)
// ZZ is the 1 byte response to the command (0x00 - Command Successful
// 0x1F - Returned if the receive message queue of the ANT chip is full, the command should be retried
// Other - Any other non-zero response code indicates an error)
//
// Incoming ANT messages (host <- ANT chip)
// 04 FF XX 00 05 YY YY II JJ ------
// ^ ^ ^ ^
// | HCI framing | | ANT Mesg |
//
// where: 04 is the 1 byte HCI packet Identifier (HCI Event packet)
// FF is the 1 byte HCI event code (0xFF Vendor Specific event)
// XX is the 1 byte Length of all parameters in bytes (number of bytes in the HCI packet after this byte)
// 00 05 is the 2 byte vendor specific event code for ANT messages (0x0500 stored in little endian)
// YY YY is the 2 byte Parameter describing the length of the entire ANT message (II JJ ------) stored in little endian
// II is the 1 byte size of the ANT message (0-249)
// JJ is the 1 byte ID of the ANT message (1-255, 0 is invalid)
// ------ is the data of the ANT message (0-249 bytes of data)
//
/////////////////////////////////////////////////////////////////////////////
public static final byte MESG_SYNC_SIZE =((byte)0); // Ant messages are embedded in HCI messages we do not include a sync byte
public static final byte MESG_SIZE_SIZE =((byte)1);
public static final byte MESG_ID_SIZE =((byte)1);
public static final byte MESG_CHANNEL_NUM_SIZE =((byte)1);
public static final byte MESG_EXT_MESG_BF_SIZE =((byte)1); // NOTE: this could increase in the future
public static final byte MESG_CHECKSUM_SIZE =((byte)0); // Ant messages are embedded in HCI messages we do not include a checksum
public static final byte MESG_DATA_SIZE =((byte)9);
// The largest serial message is an ANT data message with all of the extended fields
public static final byte MESG_ANT_MAX_PAYLOAD_SIZE =AntDefine.ANT_STANDARD_DATA_PAYLOAD_SIZE;
public static final byte MESG_MAX_EXT_DATA_SIZE =(AntDefine.ANT_EXT_MESG_DEVICE_ID_FIELD_SIZE + AntDefine.ANT_EXT_STRING_SIZE); // ANT device ID (4 bytes) + Padding for ANT EXT string size(19 bytes)
public static final byte MESG_MAX_DATA_SIZE =(MESG_ANT_MAX_PAYLOAD_SIZE + MESG_EXT_MESG_BF_SIZE + MESG_MAX_EXT_DATA_SIZE); // ANT data payload (8 bytes) + extended bitfield (1 byte) + extended data (10 bytes)
public static final byte MESG_MAX_SIZE_VALUE =(MESG_MAX_DATA_SIZE + MESG_CHANNEL_NUM_SIZE); // this is the maximum value that the serial message size value is allowed to be
public static final byte MESG_BUFFER_SIZE =(MESG_SIZE_SIZE + MESG_ID_SIZE + MESG_CHANNEL_NUM_SIZE + MESG_MAX_DATA_SIZE + MESG_CHECKSUM_SIZE);
public static final byte MESG_FRAMED_SIZE =(MESG_ID_SIZE + MESG_CHANNEL_NUM_SIZE + MESG_MAX_DATA_SIZE);
public static final byte MESG_HEADER_SIZE =(MESG_SYNC_SIZE + MESG_SIZE_SIZE + MESG_ID_SIZE);
public static final byte MESG_FRAME_SIZE =(MESG_HEADER_SIZE + MESG_CHECKSUM_SIZE);
public static final byte MESG_MAX_SIZE =(MESG_MAX_DATA_SIZE + MESG_FRAME_SIZE);
public static final byte MESG_SIZE_OFFSET =(MESG_SYNC_SIZE);
public static final byte MESG_ID_OFFSET =(MESG_SYNC_SIZE + MESG_SIZE_SIZE);
public static final byte MESG_DATA_OFFSET =(MESG_HEADER_SIZE);
public static final byte MESG_RECOMMENDED_BUFFER_SIZE =((byte) 64); // This is the recommended size for serial message buffers if there are no RAM restrictions on the system
//////////////////////////////////////////////
// Message ID's
//////////////////////////////////////////////
public static final byte MESG_INVALID_ID =((byte)0x00);
public static final byte MESG_EVENT_ID =((byte)0x01);
public static final byte MESG_VERSION_ID =((byte)0x3E);
public static final byte MESG_RESPONSE_EVENT_ID =((byte)0x40);
public static final byte MESG_UNASSIGN_CHANNEL_ID =((byte)0x41);
public static final byte MESG_ASSIGN_CHANNEL_ID =((byte)0x42);
public static final byte MESG_CHANNEL_MESG_PERIOD_ID =((byte)0x43);
public static final byte MESG_CHANNEL_SEARCH_TIMEOUT_ID =((byte)0x44);
public static final byte MESG_CHANNEL_RADIO_FREQ_ID =((byte)0x45);
public static final byte MESG_NETWORK_KEY_ID =((byte)0x46);
public static final byte MESG_RADIO_TX_POWER_ID =((byte)0x47);
public static final byte MESG_RADIO_CW_MODE_ID =((byte)0x48);
public static final byte MESG_SYSTEM_RESET_ID =((byte)0x4A);
public static final byte MESG_OPEN_CHANNEL_ID =((byte)0x4B);
public static final byte MESG_CLOSE_CHANNEL_ID =((byte)0x4C);
public static final byte MESG_REQUEST_ID =((byte)0x4D);
public static final byte MESG_BROADCAST_DATA_ID =((byte)0x4E);
public static final byte MESG_ACKNOWLEDGED_DATA_ID =((byte)0x4F);
public static final byte MESG_BURST_DATA_ID =((byte)0x50);
public static final byte MESG_CHANNEL_ID_ID =((byte)0x51);
public static final byte MESG_CHANNEL_STATUS_ID =((byte)0x52);
public static final byte MESG_RADIO_CW_INIT_ID =((byte)0x53);
public static final byte MESG_CAPABILITIES_ID =((byte)0x54);
public static final byte MESG_STACKLIMIT_ID =((byte)0x55);
public static final byte MESG_SCRIPT_DATA_ID =((byte)0x56);
public static final byte MESG_SCRIPT_CMD_ID =((byte)0x57);
public static final byte MESG_ID_LIST_ADD_ID =((byte)0x59);
public static final byte MESG_ID_LIST_CONFIG_ID =((byte)0x5A);
public static final byte MESG_OPEN_RX_SCAN_ID =((byte)0x5B);
public static final byte MESG_EXT_CHANNEL_RADIO_FREQ_ID =((byte)0x5C); // OBSOLETE: (for 905 radio)
public static final byte MESG_EXT_BROADCAST_DATA_ID =((byte)0x5D);
public static final byte MESG_EXT_ACKNOWLEDGED_DATA_ID =((byte)0x5E);
public static final byte MESG_EXT_BURST_DATA_ID =((byte)0x5F);
public static final byte MESG_CHANNEL_RADIO_TX_POWER_ID =((byte)0x60);
public static final byte MESG_GET_SERIAL_NUM_ID =((byte)0x61);
public static final byte MESG_GET_TEMP_CAL_ID =((byte)0x62);
public static final byte MESG_SET_LP_SEARCH_TIMEOUT_ID =((byte)0x63);
public static final byte MESG_SET_TX_SEARCH_ON_NEXT_ID =((byte)0x64);
public static final byte MESG_SERIAL_NUM_SET_CHANNEL_ID_ID =((byte)0x65);
public static final byte MESG_RX_EXT_MESGS_ENABLE_ID =((byte)0x66);
public static final byte MESG_RADIO_CONFIG_ALWAYS_ID =((byte)0x67);
public static final byte MESG_ENABLE_LED_FLASH_ID =((byte)0x68);
public static final byte MESG_XTAL_ENABLE_ID =((byte)0x6D);
public static final byte MESG_STARTUP_MESG_ID =((byte)0x6F);
public static final byte MESG_AUTO_FREQ_CONFIG_ID =((byte)0x70);
public static final byte MESG_PROX_SEARCH_CONFIG_ID =((byte)0x71);
public static final byte MESG_EVENT_BUFFERING_CONFIG_ID =((byte)0x74);
public static final byte MESG_CUBE_CMD_ID =((byte)0x80);
public static final byte MESG_GET_PIN_DIODE_CONTROL_ID =((byte)0x8D);
public static final byte MESG_PIN_DIODE_CONTROL_ID =((byte)0x8E);
public static final byte MESG_FIT1_SET_AGC_ID =((byte)0x8F);
public static final byte MESG_FIT1_SET_EQUIP_STATE_ID =((byte)0x91); // *** CONFLICT: w/ Sensrcore, Fit1 will never have sensrcore enabled
// Sensrcore Messages
public static final byte MESG_SET_CHANNEL_INPUT_MASK_ID =((byte)0x90);
public static final byte MESG_SET_CHANNEL_DATA_TYPE_ID =((byte)0x91);
public static final byte MESG_READ_PINS_FOR_SECT_ID =((byte)0x92);
public static final byte MESG_TIMER_SELECT_ID =((byte)0x93);
public static final byte MESG_ATOD_SETTINGS_ID =((byte)0x94);
public static final byte MESG_SET_SHARED_ADDRESS_ID =((byte)0x95);
public static final byte MESG_ATOD_EXTERNAL_ENABLE_ID =((byte)0x96);
public static final byte MESG_ATOD_PIN_SETUP_ID =((byte)0x97);
public static final byte MESG_SETUP_ALARM_ID =((byte)0x98);
public static final byte MESG_ALARM_VARIABLE_MODIFY_TEST_ID =((byte)0x99);
public static final byte MESG_PARTIAL_RESET_ID =((byte)0x9A);
public static final byte MESG_OVERWRITE_TEMP_CAL_ID =((byte)0x9B);
public static final byte MESG_SERIAL_PASSTHRU_SETTINGS_ID =((byte)0x9C);
public static final byte MESG_BIST_ID =((byte)0xAA);
public static final byte MESG_UNLOCK_INTERFACE_ID =((byte)0xAD);
public static final byte MESG_SERIAL_ERROR_ID =((byte)0xAE);
public static final byte MESG_SET_ID_STRING_ID =((byte)0xAF);
public static final byte MESG_PORT_GET_IO_STATE_ID =((byte)0xB4);
public static final byte MESG_PORT_SET_IO_STATE_ID =((byte)0xB5);
public static final byte MESG_SLEEP_ID =((byte)0xC5);
public static final byte MESG_GET_GRMN_ESN_ID =((byte)0xC6);
public static final byte MESG_SET_USB_INFO_ID =((byte)0xC7);
public static final byte MESG_COMMAND_COMPLETE_RESPONSE_ID =((byte)0xC8);
//////////////////////////////////////////////
// Command complete results
//////////////////////////////////////////////
public static final byte MESG_COMMAND_COMPLETE_SUCCESS =((byte)0x00);
public static final byte MESG_COMMAND_COMPLETE_RETRY =((byte)0x1F);
//////////////////////////////////////////////
// Message Sizes
//////////////////////////////////////////////
public static final byte MESG_INVALID_SIZE =((byte)0);
public static final byte MESG_VERSION_SIZE =((byte)13);
public static final byte MESG_RESPONSE_EVENT_SIZE =((byte)3);
public static final byte MESG_CHANNEL_STATUS_SIZE =((byte)2);
public static final byte MESG_UNASSIGN_CHANNEL_SIZE =((byte)1);
public static final byte MESG_ASSIGN_CHANNEL_SIZE =((byte)3);
public static final byte MESG_CHANNEL_ID_SIZE =((byte)5);
public static final byte MESG_CHANNEL_MESG_PERIOD_SIZE =((byte)3);
public static final byte MESG_CHANNEL_SEARCH_TIMEOUT_SIZE =((byte)2);
public static final byte MESG_CHANNEL_RADIO_FREQ_SIZE =((byte)2);
public static final byte MESG_CHANNEL_RADIO_TX_POWER_SIZE =((byte)2);
public static final byte MESG_NETWORK_KEY_SIZE =((byte)9);
public static final byte MESG_RADIO_TX_POWER_SIZE =((byte)2);
public static final byte MESG_RADIO_CW_MODE_SIZE =((byte)3);
public static final byte MESG_RADIO_CW_INIT_SIZE =((byte)1);
public static final byte MESG_SYSTEM_RESET_SIZE =((byte)1);
public static final byte MESG_OPEN_CHANNEL_SIZE =((byte)1);
public static final byte MESG_CLOSE_CHANNEL_SIZE =((byte)1);
public static final byte MESG_REQUEST_SIZE =((byte)2);
public static final byte MESG_CAPABILITIES_SIZE =((byte)6);
public static final byte MESG_STACKLIMIT_SIZE =((byte)2);
public static final byte MESG_SCRIPT_DATA_SIZE =((byte)10);
public static final byte MESG_SCRIPT_CMD_SIZE =((byte)3);
public static final byte MESG_ID_LIST_ADD_SIZE =((byte)6);
public static final byte MESG_ID_LIST_CONFIG_SIZE =((byte)3);
public static final byte MESG_OPEN_RX_SCAN_SIZE =((byte)1);
public static final byte MESG_EXT_CHANNEL_RADIO_FREQ_SIZE =((byte)3);
public static final byte MESG_RADIO_CONFIG_ALWAYS_SIZE =((byte)2);
public static final byte MESG_RX_EXT_MESGS_ENABLE_SIZE =((byte)2);
public static final byte MESG_SET_TX_SEARCH_ON_NEXT_SIZE =((byte)2);
public static final byte MESG_SET_LP_SEARCH_TIMEOUT_SIZE =((byte)2);
public static final byte MESG_SERIAL_NUM_SET_CHANNEL_ID_SIZE =((byte)3);
public static final byte MESG_ENABLE_LED_FLASH_SIZE =((byte)2);
public static final byte MESG_GET_SERIAL_NUM_SIZE =((byte)4);
public static final byte MESG_GET_TEMP_CAL_SIZE =((byte)4);
public static final byte MESG_XTAL_ENABLE_SIZE =((byte)1);
public static final byte MESG_STARTUP_MESG_SIZE =((byte)1);
public static final byte MESG_AUTO_FREQ_CONFIG_SIZE =((byte)4);
public static final byte MESG_PROX_SEARCH_CONFIG_SIZE =((byte)2);
public static final byte MESG_GET_PIN_DIODE_CONTROL_SIZE =((byte)1);
public static final byte MESG_PIN_DIODE_CONTROL_ID_SIZE =((byte)2);
public static final byte MESG_FIT1_SET_EQUIP_STATE_SIZE =((byte)2);
public static final byte MESG_FIT1_SET_AGC_SIZE =((byte)3);
public static final byte MESG_BIST_SIZE =((byte)6);
public static final byte MESG_UNLOCK_INTERFACE_SIZE =((byte)1);
public static final byte MESG_SET_SHARED_ADDRESS_SIZE =((byte)3);
public static final byte MESG_GET_GRMN_ESN_SIZE =((byte)5);
public static final byte MESG_PORT_SET_IO_STATE_SIZE =((byte)5);
public static final byte MESG_EVENT_BUFFERING_CONFIG_SIZE =((byte)6);
public static final byte MESG_SLEEP_SIZE =((byte)1);
public static final byte MESG_EXT_DATA_SIZE =((byte)13);
protected AntMesg()
{ }
}
+70
View File
@@ -0,0 +1,70 @@
/*
* Copyright 2010 Dynastream Innovations Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.dsi.ant;
/**
* The Android Ant API is not finalized, and *will* be updated and expanded.
* This is the base level ANT messaging API and gives any application full
* control over the ANT radio HW. Caution should be exercised when using
* this interface.
*
* Public API for controlling the Ant Service.
*
* {@hide}
*/
interface IAnt {
// Since version 1 (1.0):
boolean enable();
boolean disable();
boolean isEnabled();
boolean ANTTxMessage(in byte[] message);
boolean ANTResetSystem();
boolean ANTUnassignChannel(byte channelNumber);
boolean ANTAssignChannel(byte channelNumber, byte channelType, byte networkNumber);
boolean ANTSetChannelId(byte channelNumber, int deviceNumber, byte deviceType, byte txType);
boolean ANTSetChannelPeriod(byte channelNumber, int channelPeriod);
boolean ANTSetChannelRFFreq(byte channelNumber, byte radioFrequency);
boolean ANTSetChannelSearchTimeout(byte channelNumber, byte searchTimeout);
boolean ANTSetLowPriorityChannelSearchTimeout(byte channelNumber, byte searchTimeout);
boolean ANTSetProximitySearch(byte channelNumber, byte searchThreshold);
boolean ANTSetChannelTxPower(byte channelNumber, byte txPower);
boolean ANTAddChannelId(byte channelNumber, int deviceNumber, byte deviceType, byte txType, byte listIndex);
boolean ANTConfigList(byte channelNumber, byte listSize, byte exclude);
boolean ANTOpenChannel(byte channelNumber);
boolean ANTCloseChannel(byte channelNumber);
boolean ANTRequestMessage(byte channelNumber, byte messageID);
boolean ANTSendBroadcastData(byte channelNumber, in byte[] txBuffer);
boolean ANTSendAcknowledgedData(byte channelNumber, in byte[] txBuffer);
boolean ANTSendBurstTransferPacket(byte control, in byte[] txBuffer);
int ANTSendBurstTransfer(byte channelNumber, in byte[] txBuffer);
int ANTTransmitBurst(byte channelNumber, in byte[] txBuffer, int initialPacket, boolean containsEndOfBurst);
// Since version 4 (1.3):
boolean ANTConfigEventBuffering(int screenOnFlushTimerInterval, int screenOnFlushBufferThreshold, int screenOffFlushTimerInterval, int screenOffFlushBufferThreshold);
// Since version 2 (1.1):
boolean ANTDisableEventBuffering();
// Since version 3 (1.2):
int getServiceLibraryVersionCode();
String getServiceLibraryVersionName();
}
+77
View File
@@ -0,0 +1,77 @@
/*
* Copyright 2011 Dynastream Innovations Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.dsi.ant;
/**
* The Android Ant API is not finalized, and *will* be updated and expanded.
* This is the base level ANT messaging API and gives any application full
* control over the ANT radio HW. Caution should be exercised when using
* this interface.
*
* Public API for controlling the Ant Service.
*
* {@hide}
*/
interface IAnt_6 {
// Since version 1 (1.0):
boolean enable();
boolean disable();
boolean isEnabled();
boolean ANTTxMessage(in byte[] message);
boolean ANTResetSystem();
boolean ANTUnassignChannel(byte channelNumber);
boolean ANTAssignChannel(byte channelNumber, byte channelType, byte networkNumber);
boolean ANTSetChannelId(byte channelNumber, int deviceNumber, byte deviceType, byte txType);
boolean ANTSetChannelPeriod(byte channelNumber, int channelPeriod);
boolean ANTSetChannelRFFreq(byte channelNumber, byte radioFrequency);
boolean ANTSetChannelSearchTimeout(byte channelNumber, byte searchTimeout);
boolean ANTSetLowPriorityChannelSearchTimeout(byte channelNumber, byte searchTimeout);
boolean ANTSetProximitySearch(byte channelNumber, byte searchThreshold);
boolean ANTSetChannelTxPower(byte channelNumber, byte txPower);
boolean ANTAddChannelId(byte channelNumber, int deviceNumber, byte deviceType, byte txType, byte listIndex);
boolean ANTConfigList(byte channelNumber, byte listSize, byte exclude);
boolean ANTOpenChannel(byte channelNumber);
boolean ANTCloseChannel(byte channelNumber);
boolean ANTRequestMessage(byte channelNumber, byte messageID);
boolean ANTSendBroadcastData(byte channelNumber, in byte[] txBuffer);
boolean ANTSendAcknowledgedData(byte channelNumber, in byte[] txBuffer);
boolean ANTSendBurstTransferPacket(byte control, in byte[] txBuffer);
int ANTSendBurstTransfer(byte channelNumber, in byte[] txBuffer);
int ANTTransmitBurst(byte channelNumber, in byte[] txBuffer, int initialPacket, boolean containsEndOfBurst);
// Since version 4 (1.3):
boolean ANTConfigEventBuffering(int screenOnFlushTimerInterval, int screenOnFlushBufferThreshold, int screenOffFlushTimerInterval, int screenOffFlushBufferThreshold);
// Since version 2 (1.1):
boolean ANTDisableEventBuffering();
// Since version 3 (1.2):
int getServiceLibraryVersionCode();
String getServiceLibraryVersionName();
// Since version 6 (1.5):
boolean claimInterface();
boolean requestForceClaimInterface(String appName);
boolean stopRequestForceClaimInterface();
boolean releaseInterface();
boolean hasClaimedInterface();
}
@@ -0,0 +1,29 @@
/*
* Copyright 2010 Dynastream Innovations Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.dsi.ant;
/**
*
* API for configuring the non-ANT components of the ANT Service.
*
* {@hide}
*/
interface IServiceSettings {
void debugLogging(boolean debug);
boolean setNumCombinedBurstPackets(int numPackets);
int getNumCombinedBurstPackets();
}
+35
View File
@@ -0,0 +1,35 @@
/*
* Copyright 2011 Dynastream Innovations Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.dsi.ant;
/**
* Defines version numbers
*
* @hide
*/
public class Version {
//////////////////////////////////////////////
// Library Version Information
//////////////////////////////////////////////
public static final int ANT_LIBRARY_VERSION_CODE = 6;
public static final int ANT_LIBRARY_VERSION_MAJOR = 2;
public static final int ANT_LIBRARY_VERSION_MINOR = 0;
public static final String ANT_LIBRARY_VERSION_NAME = String.valueOf(ANT_LIBRARY_VERSION_MAJOR) + "." + String.valueOf(ANT_LIBRARY_VERSION_MINOR);
}
@@ -0,0 +1,20 @@
package com.dsi.ant.exception;
public class AntInterfaceException extends Exception
{
/**
*
*/
private static final long serialVersionUID = -7278855366167722274L;
public AntInterfaceException()
{
this("Unknown ANT Interface error");
}
public AntInterfaceException(String string)
{
super(string);
}
}
@@ -0,0 +1,23 @@
package com.dsi.ant.exception;
import android.os.RemoteException;
public class AntRemoteException extends AntInterfaceException
{
/**
*
*/
private static final long serialVersionUID = 8950974759973459561L;
public AntRemoteException(RemoteException e)
{
this("ANT Interface error, ANT Radio Service remote error: "+e.toString(), e);
}
public AntRemoteException(String string, RemoteException e)
{
super(string);
this.initCause(e);
}
}
@@ -0,0 +1,19 @@
package com.dsi.ant.exception;
public class AntServiceNotConnectedException extends AntInterfaceException
{
/**
*
*/
private static final long serialVersionUID = -2085081032170129309L;
public AntServiceNotConnectedException()
{
this("ANT Interface error, ANT Radio Service not connected.");
}
public AntServiceNotConnectedException(String string)
{
super(string);
}
}
@@ -0,0 +1,139 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks;
import com.google.android.maps.mytracks.R;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.util.Log;
/**
* Choose which account to upload track information to.
* @author Sandor Dornbush
*/
public class AccountChooser {
/**
* The last selected account.
*/
private int selectedAccountIndex = -1;
private Account selectedAccount = null;
/**
* An interface for receiving updates once the user has selected the account.
*/
public interface AccountHandler {
/**
* Handle the account being selected.
* @param account The selected account or null if none could be found
*/
public void onAccountSelected(Account account);
}
/**
* Chooses the best account to upload to.
* If no account is found the user will be alerted.
* If only one account is found that will be used.
* If multiple accounts are found the user will be allowed to choose.
*
* @param activity The parent activity
* @param handler The handler to be notified when an account has been selected
*/
public void chooseAccount(final Activity activity,
final AccountHandler handler) {
final Account[] accounts = AccountManager.get(activity)
.getAccountsByType(Constants.ACCOUNT_TYPE);
if (accounts.length < 1) {
alertNoAccounts(activity, handler);
return;
}
if (accounts.length == 1) {
handler.onAccountSelected(accounts[0]);
return;
}
// TODO This should be read out of a preference.
if (selectedAccount != null) {
handler.onAccountSelected(selectedAccount);
return;
}
// Let the user choose.
Log.e(Constants.TAG, "Multiple matching accounts found.");
final AlertDialog.Builder builder = new AlertDialog.Builder(activity);
builder.setTitle(R.string.choose_account_title);
builder.setCancelable(false);
builder.setPositiveButton(R.string.ok,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
if (selectedAccountIndex >= 0) {
selectedAccount = accounts[selectedAccountIndex];
}
handler.onAccountSelected(selectedAccount);
}
});
builder.setNegativeButton(R.string.cancel,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
handler.onAccountSelected(null);
}
});
String[] choices = new String[accounts.length];
for (int i = 0; i < accounts.length; i++) {
choices[i] = accounts[i].name;
}
builder.setSingleChoiceItems(choices, selectedAccountIndex,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
selectedAccountIndex = which;
}
});
builder.show();
}
public void setChosenAccount(String accountName, String accountType) {
selectedAccount = new Account(accountName, accountType);
}
/**
* Puts up a dialog alerting the user that no suitable account was found.
*/
private void alertNoAccounts(final Activity activity,
final AccountHandler handler) {
Log.e(Constants.TAG, "No matching accounts found.");
final AlertDialog.Builder builder = new AlertDialog.Builder(activity);
builder.setTitle(R.string.no_account_found_title);
builder.setMessage(R.string.no_account_found);
builder.setCancelable(true);
builder.setPositiveButton(R.string.ok,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
handler.onAccountSelected(null);
}
});
builder.show();
}
}
@@ -0,0 +1,115 @@
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.maps.mytracks.R;
import android.app.Activity;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.os.Bundle;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.Window;
import android.widget.ScrollView;
import android.widget.TextView;
import java.util.List;
/**
* Activity for viewing the combined statistics for all the recorded tracks.
*
* Other features to add - menu items to change setings.
*
* @author Fergus Nelson
*/
public class AggregatedStatsActivity extends Activity implements
OnSharedPreferenceChangeListener {
private final StatsUtilities utils;
private MyTracksProviderUtils tracksProvider;
private boolean metricUnits = true;
public AggregatedStatsActivity() {
this.utils = new StatsUtilities(this);
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
String key) {
Log.d(Constants.TAG, "StatsActivity: onSharedPreferences changed "
+ key);
if (key != null) {
if (key.equals(getString(R.string.metric_units_key))) {
metricUnits = sharedPreferences.getBoolean(
getString(R.string.metric_units_key), true);
utils.setMetricUnits(metricUnits);
utils.updateUnits();
loadAggregatedStats();
}
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.tracksProvider = MyTracksProviderUtils.Factory.get(this);
// We don't need a window title bar:
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.stats);
ScrollView sv = ((ScrollView) findViewById(R.id.scrolly));
sv.setScrollBarStyle(ScrollView.SCROLLBARS_OUTSIDE_INSET);
SharedPreferences preferences = getSharedPreferences(
Constants.SETTINGS_NAME, 0);
if (preferences != null) {
metricUnits = preferences.getBoolean(getString(R.string.metric_units_key), true);
preferences.registerOnSharedPreferenceChangeListener(this);
}
utils.setMetricUnits(metricUnits);
utils.updateUnits();
utils.setSpeedLabel(R.id.speed_label, R.string.speed, R.string.pace_label);
utils.setSpeedLabels();
DisplayMetrics metrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(metrics);
if (metrics.heightPixels > 600) {
((TextView) findViewById(R.id.speed_register)).setTextSize(80.0f);
}
loadAggregatedStats();
}
/**
* 1. Reads tracks from the db
* 2. Merges the trip stats from the tracks
* 3. Updates the view
*/
private void loadAggregatedStats() {
List<Track> tracks = retrieveTracks();
TripStatistics rollingStats = null;
if (!tracks.isEmpty()) {
rollingStats = new TripStatistics(tracks.iterator().next()
.getStatistics());
for (int i = 1; i < tracks.size(); i++) {
rollingStats.merge(tracks.get(i).getStatistics());
}
}
updateView(rollingStats);
}
private List<Track> retrieveTracks() {
return tracksProvider.getAllTracks();
}
private void updateView(TripStatistics aggStats) {
if (aggStats != null) {
utils.setAllStats(aggStats);
}
}
}
@@ -0,0 +1,62 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks;
import com.google.android.maps.mytracks.R;
import android.content.Context;
import android.preference.Preference;
import android.util.AttributeSet;
/**
* A preference for an ANT device pairing.
* Currently this shows the ID and lets the user clear that ID for future pairing.
* TODO: Support pairing from this preference.
*
* @author Sandor Dornbush
*/
public class AntPreference extends Preference {
public AntPreference(Context context) {
super(context);
init();
}
public AntPreference(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
int sensorId = getPersistedInt(0);
if (sensorId == 0) {
setSummary(R.string.settings_ant_not_paired);
} else {
setSummary(String.format(getContext().getString(R.string.settings_ant_paired), sensorId));
}
// Add actions to allow repairing.
setOnPreferenceClickListener(
new OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
AntPreference.this.persistInt(0);
setSummary(R.string.settings_ant_not_paired);
return true;
}
});
}
}
@@ -0,0 +1,57 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks;
import static android.content.Intent.ACTION_BOOT_COMPLETED;
import static com.google.android.apps.mytracks.Constants.RESUME_TRACK_EXTRA_NAME;
import static com.google.android.apps.mytracks.Constants.TAG;
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);
startIntent.putExtra(RESUME_TRACK_EXTRA_NAME, true);
context.startService(startIntent);
} else {
Log.w(TAG, "BootReceiver: unsupported action");
}
}
}
@@ -0,0 +1,478 @@
/*
* 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 static com.google.android.apps.mytracks.Constants.TAG;
import com.google.android.apps.mytracks.ChartView.Mode;
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.TrackDataHub.ListenerDataType;
import com.google.android.apps.mytracks.content.TrackDataListener;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerFactory;
import com.google.android.apps.mytracks.stats.DoubleBuffer;
import com.google.android.apps.mytracks.stats.TripStatisticsBuilder;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.apps.mytracks.util.LocationUtils;
import com.google.android.apps.mytracks.util.UnitConversions;
import com.google.android.maps.mytracks.R;
import android.app.Activity;
import android.app.Dialog;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.location.Location;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.view.Window;
import android.widget.LinearLayout;
import android.widget.ZoomControls;
import java.util.ArrayList;
import java.util.EnumSet;
/**
* An activity that displays a chart from the track point provider.
*
* @author Sandor Dornbush
* @author Rodrigo Damazio
*/
public class ChartActivity extends Activity implements TrackDataListener {
private static final int CHART_SETTINGS_DIALOG = 1;
private final DoubleBuffer elevationBuffer =
new DoubleBuffer(Constants.ELEVATION_SMOOTHING_FACTOR);
private final DoubleBuffer speedBuffer =
new DoubleBuffer(Constants.SPEED_SMOOTHING_FACTOR);
private final ArrayList<double[]> pendingPoints = new ArrayList<double[]>();
private TrackDataHub dataHub;
// Stats gathered from received data.
private double profileLength = 0;
private long startTime = -1;
private Location lastLocation;
private double trackMaxSpeed;
// Modes of operation
private boolean metricUnits;
private boolean reportSpeed;
/*
* UI elements:
*/
private ChartView chartView;
private MenuItem chartSettingsMenuItem;
private LinearLayout busyPane;
private ZoomControls zoomControls;
/**
* A runnable that can be posted to the UI thread. It will remove the spinner
* (if any), enable/disable zoom controls and orange pointer as appropriate
* and redraw.
*/
private final Runnable updateChart = new Runnable() {
@Override
public void run() {
// Get a local reference in case it's set to null concurrently with this.
TrackDataHub localDataHub = dataHub;
if (localDataHub == null || isFinishing()) {
return;
}
busyPane.setVisibility(View.GONE);
zoomControls.setIsZoomInEnabled(chartView.canZoomIn());
zoomControls.setIsZoomOutEnabled(chartView.canZoomOut());
chartView.setShowPointer(localDataHub.isRecordingSelected());
chartView.invalidate();
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
Log.w(TAG, "ChartActivity.onCreate");
super.onCreate(savedInstanceState);
// The volume we want to control is the Text-To-Speech volume
int volumeStream =
new StatusAnnouncerFactory(ApiFeatures.getInstance()).getVolumeStream();
setVolumeControlStream(volumeStream);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.mytracks_charts);
ViewGroup layout = (ViewGroup) findViewById(R.id.elevation_chart);
chartView = new ChartView(this);
LayoutParams params =
new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT);
layout.addView(chartView, params);
busyPane = (LinearLayout) findViewById(R.id.elevation_busypane);
zoomControls = (ZoomControls) findViewById(R.id.elevation_zoom);
zoomControls.setOnZoomInClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
zoomIn();
}
});
zoomControls.setOnZoomOutClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
zoomOut();
}
});
}
@Override
protected void onResume() {
super.onResume();
dataHub = ((MyTracksApplication) getApplication()).getTrackDataHub();
dataHub.registerTrackDataListener(this, EnumSet.of(
ListenerDataType.SELECTED_TRACK_CHANGED,
ListenerDataType.TRACK_UPDATES,
ListenerDataType.POINT_UPDATES,
ListenerDataType.SAMPLED_OUT_POINT_UPDATES,
ListenerDataType.WAYPOINT_UPDATES,
ListenerDataType.DISPLAY_PREFERENCES));
}
@Override
protected void onPause() {
dataHub.unregisterTrackDataListener(this);
dataHub = null;
super.onPause();
}
private void zoomIn() {
chartView.zoomIn();
zoomControls.setIsZoomInEnabled(chartView.canZoomIn());
zoomControls.setIsZoomOutEnabled(chartView.canZoomOut());
}
private void zoomOut() {
chartView.zoomOut();
zoomControls.setIsZoomInEnabled(chartView.canZoomIn());
zoomControls.setIsZoomOutEnabled(chartView.canZoomOut());
}
private void setMode(Mode newMode) {
if (chartView.getMode() != newMode) {
chartView.setMode(newMode);
dataHub.reloadDataForListener(this);
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
chartSettingsMenuItem =
menu.add(0, Constants.MENU_CHART_SETTINGS, 0,
R.string.chart_settings);
chartSettingsMenuItem.setIcon(R.drawable.chart_settings);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case Constants.MENU_CHART_SETTINGS:
showDialog(CHART_SETTINGS_DIALOG);
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
protected Dialog onCreateDialog(int id) {
if (id == CHART_SETTINGS_DIALOG) {
final ChartSettingsDialog settingsDialog = new ChartSettingsDialog(this);
settingsDialog.setOnClickListener(new OnClickListener() {
@Override
public void onClick(DialogInterface arg0, int which) {
if (which != DialogInterface.BUTTON_POSITIVE) {
return;
}
for (int i = 0; i < ChartView.NUM_SERIES; i++) {
chartView.setChartValueSeriesEnabled(i, settingsDialog.isSeriesEnabled(i));
}
setMode(settingsDialog.getMode());
chartView.postInvalidate();
}
});
return settingsDialog;
}
return super.onCreateDialog(id);
}
@Override
protected void onPrepareDialog(int id, Dialog dialog) {
super.onPrepareDialog(id, dialog);
if (id == CHART_SETTINGS_DIALOG) {
prepareSettingsDialog((ChartSettingsDialog) dialog);
}
}
private void prepareSettingsDialog(ChartSettingsDialog settingsDialog) {
settingsDialog.setMode(chartView.getMode());
for (int i = 0; i < ChartView.NUM_SERIES; i++) {
settingsDialog.setSeriesEnabled(i, chartView.isChartValueSeriesEnabled(i));
}
}
/**
* Given a location, creates a new data point for the chart. A data point is
* an array double[3 or 6], where:
* data[0] = the time or distance
* data[1] = the elevation
* data[2] = the speed
* and possibly:
* data[3] = power
* data[4] = cadence
* data[5] = heart rate
*
* This must be called in order for each point.
*
* @param location the location to get data for (this method takes ownership of that location)
* @param result the resulting point to fill out
* @return the previous location, now available for reuse
*/
private void fillDataPoint(Location location, double result[]) {
double timeOrDistance = Double.NaN,
elevation = Double.NaN,
speed = Double.NaN,
power = Double.NaN,
cadence = Double.NaN,
heartRate = Double.NaN;
if (location instanceof MyTracksLocation &&
((MyTracksLocation) location).getSensorDataSet() != null) {
SensorDataSet sensorData = ((MyTracksLocation) location).getSensorDataSet();
if (sensorData.hasPower()
&& sensorData.getPower().getState() == Sensor.SensorState.SENDING
&& sensorData.getPower().hasValue()) {
power = sensorData.getPower().getValue();
}
if (sensorData.hasCadence()
&& sensorData.getCadence().getState() == Sensor.SensorState.SENDING
&& sensorData.getCadence().hasValue()) {
cadence = sensorData.getCadence().getValue();
}
if (sensorData.hasHeartRate()
&& sensorData.getHeartRate().getState() == Sensor.SensorState.SENDING
&& sensorData.getHeartRate().hasValue()) {
heartRate = sensorData.getHeartRate().getValue();
}
}
// TODO: Account for segment splits?
Mode mode = chartView.getMode();
switch (mode) {
case BY_DISTANCE:
timeOrDistance = profileLength / 1000.0;
if (lastLocation != null) {
double d = lastLocation.distanceTo(location);
if (metricUnits) {
profileLength += d;
} else {
profileLength += d * UnitConversions.KM_TO_MI;
}
}
break;
case BY_TIME:
if (startTime == -1) {
// Base case
startTime = location.getTime();
}
timeOrDistance = (location.getTime() - startTime);
break;
default:
Log.w(TAG, "ChartActivity unknown mode: " + mode);
}
elevationBuffer.setNext(metricUnits
? location.getAltitude()
: location.getAltitude() * UnitConversions.M_TO_FT);
elevation = elevationBuffer.getAverage();
if (lastLocation == null) {
if (Math.abs(location.getSpeed() - 128) > 1) {
speedBuffer.setNext(location.getSpeed());
}
} else if (TripStatisticsBuilder.isValidSpeed(
location.getTime(), location.getSpeed(), lastLocation.getTime(),
lastLocation.getSpeed(), speedBuffer)
&& (location.getSpeed() <= trackMaxSpeed)) {
speedBuffer.setNext(location.getSpeed());
}
speed = speedBuffer.getAverage() * 3.6;
if (!metricUnits) {
speed *= UnitConversions.KM_TO_MI;
}
if (!reportSpeed) {
if (speed != 0) {
// Format as hours per unit
speed = (60.0 / speed);
} else {
speed = Double.NaN;
}
}
// Keep a copy so the location can be reused.
lastLocation = location;
if (result != null) {
result[0] = timeOrDistance;
result[1] = elevation;
result[2] = speed;
result[3] = power;
result[4] = cadence;
result[5] = heartRate;
}
}
@Override
public void onProviderStateChange(ProviderState state) {
// We don't care.
}
@Override
public void onCurrentLocationChanged(Location loc) {
// We don't care.
}
@Override
public void onCurrentHeadingChanged(double heading) {
// We don't care.
}
@Override
public void onSelectedTrackChanged(Track track, boolean isRecording) {
runOnUiThread(new Runnable() {
@Override
public void run() {
busyPane.setVisibility(View.VISIBLE);
}
});
}
@Override
public void onTrackUpdated(Track track) {
if (track == null || track.getStatistics() == null) {
trackMaxSpeed = 0.0;
return;
}
trackMaxSpeed = track.getStatistics().getMaxSpeed();
}
@Override
public void clearTrackPoints() {
profileLength = 0;
lastLocation = null;
startTime = -1;
elevationBuffer.reset();
chartView.reset();
speedBuffer.reset();
pendingPoints.clear();
runOnUiThread(new Runnable() {
@Override
public void run() {
chartView.resetScroll();
}
});
}
@Override
public void onNewTrackPoint(Location loc) {
if (LocationUtils.isValidLocation(loc)) {
double[] point = new double[6];
fillDataPoint(loc, point);
pendingPoints.add(point);
}
}
@Override
public void onSampledOutTrackPoint(Location loc) {
// Still account for the point in the smoothing buffers.
fillDataPoint(loc, null);
}
@Override
public void onSegmentSplit() {
// Do nothing.
}
@Override
public void onNewTrackPointsDone() {
chartView.addDataPoints(pendingPoints);
pendingPoints.clear();
runOnUiThread(updateChart);
}
@Override
public void clearWaypoints() {
chartView.clearWaypoints();
}
@Override
public void onNewWaypoint(Waypoint wpt) {
chartView.addWaypoint(wpt);
}
@Override
public void onNewWaypointsDone() {
runOnUiThread(updateChart);
}
@Override
public boolean onUnitsChanged(boolean metric) {
boolean changed = metric != this.metricUnits;
if (!changed) return false;
this.metricUnits = metric;
chartView.setMetricUnits(metric);
return true; // Reload data
}
@SuppressWarnings("hiding")
@Override
public boolean onReportSpeedChanged(boolean reportSpeed) {
boolean changed = reportSpeed != this.reportSpeed;
if (!changed) return false;
this.reportSpeed = reportSpeed;
chartView.setReportSpeed(reportSpeed, this);
return true; // Reload data
}
}
@@ -0,0 +1,112 @@
/*
* 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.ChartView.Mode;
import com.google.android.maps.mytracks.R;
import android.app.Dialog;
import android.content.Context;
import android.os.Bundle;
import android.view.View;
import android.view.Window;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.RadioButton;
import android.widget.RadioGroup;
/**
* An activity that allows the user to set the chart settings.
*
* @author Sandor Dornbush
*/
public class ChartSettingsDialog extends Dialog {
private RadioButton distance;
private CheckBox[] series;
private OnClickListener clickListener;
public ChartSettingsDialog(Context context) {
super(context);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.chart_settings);
Button cancel = (Button) findViewById(R.id.chart_settings_cancel);
cancel.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
if (clickListener != null) {
clickListener.onClick(ChartSettingsDialog.this, BUTTON_NEGATIVE);
}
dismiss();
}
});
Button okButton = (Button) findViewById(R.id.chart_settings_ok);
okButton.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
if (clickListener != null) {
clickListener.onClick(ChartSettingsDialog.this, BUTTON_POSITIVE);
}
dismiss();
}
});
distance = (RadioButton) findViewById(R.id.chart_settings_by_distance);
series = new CheckBox[ChartView.NUM_SERIES];
series[ChartView.ELEVATION_SERIES] =
(CheckBox) findViewById(R.id.chart_settings_elevation);
series[ChartView.SPEED_SERIES] =
(CheckBox) findViewById(R.id.chart_settings_speed);
series[ChartView.POWER_SERIES] =
(CheckBox) findViewById(R.id.chart_settings_power);
series[ChartView.CADENCE_SERIES] =
(CheckBox) findViewById(R.id.chart_settings_cadence);
series[ChartView.HEART_RATE_SERIES] =
(CheckBox) findViewById(R.id.chart_settings_heart_rate);
}
public void setMode(Mode mode) {
RadioGroup rd = (RadioGroup) findViewById(R.id.chart_settings_x);
rd.check(mode == Mode.BY_DISTANCE
? R.id.chart_settings_by_distance
: R.id.chart_settings_by_time);
}
public void setSeriesEnabled(int seriesIdx, boolean enabled) {
series[seriesIdx].setChecked(enabled);
}
public Mode getMode() {
if (distance == null) return Mode.BY_DISTANCE;
return distance.isChecked() ? Mode.BY_DISTANCE : Mode.BY_TIME;
}
public boolean isSeriesEnabled(int seriesIdx) {
if (series == null) return true;
return series[seriesIdx].isChecked();
}
public void setOnClickListener(OnClickListener clickListener) {
this.clickListener = clickListener;
}
}
@@ -0,0 +1,305 @@
/*
* 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 android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Paint.Style;
import java.text.DecimalFormat;
/**
* This class encapsulates meta data about one series of values for a chart.
*
* @author Sandor Dornbush
*/
public class ChartValueSeries {
private final ExtremityMonitor monitor = new ExtremityMonitor();
private final DecimalFormat format;
private final Path path = new Path();
private final Paint fillPaint;
private final Paint strokePaint;
private final Paint labelPaint;
private final ZoomSettings zoomSettings;
private String title;
private double min;
private double max = 1.0;
private int effectiveMax;
private int effectiveMin;
private double spread;
private int interval;
private boolean enabled = true;
/**
* This class controls how effective min/max values of a {@link ChartValueSeries} are calculated.
*/
public static class ZoomSettings {
private int intervals;
private final int absoluteMin;
private final int absoluteMax;
private final int[] zoomLevels;
public ZoomSettings(int intervals, int[] zoomLevels) {
this.intervals = intervals;
this.absoluteMin = Integer.MAX_VALUE;
this.absoluteMax = Integer.MIN_VALUE;
this.zoomLevels = zoomLevels;
checkArgs();
}
public ZoomSettings(int intervals, int absoluteMin, int absoluteMax, int[] zoomLevels) {
this.intervals = intervals;
this.absoluteMin = absoluteMin;
this.absoluteMax = absoluteMax;
this.zoomLevels = zoomLevels;
checkArgs();
}
private void checkArgs() {
if (intervals <= 0 || zoomLevels == null || zoomLevels.length == 0) {
throw new IllegalArgumentException("Expecing positive intervals and non-empty zoom levels");
}
for (int i = 1; i < zoomLevels.length; ++i) {
if (zoomLevels[i] <= zoomLevels[i - 1]) {
throw new IllegalArgumentException("Expecting zoom levels in ascending order");
}
}
}
public int getIntervals() {
return intervals;
}
public int getAbsoluteMin() {
return absoluteMin;
}
public int getAbsoluteMax() {
return absoluteMax;
}
public int[] getZoomLevels() {
return zoomLevels;
}
/**
* Calculates the interval between markings given the min and max values.
* This function attempts to find the smallest zoom level that fits [min,max] after rounding
* it to the current zoom level.
*
* @param min the minimum value in the series
* @param max the maximum value in the series
* @return the calculated interval for the given range
*/
public int calculateInterval(double min, double max) {
min = Math.min(min, absoluteMin);
max = Math.max(max, absoluteMax);
for (int i = 0; i < zoomLevels.length; ++i) {
int zoomLevel = zoomLevels[i];
int roundedMin = (int)(min / zoomLevel) * zoomLevel;
if (roundedMin > min) {
roundedMin -= zoomLevel;
}
double interval = (max - roundedMin) / intervals;
if (zoomLevel >= interval) {
return zoomLevel;
}
}
return zoomLevels[zoomLevels.length - 1];
}
}
/**
* Constructs a new chart value series.
*
* @param context The context for the chart
* @param formatString The format of the decimal format for this series
* @param fill The paint for filling the chart
* @param stroke The paint for stroking the outside the chart, optional
* @param zoomSettings The settings related to zooming
*
* TODO: Get rid of Context and inject appropriate values instead.
*/
public ChartValueSeries(Context context, String formatString, int fillColor, int strokeColor,
ZoomSettings zoomSettings, int titleId) {
this.format = new DecimalFormat(formatString);
fillPaint = new Paint();
fillPaint.setStyle(Style.FILL);
fillPaint.setColor(context.getResources().getColor(fillColor));
fillPaint.setAntiAlias(true);
if (strokeColor != -1) {
strokePaint = new Paint();
strokePaint.setStyle(Style.STROKE);
strokePaint.setColor(context.getResources().getColor(strokeColor));
strokePaint.setAntiAlias(true);
// Make a copy of the stroke paint with the default thickness.
labelPaint = new Paint(strokePaint);
strokePaint.setStrokeWidth(2f);
} else {
strokePaint = null;
labelPaint = fillPaint;
}
this.zoomSettings = zoomSettings;
this.title = context.getString(titleId);
}
/**
* Draws the path of the chart
*/
public void drawPath(Canvas c) {
c.drawPath(path, fillPaint);
if (strokePaint != null) {
c.drawPath(path, strokePaint);
}
}
/**
* Resets this series
*/
public void reset() {
monitor.reset();
}
/**
* Updates this series with a new value
*/
public void update(double d) {
monitor.update(d);
}
/**
* @return The interval between markers
*/
public int getInterval() {
return interval;
}
/**
* Determines what the min and max of the chart will be.
* This will round down and up the min and max respectively.
*/
public void updateDimension() {
if (monitor.getMax() == Double.NEGATIVE_INFINITY) {
min = 0;
max = 1;
} else {
min = monitor.getMin();
max = monitor.getMax();
}
min = Math.min(min, zoomSettings.getAbsoluteMin());
max = Math.max(max, zoomSettings.getAbsoluteMax());
this.interval = zoomSettings.calculateInterval(min, max);
// Round it up.
effectiveMax = ((int) (max / interval)) * interval + interval;
// Round it down.
effectiveMin = ((int) (min / interval)) * interval;
if (min < 0) {
effectiveMin -= interval;
}
spread = effectiveMax - effectiveMin;
}
/**
* @return The length of the longest string from the series
*/
public int getMaxLabelLength() {
String minS = format.format(effectiveMin);
String maxS = format.format(getMax());
return Math.max(minS.length(), maxS.length());
}
/**
* @return The rounded down minimum value
*/
public int getMin() {
return effectiveMin;
}
/**
* @return The rounded up maximum value
*/
public int getMax() {
return effectiveMax;
}
/**
* @return The difference between the min and max values in the series
*/
public double getSpread() {
return spread;
}
/**
* @return The format for the decimal format for this series
*/
DecimalFormat getFormat() {
return format;
}
/**
* @return The path for this series
*/
Path getPath() {
return path;
}
/**
* @return The paint for this series
*/
Paint getPaint() {
return strokePaint == null ? fillPaint : strokePaint;
}
public Paint getLabelPaint() {
return labelPaint;
}
/**
* @return The title of the series
*/
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
/**
* @return is this series enabled
*/
public boolean isEnabled() {
return enabled;
}
/**
* Sets the series enabled flag.
*/
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public boolean hasData() {
return monitor.hasData();
}
}
@@ -0,0 +1,889 @@
/*
* 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.ChartValueSeries.ZoomSettings;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.stats.ExtremityMonitor;
import com.google.android.apps.mytracks.util.StringUtils;
import com.google.android.apps.mytracks.util.UnitConversions;
import com.google.android.maps.mytracks.R;
import android.content.Context;
import android.content.Intent;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.DashPathEffect;
import android.graphics.Paint;
import android.graphics.Paint.Align;
import android.graphics.Paint.Style;
import android.graphics.Path;
import android.graphics.drawable.Drawable;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.widget.Scroller;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.ArrayList;
/**
* Visualization of the chart.
*
* @author Sandor Dornbush
* @author Leif Hendrik Wilden
*/
public class ChartView extends View {
private static final int MIN_ZOOM_LEVEL = 1;
/*
* Scrolling logic:
*/
private final Scroller scroller;
private VelocityTracker velocityTracker = null;
/** Position of the last motion event */
private float lastMotionX;
/*
* Zoom logic:
*/
private int zoomLevel = 1;
private int maxZoomLevel = 10;
private static final int MAX_INTERVALS = 5;
/*
* Borders, margins, dimensions (in pixels):
*/
private int leftBorder = -1;
/**
* Unscaled top border of the chart.
*/
private static final int TOP_BORDER = 15;
/**
* Device scaled top border of the chart.
*/
private int topBorder;
/**
* Unscaled bottom border of the chart.
*/
private static final float BOTTOM_BORDER = 40;
/**
* Device scaled bottom border of the chart.
*/
private int bottomBorder;
private static final int RIGHT_BORDER = 17;
/** Space to leave for drawing the unit labels */
private static final int UNIT_BORDER = 15;
private static final int FONT_HEIGHT = 10;
private int w = 0;
private int h = 0;
private int effectiveWidth = 0;
private int effectiveHeight = 0;
/*
* Ranges (in data units):
*/
private double maxX = 1;
/**
* The various series.
*/
public static final int ELEVATION_SERIES = 0;
public static final int SPEED_SERIES = 1;
public static final int POWER_SERIES = 2;
public static final int CADENCE_SERIES = 3;
public static final int HEART_RATE_SERIES = 4;
public static final int NUM_SERIES = 5;
private ChartValueSeries[] series;
private final ExtremityMonitor xMonitor = new ExtremityMonitor();
private static final NumberFormat X_FORMAT = new DecimalFormat("###,###");
private static final NumberFormat X_SHORT_FORMAT = new DecimalFormat("#.0");
/*
* Paints etc. used when drawing the chart:
*/
private final Paint borderPaint = new Paint();
private final Paint labelPaint = new Paint();
private final Paint gridPaint = new Paint();
private final Paint gridBarPaint = new Paint();
private final Paint clearPaint = new Paint();
private final Drawable pointer;
private final Drawable statsMarker;
private final Drawable waypointMarker;
private final int markerWidth, markerHeight;
/**
* The chart data stored as an array of double arrays. Each one dimensional
* array is composed of [x, y].
*/
private final ArrayList<double[]> data = new ArrayList<double[]>();
/**
* List of way points to be displayed.
*/
private final ArrayList<Waypoint> waypoints = new ArrayList<Waypoint>();
private boolean metricUnits = true;
private boolean showPointer = false;
/** Display chart versus distance or time */
public enum Mode {
BY_DISTANCE, BY_TIME
}
private Mode mode = Mode.BY_DISTANCE;
public ChartView(Context context) {
super(context);
setUpChartValueSeries(context);
labelPaint.setStyle(Style.STROKE);
labelPaint.setColor(context.getResources().getColor(R.color.black));
labelPaint.setAntiAlias(true);
borderPaint.setStyle(Style.STROKE);
borderPaint.setColor(context.getResources().getColor(R.color.black));
borderPaint.setAntiAlias(true);
gridPaint.setStyle(Style.STROKE);
gridPaint.setColor(context.getResources().getColor(R.color.gray));
gridPaint.setAntiAlias(false);
gridBarPaint.set(gridPaint);
gridBarPaint.setPathEffect(new DashPathEffect(new float[] {3, 2}, 0));
clearPaint.setStyle(Style.FILL);
clearPaint.setColor(context.getResources().getColor(R.color.white));
clearPaint.setAntiAlias(false);
pointer = context.getResources().getDrawable(R.drawable.arrow_180);
pointer.setBounds(0, 0,
pointer.getIntrinsicWidth(), pointer.getIntrinsicHeight());
statsMarker = getResources().getDrawable(R.drawable.ylw_pushpin);
markerWidth = statsMarker.getIntrinsicWidth();
markerHeight = statsMarker.getIntrinsicHeight();
statsMarker.setBounds(0, 0, markerWidth, markerHeight);
waypointMarker = getResources().getDrawable(R.drawable.blue_pushpin);
waypointMarker.setBounds(0, 0, markerWidth, markerHeight);
scroller = new Scroller(context);
setFocusable(true);
setClickable(true);
updateDimensions();
}
private void setUpChartValueSeries(Context context) {
series = new ChartValueSeries[NUM_SERIES];
// Create the value series.
series[ELEVATION_SERIES] =
new ChartValueSeries(context,
"###,###",
R.color.elevation_fill,
R.color.elevation_border,
new ZoomSettings(MAX_INTERVALS,
new int[] {5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000}),
R.string.elevation);
series[SPEED_SERIES] =
new ChartValueSeries(context,
"###,###",
R.color.speed_fill,
R.color.speed_border,
new ZoomSettings(MAX_INTERVALS, 0, Integer.MIN_VALUE,
new int[] {1, 5, 10, 20, 50}),
R.string.speed);
series[POWER_SERIES] =
new ChartValueSeries(context,
"###,###",
R.color.power_fill,
R.color.power_border,
new ZoomSettings(MAX_INTERVALS, 0, 1000, new int[] {5, 50, 100, 200}),
R.string.power);
series[CADENCE_SERIES] =
new ChartValueSeries(context,
"###,###",
R.color.cadence_fill,
R.color.cadence_border,
new ZoomSettings(MAX_INTERVALS, 0, Integer.MIN_VALUE,
new int[] {5, 10, 25, 50}),
R.string.cadence);
series[HEART_RATE_SERIES] =
new ChartValueSeries(context,
"###,###",
R.color.heartrate_fill,
R.color.heartrate_border,
new ZoomSettings(MAX_INTERVALS, 0, Integer.MIN_VALUE,
new int[] {25, 50}),
R.string.heart_rate);
}
public void clearWaypoints() {
waypoints.clear();
}
public void addWaypoint(Waypoint waypoint) {
waypoints.add(waypoint);
}
/**
* Determines whether the pointer icon is shown on the last data point.
*/
public void setShowPointer(boolean showPointer) {
this.showPointer = showPointer;
}
/**
* Sets whether metric units are used or not.
*/
public void setMetricUnits(boolean metricUnits) {
this.metricUnits = metricUnits;
}
public void setReportSpeed(boolean reportSpeed, Context c) {
series[SPEED_SERIES].setTitle(c.getString(reportSpeed
? R.string.speed
: R.string.pace_label));
}
private void addDataPointInternal(double[] theData) {
xMonitor.update(theData[0]);
int min = Math.min(series.length, theData.length - 1);
for (int i = 1; i <= min; i++) {
if (!Double.isNaN(theData[i])) {
series[i - 1].update(theData[i]);
}
}
// Fill in the extra's if needed.
for (int i = theData.length; i < series.length; i++) {
if (series[i].hasData()) {
series[i].update(0);
}
}
}
/**
* Adds multiple data points to the chart.
*
* @param theData an array list of data points to be added
*/
public void addDataPoints(ArrayList<double[]> theData) {
synchronized (data) {
data.addAll(theData);
for (int i = 0; i < theData.size(); i++) {
double d[] = theData.get(i);
addDataPointInternal(d);
}
updateDimensions();
setUpPath();
}
}
/**
* Clears all data.
*/
public void reset() {
synchronized (data) {
data.clear();
xMonitor.reset();
zoomLevel = 1;
updateDimensions();
}
}
public void resetScroll() {
scrollTo(0, 0);
}
/**
* @return true if the chart can be zoomed into.
*/
public boolean canZoomIn() {
return zoomLevel < maxZoomLevel;
}
/**
* @return true if the chart can be zoomed out
*/
public boolean canZoomOut() {
return zoomLevel > MIN_ZOOM_LEVEL;
}
/**
* Zooms in one level (factor 2).
*/
public void zoomIn() {
if (canZoomIn()) {
zoomLevel++;
setUpPath();
invalidate();
}
}
/**
* Zooms out one level (factor 2).
*/
public void zoomOut() {
if (canZoomOut()) {
zoomLevel--;
scroller.abortAnimation();
int scrollX = getScrollX();
if (scrollX > effectiveWidth * (zoomLevel - 1)) {
scrollX = effectiveWidth * (zoomLevel - 1);
scrollTo(scrollX, 0);
}
setUpPath();
invalidate();
}
}
/**
* Initiates flinging.
*
* @param velocityX start velocity (pixels per second)
*/
public void fling(int velocityX) {
scroller.fling(getScrollX(), 0, velocityX, 0, 0,
effectiveWidth * (zoomLevel - 1), 0, 0);
invalidate();
}
/**
* Scrolls the view horizontally by the given amount.
*
* @param deltaX number of pixels to scroll
*/
public void scrollBy(int deltaX) {
int scrollX = getScrollX() + deltaX;
if (scrollX < 0) {
scrollX = 0;
}
int available = effectiveWidth * (zoomLevel - 1);
if (scrollX > available) {
scrollX = available;
}
scrollTo(scrollX, 0);
}
/**
* @return the current display mode (by distance, by time)
*/
public Mode getMode() {
return mode;
}
/**
* Sets the display mode (by distance, by time).
* It is expected that after the mode change, data will be reloaded.
*/
public void setMode(Mode mode) {
this.mode = mode;
}
private int getWaypointX(Waypoint waypoint) {
return (mode == Mode.BY_DISTANCE)
? getX(metricUnits
? waypoint.getLength() / 1000.0
: waypoint.getLength() * UnitConversions.KM_TO_MI / 1000.0)
: getX(waypoint.getDuration());
}
/**
* Called by the parent to indicate that the mScrollX/Y values need to be
* updated. Triggers a redraw during flinging.
*/
@Override
public void computeScroll() {
if (scroller.computeScrollOffset()) {
int oldX = getScrollX();
int x = scroller.getCurrX();
scrollTo(x, 0);
if (oldX != x) {
onScrollChanged(x, 0, oldX, 0);
postInvalidate();
}
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (velocityTracker == null) {
velocityTracker = VelocityTracker.obtain();
}
velocityTracker.addMovement(event);
final int action = event.getAction();
final float x = event.getX();
switch (action) {
case MotionEvent.ACTION_DOWN:
/*
* If being flinged and user touches, stop the fling. isFinished will be
* false if being flinged.
*/
if (!scroller.isFinished()) {
scroller.abortAnimation();
}
// Remember where the motion event started
lastMotionX = x;
break;
case MotionEvent.ACTION_MOVE:
// Scroll to follow the motion event
final int deltaX = (int) (lastMotionX - x);
lastMotionX = x;
if (deltaX < 0) {
if (getScrollX() > 0) {
scrollBy(deltaX);
}
} else if (deltaX > 0) {
final int availableToScroll =
effectiveWidth * (zoomLevel - 1) - getScrollX();
if (availableToScroll > 0) {
scrollBy(Math.min(availableToScroll, deltaX));
}
}
break;
case MotionEvent.ACTION_UP:
// Check if top area with waypoint markers was touched and find the
// touched marker if any:
if (event.getY() < 100) {
int dmin = Integer.MAX_VALUE;
Waypoint nearestWaypoint = null;
for (int i = 0; i < waypoints.size(); i++) {
final Waypoint waypoint = waypoints.get(i);
final int d = Math.abs(getWaypointX(waypoint) - (int) event.getX()
- getScrollX());
if (d < dmin) {
dmin = d;
nearestWaypoint = waypoint;
}
}
if (nearestWaypoint != null && dmin < 100) {
Intent intent =
new Intent(getContext(), WaypointDetails.class);
intent.putExtra(WaypointDetails.WAYPOINT_ID_EXTRA, nearestWaypoint.getId());
getContext().startActivity(intent);
return true;
}
}
VelocityTracker myVelocityTracker = velocityTracker;
myVelocityTracker.computeCurrentVelocity(1000);
int initialVelocity = (int) myVelocityTracker.getXVelocity();
if (Math.abs(initialVelocity) >
ViewConfiguration.getMinimumFlingVelocity()) {
fling(-initialVelocity);
}
if (velocityTracker != null) {
velocityTracker.recycle();
velocityTracker = null;
}
break;
}
return true;
}
@Override
protected void onDraw(Canvas c) {
synchronized (data) {
updateEffectiveDimensionsIfChanged(c);
// Keep original state.
c.save();
c.drawColor(Color.WHITE);
if (data.isEmpty()) {
// No data, draw only axes
drawXAxis(c);
drawYAxis(c);
c.restore();
return;
}
// Clip to graph drawing space
c.save();
clipToGraphSpace(c);
// Draw the grid and the data on it.
drawGrid(c);
drawDataSeries(c);
drawWaypoints(c);
// Go back to full canvas drawing.
c.restore();
// Draw the axes and their labels.
drawAxesAndLabels(c);
// Go back to original state.
c.restore();
// Draw the pointer
if (showPointer) {
drawPointer(c);
}
}
}
/** Clips the given canvas to the area where the graph lines should be drawn. */
private void clipToGraphSpace(Canvas c) {
c.clipRect(leftBorder + 1 + getScrollX(), topBorder + 1,
w - RIGHT_BORDER + getScrollX() - 1, h - bottomBorder - 1);
}
/** Draws the axes and their labels into th e given canvas. */
private void drawAxesAndLabels(Canvas c) {
drawXLabels(c);
drawXAxis(c);
drawSeriesTitles(c);
c.translate(getScrollX(), 0);
drawYAxis(c);
float density = getContext().getResources().getDisplayMetrics().density;
final int spacer = (int) (5 * density);
int x = leftBorder - spacer;
for (ChartValueSeries cvs : series) {
if (cvs.isEnabled() && cvs.hasData()) {
x -= drawYLabels(cvs, c, x) + spacer;
}
}
}
/** Draws the current pointer into the given canvas. */
private void drawPointer(Canvas c) {
c.translate(getX(maxX) - pointer.getIntrinsicWidth() / 2,
getY(series[0], data.get(data.size() - 1)[1])
- pointer.getIntrinsicHeight() / 2 - 12);
pointer.draw(c);
}
/** Draws the waypoints into the given canvas. */
private void drawWaypoints(Canvas c) {
for (int i = 1; i < waypoints.size(); i++) {
final Waypoint waypoint = waypoints.get(i);
if (waypoint.getLocation() == null) {
continue;
}
c.save();
final float x = getWaypointX(waypoint);
c.drawLine(x, h - bottomBorder, x, topBorder, gridPaint);
c.translate(x - (float) markerWidth / 2.0f, (float) markerHeight);
if (waypoints.get(i).getType() == Waypoint.TYPE_STATISTICS) {
statsMarker.draw(c);
} else {
waypointMarker.draw(c);
}
c.restore();
}
}
/** Draws the data series into the given canvas. */
private void drawDataSeries(Canvas c) {
for (ChartValueSeries cvs : series) {
if (cvs.isEnabled() && cvs.hasData()) {
cvs.drawPath(c);
}
}
}
/** Draws the colored titles for the data series. */
private void drawSeriesTitles(Canvas c) {
int sections = 1;
for (ChartValueSeries cvs : series) {
if (cvs.isEnabled() && cvs.hasData()) {
sections++;
}
}
int j = 0;
for (ChartValueSeries cvs : series) {
if (cvs.isEnabled() && cvs.hasData()) {
int x = (int) (w * (double) ++j / sections) + getScrollX();
c.drawText(cvs.getTitle(), x, topBorder, cvs.getLabelPaint());
}
}
}
/**
* Sets up the path that is used to draw the chart in onDraw(). The path
* needs to be updated any time after the data or histogram dimensions change.
*/
private void setUpPath() {
synchronized (data) {
for (ChartValueSeries cvs : series) {
cvs.getPath().reset();
}
if (!data.isEmpty()) {
drawPaths();
closePaths();
}
}
}
/** Actually draws the data points as a path. */
private void drawPaths() {
// All of the data points to the respective series.
// TODO: Come up with a better sampling than Math.max(1, (maxZoomLevel - zoomLevel + 1) / 2);
int sampling = 1;
for (int i = 0; i < data.size(); i += sampling) {
double[] d = data.get(i);
int min = Math.min(series.length, d.length - 1);
for (int j = 0; j < min; ++j) {
ChartValueSeries cvs = series[j];
Path path = cvs.getPath();
int x = getX(d[0]);
int y = getY(cvs, d[j + 1]);
if (i == 0) {
path.moveTo(x, y);
} else {
path.lineTo(x, y);
}
}
}
}
/** Closes the drawn path so it looks like a solid graph. */
private void closePaths() {
// Close the path.
int yCorner = topBorder + effectiveHeight;
int xCorner = getX(data.get(0)[0]);
int min = series.length;
for (int j = 0; j < min; j++) {
ChartValueSeries cvs = series[j];
Path path = cvs.getPath();
int first = getFirstPointPopulatedIndex(j + 1);
if (first != -1) {
// Bottom right corner
path.lineTo(getX(data.get(data.size() - 1)[0]), yCorner);
// Bottom left corner
path.lineTo(xCorner, yCorner);
// Top right corner
path.lineTo(xCorner, getY(cvs, data.get(first)[j + 1]));
}
}
}
/**
* Finds the index of the first point which has a series populated.
*
* @param seriesIndex The index of the value series to search for
* @return The index in the first data for the point in the series that has series
* index value populated or -1 if none is found
*/
private int getFirstPointPopulatedIndex(int seriesIndex) {
for (int i = 0; i < data.size(); i++) {
if (data.get(i).length > seriesIndex) {
return i;
}
}
return -1;
}
/**
* Updates the chart dimensions.
*/
private void updateDimensions() {
maxX = xMonitor.getMax();
if (data.size() <= 1) {
maxX = 1;
}
for (ChartValueSeries cvs : series) {
cvs.updateDimension();
}
// TODO: This is totally broken. Make sure that we calculate based on measureText for each
// grid line, as the labels may vary across intervals.
int maxLength = 0;
for (ChartValueSeries cvs : series) {
if (cvs.isEnabled() && cvs.hasData()) {
maxLength += cvs.getMaxLabelLength();
}
}
float density = getContext().getResources().getDisplayMetrics().density;
maxLength = Math.max(maxLength, 1);
leftBorder = (int) (density * (4 + 8 * maxLength));
bottomBorder = (int) (density * BOTTOM_BORDER);
topBorder = (int) (density * TOP_BORDER);
updateEffectiveDimensions();
}
/** Updates the effective dimensions where the graph will be drawn. */
private void updateEffectiveDimensions() {
effectiveWidth = Math.max(0, w - leftBorder - RIGHT_BORDER);
effectiveHeight = Math.max(0, h - topBorder - bottomBorder);
}
/**
* Updates the effective dimensions where the graph will be drawn, only if the
* dimensions of the given canvas have changed since the last call.
*/
private void updateEffectiveDimensionsIfChanged(Canvas c) {
if (w != c.getWidth() || h != c.getHeight()) {
// Dimensions have changed (for example due to orientation change).
w = c.getWidth();
h = c.getHeight();
updateEffectiveDimensions();
setUpPath();
}
}
private int getX(double distance) {
return leftBorder + (int) ((distance * effectiveWidth / maxX) * zoomLevel);
}
private int getY(ChartValueSeries cvs, double y) {
int effectiveSpread = cvs.getInterval() * MAX_INTERVALS;
return topBorder + effectiveHeight
- (int) ((y - cvs.getMin()) * effectiveHeight / effectiveSpread);
}
/** Draws the labels on the X axis into the given canvas. */
private void drawXLabels(Canvas c) {
double interval = (int) (maxX / zoomLevel / 4);
boolean shortFormat = false;
if (interval < 1) {
interval = .5;
shortFormat = true;
} else if (interval < 5) {
interval = 2;
} else if (interval < 10) {
interval = 5;
} else {
interval = (interval / 10) * 10;
}
drawXLabel(c, 0, shortFormat);
int numLabels = 1;
for (int i = 1; i * interval < maxX; i++) {
drawXLabel(c, i * interval, shortFormat);
numLabels++;
}
if (numLabels < 2) {
drawXLabel(c, (int) maxX, shortFormat);
}
}
/** Draws the labels on the Y axis into the given canvas. */
private float drawYLabels(ChartValueSeries cvs, Canvas c, int x) {
int interval = cvs.getInterval();
float maxTextWidth = 0;
for (int i = 0; i < MAX_INTERVALS; ++i) {
maxTextWidth = Math.max(maxTextWidth, drawYLabel(cvs, c, x, i * interval + cvs.getMin()));
}
return maxTextWidth;
}
/** Draws a single label on the X axis. */
private void drawXLabel(Canvas c, double x, boolean shortFormat) {
if (x < 0) {
return;
}
String s =
(mode == Mode.BY_DISTANCE)
? (shortFormat ? X_SHORT_FORMAT.format(x) : X_FORMAT.format(x))
: StringUtils.formatTime((long) x);
c.drawText(s,
getX(x),
effectiveHeight + UNIT_BORDER + topBorder,
labelPaint);
}
/** Draws a single label on the Y axis. */
private float drawYLabel(ChartValueSeries cvs, Canvas c, int x, int y) {
int desiredY = (int) ((y - cvs.getMin()) * effectiveHeight /
(cvs.getInterval() * MAX_INTERVALS));
desiredY = topBorder + effectiveHeight + FONT_HEIGHT / 2 - desiredY - 1;
Paint p = new Paint(cvs.getLabelPaint());
p.setTextAlign(Align.RIGHT);
String text = cvs.getFormat().format(y);
c.drawText(text, x, desiredY, p);
return p.measureText(text);
}
/** Draws the actual X axis line and its label. */
private void drawXAxis(Canvas canvas) {
float rightEdge = getX(maxX);
final int y = effectiveHeight + topBorder;
canvas.drawLine(leftBorder, y, rightEdge, y, borderPaint);
Context c = getContext();
String s = mode == Mode.BY_DISTANCE
? (metricUnits ? c.getString(R.string.kilometer) : c.getString(R.string.mile))
: c.getString(R.string.min);
canvas.drawText(s, rightEdge, effectiveHeight + .2f * UNIT_BORDER + topBorder, labelPaint);
}
/** Draws the actual Y axis line and its label. */
private void drawYAxis(Canvas canvas) {
canvas.drawRect(0, 0,
leftBorder - 1, effectiveHeight + topBorder + UNIT_BORDER + 1,
clearPaint);
canvas.drawLine(leftBorder, UNIT_BORDER + topBorder,
leftBorder, effectiveHeight + topBorder,
borderPaint);
for (int i = 1; i < MAX_INTERVALS; ++i) {
int y = i * effectiveHeight / MAX_INTERVALS + topBorder;
canvas.drawLine(leftBorder - 5, y, leftBorder, y, gridPaint);
}
Context c = getContext();
// TODO: This should really show units for all series.
String s = metricUnits ? c.getString(R.string.meter) : c.getString(R.string.feet);
canvas.drawText(s, leftBorder - UNIT_BORDER * .2f, UNIT_BORDER * .8f + topBorder, labelPaint);
}
/** Draws the grid for the graph. */
private void drawGrid(Canvas c) {
float rightEdge = getX(maxX);
for (int i = 1; i < MAX_INTERVALS; ++i) {
int y = i * effectiveHeight / MAX_INTERVALS + topBorder;
c.drawLine(leftBorder, y, rightEdge, y, gridBarPaint);
}
}
/**
* Returns whether a given time series is enabled for drawing.
*
* @param index the time series, one of {@link #ELEVATION_SERIES},
* {@link #SPEED_SERIES}, {@link #POWER_SERIES}, etc.
* @return true if drawn, false otherwise
*/
public boolean isChartValueSeriesEnabled(int index) {
return series[index].isEnabled();
}
/**
* Sets whether a given time series will be enabled for drawing.
*
* @param index the time series, one of {@link #ELEVATION_SERIES},
* {@link #SPEED_SERIES}, {@link #POWER_SERIES}, etc.
* @return true to be drawn, false otherwise
*/
public void setChartValueSeriesEnabled(int index, boolean enabled) {
series[index].setEnabled(enabled);
}
}
@@ -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 com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.maps.mytracks.R;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import java.util.Locale;
/**
* Check to see if the units are the default for the locale.
* This comes down to warning Americans that the default is metric.
*
* @author Sandor Dornbush
*/
class CheckUnits {
private static final String PREFERENCE_UNITS_CHECKED = "checkunits.checked";
private static final String PREFERENCES_CHECK_UNITS = "checkunits";
static void check(final Context context) {
final SharedPreferences preferences =
context.getSharedPreferences(PREFERENCES_CHECK_UNITS, Activity.MODE_PRIVATE);
// Has the user already warned about the default units?
if (preferences.getBoolean(PREFERENCE_UNITS_CHECKED, false)) {
return;
}
// Is the user in the US?
Locale current = Locale.getDefault();
Locale enUs = new Locale(Locale.ENGLISH.getLanguage(),
Locale.US.getCountry());
if (!current.equals(enUs)) {
return;
}
final AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setTitle(R.string.check_units_title);
builder.setCancelable(true);
builder.setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
recordCheckPerformed(preferences);
}
});
builder.setNegativeButton(R.string.no, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
changeDefaultUnits(context, preferences);
}
});
builder.setOnCancelListener(new DialogInterface.OnCancelListener() {
public void onCancel(DialogInterface dialog) {
recordCheckPerformed(preferences);
}
});
builder.setMessage(R.string.check_units_message);
builder.show();
}
private static void changeDefaultUnits(Context context, SharedPreferences preferences) {
recordCheckPerformed(preferences);
Intent startIntent = new Intent(context, SettingsActivity.class);
startIntent.putExtra(context.getString(R.string.open_settings_screen),
context.getString(R.string.display_settings_screen_key));
context.startActivity(startIntent);
}
private static void recordCheckPerformed(SharedPreferences preferences) {
ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges(
preferences.edit().putBoolean(PREFERENCE_UNITS_CHECKED, true));
}
private CheckUnits() {
}
}
@@ -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,222 @@
/*
* 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;
/**
* Constants used by the MyTracks application.
*
* @author Leif Hendrik Wilden
*/
public abstract class Constants {
/**
* Should be used by all log statements
*/
public static final String TAG = "MyTracks";
/**
* Name of the top-level directory inside the SD card where our files will
* be read from/written to.
*/
public static final String SDCARD_TOP_DIR = "MyTracks";
/*
* onActivityResult request codes:
*/
public static final int GET_LOGIN = 0;
public static final int GET_MAP = 1;
public static final int SHOW_TRACK = 2;
public static final int AUTHENTICATE_TO_MY_MAPS = 3;
public static final int AUTHENTICATE_TO_FUSION_TABLES = 4;
public static final int AUTHENTICATE_TO_DOCLIST = 5;
public static final int AUTHENTICATE_TO_TRIX = 6;
public static final int SHARE_GPX_FILE = 7;
public static final int SHARE_KML_FILE = 8;
public static final int SHARE_CSV_FILE = 9;
public static final int SHARE_TCX_FILE = 10;
public static final int SAVE_GPX_FILE = 11;
public static final int SAVE_KML_FILE = 12;
public static final int SAVE_CSV_FILE = 13;
public static final int SAVE_TCX_FILE = 14;
public static final int SHOW_WAYPOINT = 15;
public static final int WELCOME = 16;
/*
* Menu ids:
*/
public static final int MENU_MY_LOCATION = 1;
public static final int MENU_TOGGLE_LAYERS = 2;
public static final int MENU_CHART_SETTINGS = 3;
/*
* Context menu ids:
*/
public static final int MENU_EDIT = 100;
public static final int MENU_DELETE = 101;
public static final int MENU_SEND_TO_GOOGLE = 102;
public static final int MENU_SHARE = 103;
public static final int MENU_SHOW = 104;
public static final int MENU_SHARE_LINK = 200;
public static final int MENU_SHARE_GPX_FILE = 201;
public static final int MENU_SHARE_KML_FILE = 202;
public static final int MENU_SHARE_CSV_FILE = 203;
public static final int MENU_SHARE_TCX_FILE = 204;
public static final int MENU_WRITE_TO_SD_CARD = 205;
public static final int MENU_SAVE_GPX_FILE = 206;
public static final int MENU_SAVE_KML_FILE = 207;
public static final int MENU_SAVE_CSV_FILE = 208;
public static final int MENU_SAVE_TCX_FILE = 209;
public static final int MENU_CLEAR_MAP = 210;
/**
* The number of distance readings to smooth to get a stable signal.
*/
public static final int DISTANCE_SMOOTHING_FACTOR = 25;
/**
* The number of elevation readings to smooth to get a somewhat accurate
* signal.
*/
public static final int ELEVATION_SMOOTHING_FACTOR = 25;
/**
* The number of grade readings to smooth to get a somewhat accurate signal.
*/
public static final int GRADE_SMOOTHING_FACTOR = 5;
/**
* The number of speed reading to smooth to get a somewhat accurate signal.
*/
public static final int SPEED_SMOOTHING_FACTOR = 25;
/**
* Maximum number of track points displayed by the map overlay.
*/
public static final int MAX_DISPLAYED_TRACK_POINTS = 10000;
/**
* Target number of track points displayed by the map overlay.
* We may display more than this number of points.
*/
public static final int TARGET_DISPLAYED_TRACK_POINTS = 5000;
/**
* Maximum number of track points ever loaded at once from the provider into
* memory.
* With a recording frequency of 2 seconds, 15000 corresponds to 8.3 hours.
*/
public static final int MAX_LOADED_TRACK_POINTS = 20000;
/**
* Maximum number of track points ever loaded at once from the provider into
* memory in a single call to read points.
*/
public static final int MAX_LOADED_TRACK_POINTS_PER_BATCH = 1000;
/**
* Maximum number of way points displayed by the map overlay.
*/
public static final int MAX_DISPLAYED_WAYPOINTS_POINTS = 128;
/**
* Maximum number of way points that will be loaded at one time.
*/
public static final int MAX_LOADED_WAYPOINTS_POINTS = 10000;
/**
* Any time segment where the distance traveled is less than this value will
* not be considered moving.
*/
public static final double MAX_NO_MOVEMENT_DISTANCE = 2;
/**
* Anything faster than that (in meters per second) will be considered moving.
*/
public static final double MAX_NO_MOVEMENT_SPEED = 0.224;
/**
* Ignore any acceleration faster than this.
* Will ignore any speeds that imply accelaration greater than 2g's
* 2g = 19.6 m/s^2 = 0.0002 m/ms^2 = 0.02 m/(m*ms)
*/
public static final double MAX_ACCELERATION = 0.02;
/** Maximum age of a GPS location to be considered current. */
public static final long MAX_LOCATION_AGE_MS = 60 * 1000; // 1 minute
/** Maximum age of a network location to be considered current. */
public static final long MAX_NETWORK_AGE_MS = 1000 * 60 * 10; // 10 minutes
/**
* The type of account that we can use for gdata uploads.
*/
public static final String ACCOUNT_TYPE = "com.google";
/**
* The name of extra intent property to indicate whether we want to resume
* a previously recorded track.
*/
public static final String RESUME_TRACK_EXTRA_NAME =
"com.google.android.apps.mytracks.RESUME_TRACK";
public static int getActionFromMenuId(int menuId) {
switch (menuId) {
case Constants.MENU_SHARE_KML_FILE:
return Constants.SHARE_KML_FILE;
case Constants.MENU_SHARE_GPX_FILE:
return Constants.SHARE_GPX_FILE;
case Constants.MENU_SHARE_CSV_FILE:
return Constants.SHARE_CSV_FILE;
case Constants.MENU_SHARE_TCX_FILE:
return Constants.SHARE_TCX_FILE;
case Constants.MENU_SAVE_GPX_FILE:
return Constants.SAVE_GPX_FILE;
case Constants.MENU_SAVE_KML_FILE:
return Constants.SAVE_KML_FILE;
case Constants.MENU_SAVE_CSV_FILE:
return Constants.SAVE_CSV_FILE;
case Constants.MENU_SAVE_TCX_FILE:
return Constants.SAVE_TCX_FILE;
default:
return -1;
}
}
public static final String MAPSHOP_BASE_URL =
"https://maps.google.com/maps/ms";
/*
* Default values - keep in sync with those in preferences.xml.
*/
public static final int DEFAULT_ANNOUNCEMENT_FREQUENCY = -1;
public static final int DEFAULT_AUTO_RESUME_TRACK_TIMEOUT = 10; // In min.
public static final int DEFAULT_MAX_RECORDING_DISTANCE = 200;
public static final int DEFAULT_MIN_RECORDING_DISTANCE = 5;
public static final int DEFAULT_MIN_RECORDING_INTERVAL = 0;
public static final int DEFAULT_MIN_REQUIRED_ACCURACY = 200;
public static final int DEFAULT_SPLIT_FREQUENCY = 0;
public static final String SETTINGS_NAME = "SettingsActivity";
/**
* This is an abstract utility class.
*/
protected Constants() { }
}
@@ -0,0 +1,84 @@
/*
* 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.util.ApiFeatures;
import com.google.android.maps.mytracks.R;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.SharedPreferences;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
/**
* A utility class that can be used to delete all tracks and track points
* from the provider, including asking for confirmation from the user via
* a dialog.
*
* @author Leif Hendrik Wilden
*/
public class DeleteAllTracks extends Handler {
private final Context context;
private final Runnable done;
public DeleteAllTracks(Context context, Runnable done) {
this.context = context;
this.done = done;
}
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
AlertDialog dialog = null;
AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setMessage(
context.getString(R.string.all_data_will_be_permanently_deleted));
builder.setTitle(context.getString(R.string.confirmation_title_are_you_sure));
builder.setIcon(android.R.drawable.ic_dialog_alert);
builder.setPositiveButton(context.getString(R.string.yes),
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialogInterface, int i) {
dialogInterface.dismiss();
Log.w(Constants.TAG, "deleting all!");
MyTracksProviderUtils.Factory.get(context).deleteAllTracks();
SharedPreferences prefs =
context.getSharedPreferences(Constants.SETTINGS_NAME, 0);
SharedPreferences.Editor editor = prefs.edit();
// TODO: Go through data manager
editor.putLong(context.getString(R.string.selected_track_key), -1);
ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges(editor);
if (done != null) {
Handler h = new Handler();
h.post(done);
}
}
});
builder.setNegativeButton(context.getString(R.string.no),
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialogInterface, int i) {
dialogInterface.dismiss();
}
});
dialog = builder.create();
dialog.show();
}
}
@@ -0,0 +1,114 @@
/*
* 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 static com.google.android.apps.mytracks.Constants.TAG;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.TracksColumns;
import com.google.android.apps.mytracks.util.ApiFeatures;
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.content.ContentUris;
import android.content.DialogInterface;
import android.content.DialogInterface.OnCancelListener;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
/**
* Activity used to delete a track.
*
* @author Rodrigo Damazio
*/
public class DeleteTrack extends Activity
implements DialogInterface.OnClickListener, OnCancelListener {
private static final int CONFIRM_DIALOG = 1;
private MyTracksProviderUtils providerUtils;
private long deleteTrackId;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
providerUtils = MyTracksProviderUtils.Factory.get(this);
Intent intent = getIntent();
String action = intent.getAction();
Uri data = intent.getData();
if (!Intent.ACTION_DELETE.equals(action) ||
!UriUtils.matchesContentUri(data, TracksColumns.CONTENT_URI)) {
Log.e(TAG, "Got bad delete intent: " + intent);
finish();
}
deleteTrackId = ContentUris.parseId(data);
showDialog(CONFIRM_DIALOG);
}
@Override
protected Dialog onCreateDialog(int id) {
if (id != CONFIRM_DIALOG) {
Log.e(TAG, "Unknown dialog " + id);
return null;
}
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setMessage(getString(R.string.track_will_be_permanently_deleted));
builder.setTitle(getString(R.string.confirmation_title_are_you_sure));
builder.setIcon(android.R.drawable.ic_dialog_alert);
builder.setPositiveButton(getString(R.string.yes), this);
builder.setNegativeButton(getString(R.string.no), this);
builder.setOnCancelListener(this);
return builder.create();
}
@Override
public void onClick(DialogInterface dialogInterface, int which) {
dialogInterface.dismiss();
if (which == DialogInterface.BUTTON_POSITIVE) {
deleteTrack();
}
finish();
}
@Override
public void onCancel(DialogInterface dialog) {
onClick(dialog, DialogInterface.BUTTON_NEGATIVE);
}
private void deleteTrack() {
providerUtils.deleteTrack(deleteTrackId);
// If the track we just deleted was selected, unselect it.
String selectedKey = getString(R.string.selected_track_key);
SharedPreferences preferences = getSharedPreferences(Constants.SETTINGS_NAME, 0);
if (preferences.getLong(selectedKey, -1) == deleteTrackId) {
Editor editor = preferences.edit().putLong(selectedKey, -1);
ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges(editor);
}
}
}
@@ -0,0 +1,77 @@
/*
* 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 com.google.android.apps.mytracks.Constants.TAG;
import android.app.Activity;
import android.app.Dialog;
import android.util.Log;
import android.view.WindowManager.BadTokenException;
/**
* A class to handle all dialog related events for My Tracks.
*
* @author Sandor Dornbush
*/
public class DialogManager {
/**
* The equivalent of {@link #showDialogSafely(int)}, but for a specific
* dialog instance.
*/
public static void showDialogSafely(Activity activity, final Dialog dialog) {
if (activity.isFinishing()) {
Log.w(TAG, "Activity finishing - not showing dialog");
return;
}
activity.runOnUiThread(new Runnable() {
public void run() {
try {
dialog.show();
} catch (BadTokenException e) {
Log.w(TAG, "Could not display dialog", e);
} catch (IllegalStateException e) {
Log.w(TAG, "Could not display dialog", e);
}
}
});
}
/**
* The equivalent of {@link #dismissDialogSafely(int)}, but for a specific
* dialog instance.
*/
public static void dismissDialogSafely(Activity activity, final Dialog dialog) {
if (activity.isFinishing()) {
Log.w(TAG, "Activity finishing - not dismissing dialog");
return;
}
activity.runOnUiThread(new Runnable() {
public void run() {
try {
dialog.dismiss();
} catch (IllegalArgumentException e) {
// This will be thrown if this dialog was not shown before.
}
}
});
}
private DialogManager() {}
}
@@ -0,0 +1,107 @@
/*
* 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.util.ApiFeatures;
import com.google.android.apps.mytracks.util.ResourceUtils;
import com.google.android.maps.mytracks.R;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
/**
* This class handles display of EULAs ("End User License Agreements") to the
* user.
*/
class Eula {
private static final String PREFERENCE_EULA_ACCEPTED = "eula.accepted";
private static final String PREFERENCES_EULA = "eula";
private Eula() {}
/**
* Displays the EULA if necessary. This method should be called from the
* onCreate() method of your main Activity. If the user accepts, the EULA
* will never be displayed again. If the user refuses, the activity will
* finish (exit).
*
* @param activity The Activity to finish if the user rejects the EULA
*/
static void showEulaRequireAcceptance(final Activity activity) {
final SharedPreferences preferences =
activity.getSharedPreferences(PREFERENCES_EULA, Activity.MODE_PRIVATE);
if (preferences.getBoolean(PREFERENCE_EULA_ACCEPTED, false)) {
return;
}
final AlertDialog.Builder builder = initDialog(activity);
builder.setPositiveButton(R.string.accept,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
accept(activity, preferences);
}
});
builder.setNegativeButton(R.string.decline,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
refuse(activity);
}
});
builder.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
refuse(activity);
}
});
builder.show();
}
/**
* Display the EULA to the user in an informational context. They won't be
* given the choice of accepting or declining the EULA -- we're simply
* displaying it for them to read.
*/
static void showEula(Context context) {
AlertDialog.Builder builder = initDialog(context);
builder.setPositiveButton(R.string.ok, null);
builder.show();
}
private static AlertDialog.Builder initDialog(Context context) {
AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setCancelable(true);
builder.setTitle(R.string.eula_title);
builder.setMessage(ResourceUtils.readFile(context, R.raw.eula));
return builder;
}
private static void accept(Activity activity, SharedPreferences preferences) {
ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges(
preferences.edit().putBoolean(PREFERENCE_EULA_ACCEPTED, true));
Intent startIntent = new Intent(activity, WelcomeActivity.class);
activity.startActivityForResult(startIntent, Constants.WELCOME);
}
private static void refuse(Activity activity) {
activity.finish();
}
}
@@ -0,0 +1,219 @@
/*
* 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.TracksColumns;
import com.google.android.apps.mytracks.io.file.TrackWriter;
import com.google.android.apps.mytracks.io.file.TrackWriterFactory;
import com.google.android.apps.mytracks.io.file.TrackWriterFactory.TrackFileFormat;
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.ProgressDialog;
import android.content.DialogInterface;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.os.PowerManager.WakeLock;
import android.util.Log;
import android.widget.Toast;
/**
* A class that will export all tracks to the sd card.
*
* @author Sandor Dornbush
*/
public class ExportAllTracks {
// These must line up with the index in the array.
public static final int GPX_OPTION_INDEX = 0;
public static final int KML_OPTION_INDEX = 1;
public static final int CSV_OPTION_INDEX = 2;
public static final int TCX_OPTION_INDEX = 3;
private final Activity activity;
private WakeLock wakeLock;
private ProgressDialog progress;
private TrackFileFormat format = TrackFileFormat.GPX;
private final DialogInterface.OnClickListener itemClick =
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
switch (which) {
case GPX_OPTION_INDEX:
format = TrackFileFormat.GPX;
break;
case KML_OPTION_INDEX:
format = TrackFileFormat.KML;
break;
case CSV_OPTION_INDEX:
format = TrackFileFormat.CSV;
break;
case TCX_OPTION_INDEX:
format = TrackFileFormat.TCX;
break;
default:
Log.w(Constants.TAG, "Unknown export format: " + which);
}
}
};
public ExportAllTracks(Activity activity) {
this.activity = activity;
Log.i(Constants.TAG, "ExportAllTracks: Starting");
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
builder.setSingleChoiceItems(R.array.export_formats, 0, itemClick);
builder.setPositiveButton(R.string.ok, positiveClick);
builder.setNegativeButton(R.string.cancel, null);
builder.show();
}
private final DialogInterface.OnClickListener positiveClick =
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
new Thread(runner, "SendToMyMaps").start();
}
};
private final Runnable runner = new Runnable() {
public void run() {
aquireLocksAndExport();
}
};
/**
* Makes sure that we keep the phone from sleeping.
* See if there is a current track. Aquire a wake lock if there is no
* current track.
*/
private void aquireLocksAndExport() {
SharedPreferences prefs =
activity.getSharedPreferences(Constants.SETTINGS_NAME, 0);
long recordingTrackId = -1;
if (prefs != null) {
recordingTrackId =
prefs.getLong(activity.getString(R.string.recording_track_key), -1);
}
if (recordingTrackId != -1) {
wakeLock = SystemUtils.acquireWakeLock(activity, wakeLock);
}
// Now we can safely export everything.
exportAll();
// Release the wake lock if we recorded one.
// TODO check what happens if we started recording after getting this lock.
if (wakeLock != null && wakeLock.isHeld()) {
wakeLock.release();
Log.i(Constants.TAG, "ExportAllTracks: Releasing wake lock.");
}
Log.i(Constants.TAG, "ExportAllTracks: Done");
showToast(R.string.export_done, Toast.LENGTH_SHORT);
}
private void makeProgressDialog(final int trackCount) {
String exportMsg = activity.getString(R.string.tracklist_btn_export_all);
progress = new ProgressDialog(activity);
progress.setIcon(android.R.drawable.ic_dialog_info);
progress.setTitle(exportMsg);
progress.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
progress.setMax(trackCount);
progress.setProgress(0);
progress.show();
}
/**
* Actually export the tracks.
* This should be called after the wake locks have been aquired.
*/
private void exportAll() {
// Get a cursor over all tracks.
Cursor cursor = null;
try {
MyTracksProviderUtils providerUtils =
MyTracksProviderUtils.Factory.get(activity);
cursor = providerUtils.getTracksCursor("");
if (cursor == null) {
return;
}
final int trackCount = cursor.getCount();
Log.i(Constants.TAG,
"ExportAllTracks: Exporting: " + cursor.getCount() + " tracks.");
int idxTrackId = cursor.getColumnIndexOrThrow(TracksColumns._ID);
activity.runOnUiThread(new Runnable() {
public void run() {
makeProgressDialog(trackCount);
}
});
for (int i = 0; cursor.moveToNext(); i++) {
final int status = i;
activity.runOnUiThread(new Runnable() {
public void run() {
synchronized (this) {
if (progress == null) {
return;
}
progress.setProgress(status);
}
}
});
long id = cursor.getLong(idxTrackId);
Log.i(Constants.TAG, "ExportAllTracks: exporting: " + id);
TrackWriter writer =
TrackWriterFactory.newWriter(activity, providerUtils, id, format);
if (writer == null) {
showToast(R.string.error_export_generic, Toast.LENGTH_LONG);
return;
}
writer.writeTrack();
if (!writer.wasSuccess()) {
// Abort the whole export on the first error.
showToast(writer.getErrorMessage(), Toast.LENGTH_LONG);
return;
}
}
} finally {
if (cursor != null) {
cursor.close();
}
if (progress != null) {
synchronized (this) {
progress.dismiss();
progress = null;
}
}
}
}
private void showToast(final int messageId, final int length) {
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(activity, messageId, length).show();
}
});
}
}
@@ -0,0 +1,220 @@
/*
* 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.io.file.GpxImporter;
import com.google.android.apps.mytracks.util.FileUtils;
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.ProgressDialog;
import android.content.SharedPreferences;
import android.os.PowerManager.WakeLock;
import android.util.Log;
import android.widget.Toast;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import javax.xml.parsers.ParserConfigurationException;
import org.xml.sax.SAXException;
/**
* A class that will import all GPX tracks in /sdcard/MyTracks/gpx/
*
* @author David Piggott
*/
public class ImportAllTracks {
private final Activity activity;
private WakeLock wakeLock;
private ProgressDialog progress;
private FileUtils fileUtils;
private String gpxPath;
private int gpxFileCount;
private int importSuccessCount;
public ImportAllTracks(Activity activity) {
this.activity = activity;
Log.i(Constants.TAG, "ImportAllTracks: Starting");
fileUtils = new FileUtils();
gpxPath = fileUtils.buildExternalDirectoryPath("gpx");
new Thread(runner).start();
}
private final Runnable runner = new Runnable() {
public void run() {
aquireLocksAndImport();
}
};
/**
* Makes sure that we keep the phone from sleeping. See if there is a current
* track. Acquire a wake lock if there is no current track.
*/
private void aquireLocksAndImport() {
SharedPreferences prefs = activity.getSharedPreferences(Constants.SETTINGS_NAME, 0);
long recordingTrackId = -1;
if (prefs != null) {
recordingTrackId = prefs.getLong(activity.getString(R.string.recording_track_key), -1);
}
if (recordingTrackId != -1) {
wakeLock = SystemUtils.acquireWakeLock(activity, wakeLock);
}
// Now we can safely import everything.
importAll();
// Release the wake lock if we acquired one.
// TODO check what happens if we started recording after getting this lock.
if (wakeLock != null && wakeLock.isHeld()) {
wakeLock.release();
Log.i(Constants.TAG, "ImportAllTracks: Releasing wake lock.");
}
activity.runOnUiThread(new Thread() {
@Override
public void run() {
showDoneDialog();
}
});
}
private void showDoneDialog() {
Log.i(Constants.TAG, "ImportAllTracks: Done");
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
if (gpxFileCount == 0) {
builder.setMessage(activity.getString(R.string.import_multi_empty, gpxPath + "/"));
} else {
builder.setMessage(activity.getString(R.string.import_multi_done, importSuccessCount, gpxFileCount,
gpxPath + "/"));
}
builder.setPositiveButton(R.string.ok, null);
builder.show();
}
private void makeProgressDialog(final int trackCount) {
String importMsg = activity.getString(R.string.tracklist_btn_import_all);
progress = new ProgressDialog(activity);
progress.setIcon(android.R.drawable.ic_dialog_info);
progress.setTitle(importMsg);
progress.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
progress.setMax(trackCount);
progress.setProgress(0);
progress.show();
}
/**
* Actually import the tracks. This should be called after the wake locks have
* been acquired.
*/
private void importAll() {
MyTracksProviderUtils providerUtils = MyTracksProviderUtils.Factory.get(activity);
if (!fileUtils.isSdCardAvailable()) {
return;
}
List<File> gpxFiles = getGpxFiles();
gpxFileCount = gpxFiles.size();
if (gpxFileCount == 0) {
return;
}
Log.i(Constants.TAG, "ImportAllTracks: Importing: " + gpxFileCount + " tracks.");
activity.runOnUiThread(new Runnable() {
public void run() {
makeProgressDialog(gpxFileCount);
}
});
Iterator<File> gpxFilesIterator = gpxFiles.iterator();
for (int currentFileNumber = 0; gpxFilesIterator.hasNext(); currentFileNumber++) {
File currentFile = gpxFilesIterator.next();
final int status = currentFileNumber;
activity.runOnUiThread(new Runnable() {
public void run() {
synchronized (this) {
if (progress == null) {
return;
}
progress.setProgress(status);
}
}
});
if (importFile(currentFile, providerUtils)) {
importSuccessCount++;
}
}
if (progress != null) {
synchronized (this) {
progress.dismiss();
progress = null;
}
}
}
/**
* Attempts to import a GPX file. Returns true on success, issues error
* notifications and returns false on failure.
*/
private boolean importFile(File gpxFile, MyTracksProviderUtils providerUtils) {
Log.i(Constants.TAG, "ImportAllTracks: importing: " + gpxFile.getName());
try {
GpxImporter.importGPXFile(new FileInputStream(gpxFile), providerUtils);
return true;
} catch (FileNotFoundException e) {
Log.w(Constants.TAG, "GPX file wasn't found/went missing: "
+ gpxFile.getAbsolutePath(), e);
} catch (ParserConfigurationException e) {
Log.w(Constants.TAG, "Error parsing file: " + gpxFile.getAbsolutePath(), e);
} catch (SAXException e) {
Log.w(Constants.TAG, "Error parsing file: " + gpxFile.getAbsolutePath(), e);
} catch (IOException e) {
Log.w(Constants.TAG, "Error reading file: " + gpxFile.getAbsolutePath(), e);
}
Toast.makeText(activity, activity.getString(R.string.import_error, gpxFile.getName()),
Toast.LENGTH_LONG).show();
return false;
}
/**
* Returns a list of the GPX Files found in the GPX directory.
*/
private List<File> getGpxFiles() {
List<File> gpxFiles = new LinkedList<File>();
File[] gpxFileCandidates = new File(gpxPath).listFiles();
if (gpxFileCandidates != null) {
for (File file : gpxFileCandidates) {
if (!file.isDirectory() && file.getName().endsWith(".gpx")) {
gpxFiles.add(file);
}
}
}
return gpxFiles;
}
}
@@ -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,620 @@
/*
* 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.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.TrackDataHub;
import com.google.android.apps.mytracks.content.TrackDataHub.ListenerDataType;
import com.google.android.apps.mytracks.content.TrackDataListener;
import com.google.android.apps.mytracks.content.TracksColumns;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.io.file.SaveActivity;
import com.google.android.apps.mytracks.io.sendtogoogle.SendActivity;
import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerFactory;
import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.apps.mytracks.util.GeoRect;
import com.google.android.apps.mytracks.util.LocationUtils;
import com.google.android.maps.GeoPoint;
import com.google.android.maps.MapController;
import com.google.android.maps.MapView;
import com.google.android.maps.mytracks.R;
import android.content.ContentUris;
import android.content.Intent;
import android.location.Location;
import android.net.Uri;
import android.os.Bundle;
import android.provider.Settings;
import android.util.Log;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.SubMenu;
import android.view.View;
import android.view.View.OnCreateContextMenuListener;
import android.view.Window;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;
import java.util.EnumSet;
/**
* The map view activity of the MyTracks application.
*
* @author Leif Hendrik Wilden
* @author Rodrigo Damazio
*/
public class MapActivity extends com.google.android.maps.MapActivity
implements View.OnTouchListener, View.OnClickListener,
TrackDataListener {
// Saved instance state keys:
// ---------------------------
private static final String KEY_CURRENT_LOCATION = "currentLocation";
private static final String KEY_KEEP_MY_LOCATION_VISIBLE = "keepMyLocationVisible";
private TrackDataHub dataHub;
/**
* True if the map should be scrolled so that the pointer is always in the
* visible area.
*/
private boolean keepMyLocationVisible;
/**
* The current pointer location.
* This is kept to quickly center on it when the user requests.
*/
private Location currentLocation;
// UI elements:
// -------------
private RelativeLayout screen;
private MapView mapView;
private MapOverlay mapOverlay;
private LinearLayout messagePane;
private TextView messageText;
private LinearLayout busyPane;
private ImageButton optionsBtn;
private MenuItem myLocation;
private MenuItem toggleLayers;
/**
* We are not displaying driving directions. Just an arbitrary track that is
* not associated to any licensed mapping data. Therefore it should be okay to
* return false here and still comply with the terms of service.
*/
@Override
protected boolean isRouteDisplayed() {
return false;
}
/**
* We are displaying a location. This needs to return true in order to comply
* with the terms of service.
*/
@Override
protected boolean isLocationDisplayed() {
return true;
}
// Application life cycle:
// ------------------------
@Override
protected void onCreate(Bundle bundle) {
Log.d(TAG, "MapActivity.onCreate");
super.onCreate(bundle);
// The volume we want to control is the Text-To-Speech volume
int volumeStream =
new StatusAnnouncerFactory(ApiFeatures.getInstance()).getVolumeStream();
setVolumeControlStream(volumeStream);
// We don't need a window title bar:
requestWindowFeature(Window.FEATURE_NO_TITLE);
// Inflate the layout:
setContentView(R.layout.mytracks_layout);
// Remove the window's background because the MapView will obscure it
getWindow().setBackgroundDrawable(null);
// Set up a map overlay:
screen = (RelativeLayout) findViewById(R.id.screen);
mapView = (MapView) findViewById(R.id.map);
mapView.requestFocus();
mapOverlay = new MapOverlay(this);
mapView.getOverlays().add(mapOverlay);
mapView.setOnTouchListener(this);
mapView.setBuiltInZoomControls(true);
messagePane = (LinearLayout) findViewById(R.id.messagepane);
messageText = (TextView) findViewById(R.id.messagetext);
busyPane = (LinearLayout) findViewById(R.id.busypane);
optionsBtn = (ImageButton) findViewById(R.id.showOptions);
optionsBtn.setOnCreateContextMenuListener(contextMenuListener);
optionsBtn.setOnClickListener(this);
}
@Override
protected void onRestoreInstanceState(Bundle bundle) {
Log.d(TAG, "MapActivity.onRestoreInstanceState");
if (bundle != null) {
super.onRestoreInstanceState(bundle);
keepMyLocationVisible =
bundle.getBoolean(KEY_KEEP_MY_LOCATION_VISIBLE, false);
if (bundle.containsKey(KEY_CURRENT_LOCATION)) {
currentLocation = (Location) bundle.getParcelable(KEY_CURRENT_LOCATION);
if (currentLocation != null) {
showCurrentLocation();
}
} else {
currentLocation = null;
}
}
}
@Override
protected void onResume() {
Log.d(TAG, "MapActivity.onResume");
super.onResume();
dataHub = ((MyTracksApplication) getApplication()).getTrackDataHub();
dataHub.registerTrackDataListener(this, EnumSet.of(
ListenerDataType.SELECTED_TRACK_CHANGED,
ListenerDataType.POINT_UPDATES,
ListenerDataType.WAYPOINT_UPDATES,
ListenerDataType.LOCATION_UPDATES,
ListenerDataType.COMPASS_UPDATES));
}
@Override
protected void onSaveInstanceState(Bundle outState) {
Log.d(TAG, "MapActivity.onSaveInstanceState");
outState.putBoolean(KEY_KEEP_MY_LOCATION_VISIBLE, keepMyLocationVisible);
if (currentLocation != null) {
outState.putParcelable(KEY_CURRENT_LOCATION, currentLocation);
}
super.onSaveInstanceState(outState);
}
@Override
protected void onPause() {
Log.d(TAG, "MapActivity.onPause");
dataHub.unregisterTrackDataListener(this);
dataHub = null;
super.onPause();
}
// Utility functions:
// -------------------
/**
* Shows the options button if a track is selected, or hide it if not.
*/
private void updateOptionsButton(boolean trackSelected) {
optionsBtn.setVisibility(
trackSelected ? View.VISIBLE : View.INVISIBLE);
}
/**
* Tests if a location is visible.
*
* @param location a given location
* @return true if the given location is within the visible map area
*/
private boolean locationIsVisible(Location location) {
if (location == null || mapView == null) {
return false;
}
GeoPoint center = mapView.getMapCenter();
int latSpan = mapView.getLatitudeSpan();
int lonSpan = mapView.getLongitudeSpan();
// Bottom of map view is obscured by zoom controls/buttons.
// Subtract a margin from the visible area:
GeoPoint marginBottom = mapView.getProjection().fromPixels(
0, mapView.getHeight());
GeoPoint marginTop = mapView.getProjection().fromPixels(0,
mapView.getHeight()
- mapView.getZoomButtonsController().getZoomControls().getHeight());
int margin =
Math.abs(marginTop.getLatitudeE6() - marginBottom.getLatitudeE6());
GeoRect r = new GeoRect(center, latSpan, lonSpan);
r.top += margin;
GeoPoint geoPoint = LocationUtils.getGeoPoint(location);
return r.contains(geoPoint);
}
/**
* Moves the location pointer to the current location and center the map if
* the current location is outside the visible area.
*/
private void showCurrentLocation() {
if (mapOverlay == null || mapView == null) {
return;
}
mapOverlay.setMyLocation(currentLocation);
mapView.postInvalidate();
if (currentLocation != null && keepMyLocationVisible && !locationIsVisible(currentLocation)) {
GeoPoint geoPoint = LocationUtils.getGeoPoint(currentLocation);
MapController controller = mapView.getController();
controller.animateTo(geoPoint);
}
}
@Override
public void onTrackUpdated(Track track) {
// We don't care.
}
/**
* Zooms and pans the map so that the given track is visible.
*
* @param track the track
*/
private void zoomMapToBoundaries(Track track) {
if (mapView == null) {
return;
}
if (track == null || track.getNumberOfPoints() < 2) {
return;
}
TripStatistics stats = track.getStatistics();
int bottom = stats.getBottom();
int left = stats.getLeft();
int latSpanE6 = stats.getTop() - bottom;
int lonSpanE6 = stats.getRight() - left;
if (latSpanE6 > 0
&& latSpanE6 < 180E6
&& lonSpanE6 > 0
&& lonSpanE6 < 360E6) {
keepMyLocationVisible = false;
GeoPoint center = new GeoPoint(
bottom + latSpanE6 / 2,
left + lonSpanE6 / 2);
if (LocationUtils.isValidGeoPoint(center)) {
mapView.getController().setCenter(center);
mapView.getController().zoomToSpan(latSpanE6, lonSpanE6);
}
}
}
/**
* Zooms and pans the map so that the given waypoint is visible.
*/
public void showWaypoint(long waypointId) {
MyTracksProviderUtils providerUtils = MyTracksProviderUtils.Factory.get(this);
Waypoint wpt = providerUtils.getWaypoint(waypointId);
if (wpt != null && wpt.getLocation() != null) {
keepMyLocationVisible = false;
GeoPoint center = new GeoPoint(
(int) (wpt.getLocation().getLatitude() * 1E6),
(int) (wpt.getLocation().getLongitude() * 1E6));
mapView.getController().setCenter(center);
mapView.getController().setZoom(20);
mapView.invalidate();
}
}
@Override
public void onSelectedTrackChanged(final Track track, final boolean isRecording) {
runOnUiThread(new Runnable() {
@Override
public void run() {
boolean trackSelected = track != null;
updateOptionsButton(trackSelected);
mapOverlay.setTrackDrawingEnabled(trackSelected);
if (trackSelected) {
busyPane.setVisibility(View.VISIBLE);
zoomMapToBoundaries(track);
mapOverlay.setShowEndMarker(!isRecording);
busyPane.setVisibility(View.GONE);
}
mapView.invalidate();
}
});
}
private final OnCreateContextMenuListener contextMenuListener =
new OnCreateContextMenuListener() {
@Override
public void onCreateContextMenu(ContextMenu menu, View v,
ContextMenuInfo menuInfo) {
menu.setHeaderTitle(R.string.tracklist_this_track);
menu.add(0, Constants.MENU_EDIT, 0,
R.string.tracklist_edit_track);
if (!dataHub.isRecordingSelected()) {
menu.add(0, Constants.MENU_SEND_TO_GOOGLE, 0,
R.string.tracklist_send_to_google);
SubMenu share = menu.addSubMenu(0, Constants.MENU_SHARE, 0,
R.string.tracklist_share_track);
share.add(0, Constants.MENU_SHARE_LINK, 0,
R.string.tracklist_share_link);
share.add(0, Constants.MENU_SHARE_GPX_FILE, 0,
R.string.tracklist_share_gpx_file);
share.add(0, Constants.MENU_SHARE_KML_FILE, 0,
R.string.tracklist_share_kml_file);
share.add(0, Constants.MENU_SHARE_CSV_FILE, 0,
R.string.tracklist_share_csv_file);
share.add(0, Constants.MENU_SHARE_TCX_FILE, 0,
R.string.tracklist_share_tcx_file);
SubMenu save = menu.addSubMenu(0,
Constants.MENU_WRITE_TO_SD_CARD, 0,
R.string.tracklist_write_to_sd);
save.add(0, Constants.MENU_SAVE_GPX_FILE, 0,
R.string.tracklist_save_as_gpx);
save.add(0, Constants.MENU_SAVE_KML_FILE, 0,
R.string.tracklist_save_as_kml);
save.add(0, Constants.MENU_SAVE_CSV_FILE, 0,
R.string.tracklist_save_as_csv);
save.add(0, Constants.MENU_SAVE_TCX_FILE, 0,
R.string.tracklist_save_as_tcx);
menu.add(0, Constants.MENU_CLEAR_MAP, 0,
R.string.tracklist_clear_map);
menu.add(0, Constants.MENU_DELETE, 0,
R.string.tracklist_delete_track);
}
}
};
@Override
public boolean onMenuItemSelected(int featureId, MenuItem item) {
switch (item.getItemId()) {
case Constants.MENU_SEND_TO_GOOGLE:
SendActivity.sendToGoogle(this, dataHub.getSelectedTrackId(), false);
return true;
case Constants.MENU_SHARE_LINK:
SendActivity.sendToGoogle(this, dataHub.getSelectedTrackId(), true);
return true;
case Constants.MENU_SAVE_GPX_FILE:
case Constants.MENU_SAVE_KML_FILE:
case Constants.MENU_SAVE_CSV_FILE:
case Constants.MENU_SAVE_TCX_FILE:
case Constants.MENU_SHARE_GPX_FILE:
case Constants.MENU_SHARE_KML_FILE:
case Constants.MENU_SHARE_CSV_FILE:
case Constants.MENU_SHARE_TCX_FILE:
SaveActivity.handleExportTrackAction(this, dataHub.getSelectedTrackId(),
Constants.getActionFromMenuId(item.getItemId()));
return true;
case Constants.MENU_EDIT: {
Intent intent = new Intent(this, TrackDetails.class);
// TODO: Pass in a content URI
intent.putExtra("trackid", dataHub.getSelectedTrackId());
startActivity(intent);
return true;
}
case Constants.MENU_DELETE: {
Uri uri = ContentUris.withAppendedId(
TracksColumns.CONTENT_URI, dataHub.getSelectedTrackId());
Intent intent = new Intent(Intent.ACTION_DELETE, uri);
startActivity(intent);
return true;
}
case Constants.MENU_CLEAR_MAP:
dataHub.unloadCurrentTrack();
return true;
default:
return super.onMenuItemSelected(featureId, item);
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
myLocation = menu.add(0, Constants.MENU_MY_LOCATION, 0,
R.string.mylocation);
myLocation.setIcon(android.R.drawable.ic_menu_mylocation);
toggleLayers = menu.add(0, Constants.MENU_TOGGLE_LAYERS, 0,
R.string.switch_to_sat);
toggleLayers.setIcon(android.R.drawable.ic_menu_mapmode);
return true;
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
toggleLayers.setTitle(mapView.isSatellite() ?
R.string.switch_to_map : R.string.switch_to_sat);
return super.onPrepareOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case Constants.MENU_MY_LOCATION: {
dataHub.forceUpdateLocation();
keepMyLocationVisible = true;
if (mapView.getZoomLevel() < 18) {
mapView.getController().setZoom(18);
}
return true;
}
case Constants.MENU_TOGGLE_LAYERS: {
mapView.setSatellite(!mapView.isSatellite());
return true;
}
}
return super.onOptionsItemSelected(item);
}
@Override
public void onClick(View v) {
if (v == messagePane) {
startActivity(new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS));
} else if (v == optionsBtn) {
optionsBtn.performLongClick();
}
}
/**
* We want the pointer to become visible again in case of the next location
* update:
*/
@Override
public boolean onTouch(View view, MotionEvent event) {
if (keepMyLocationVisible && event.getAction() == MotionEvent.ACTION_MOVE) {
if (!locationIsVisible(currentLocation)) {
keepMyLocationVisible = false;
}
}
return false;
}
@Override
public void onProviderStateChange(ProviderState state) {
final int messageId;
final boolean isGpsDisabled;
switch (state) {
case DISABLED:
messageId = R.string.status_need_to_enable_gps;
isGpsDisabled = true;
break;
case NO_FIX:
case BAD_FIX:
messageId = R.string.status_wait_for_gps_fix;
isGpsDisabled = false;
break;
case GOOD_FIX:
// Nothing to show.
messageId = -1;
isGpsDisabled = false;
break;
default:
throw new IllegalArgumentException("Unexpected state: " + state);
}
runOnUiThread(new Runnable() {
@Override
public void run() {
if (messageId != -1) {
messageText.setText(messageId);
messagePane.setVisibility(View.VISIBLE);
if (isGpsDisabled) {
// Give a warning about this state.
Toast.makeText(MapActivity.this,
R.string.error_no_gps,
Toast.LENGTH_LONG).show();
// Make clicking take the user to the location settings.
messagePane.setOnClickListener(MapActivity.this);
} else {
messagePane.setOnClickListener(null);
}
} else {
messagePane.setVisibility(View.GONE);
}
screen.requestLayout();
}
});
}
@Override
public void onCurrentLocationChanged(Location location) {
currentLocation = location;
showCurrentLocation();
}
@Override
public void onCurrentHeadingChanged(double heading) {
synchronized (this) {
if (mapOverlay.setHeading((float) heading)) {
mapView.postInvalidate();
}
}
}
@Override
public void clearWaypoints() {
mapOverlay.clearWaypoints();
}
@Override
public void onNewWaypoint(Waypoint waypoint) {
if (LocationUtils.isValidLocation(waypoint.getLocation())) {
// TODO: Optimize locking inside addWaypoint
mapOverlay.addWaypoint(waypoint);
}
}
@Override
public void onNewWaypointsDone() {
mapView.postInvalidate();
}
@Override
public void clearTrackPoints() {
mapOverlay.clearPoints();
}
@Override
public void onNewTrackPoint(Location loc) {
mapOverlay.addLocation(loc);
}
@Override
public void onSegmentSplit() {
mapOverlay.addSegmentSplit();
}
@Override
public void onSampledOutTrackPoint(Location loc) {
// We don't care.
}
@Override
public void onNewTrackPointsDone() {
mapView.postInvalidate();
}
@Override
public boolean onUnitsChanged(boolean metric) {
// We don't care.
return false;
}
@Override
public boolean onReportSpeedChanged(boolean reportSpeed) {
// We don't care.
return false;
}
}
@@ -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;
import static com.google.android.apps.mytracks.Constants.TAG;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.maps.TrackPathPainter;
import com.google.android.apps.mytracks.maps.TrackPathPainterFactory;
import com.google.android.apps.mytracks.maps.TrackPathUtilities;
import com.google.android.apps.mytracks.util.LocationUtils;
import com.google.android.maps.GeoPoint;
import com.google.android.maps.MapView;
import com.google.android.maps.Overlay;
import com.google.android.maps.Projection;
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.res.Resources;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
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 a "MyLocation" arrow, an error circle, the
* currently recording track and optionally a selected track.
*
* @author Leif Hendrik Wilden
*/
public class MapOverlay extends Overlay implements OnSharedPreferenceChangeListener {
private final Drawable[] arrows;
private final int arrowWidth, arrowHeight;
private final Drawable statsMarker;
private final Drawable waypointMarker;
private final Drawable startMarker;
private final Drawable endMarker;
private final int markerWidth, markerHeight;
private final Paint errorCirclePaint;
private final Context context;
private final List<Waypoint> waypoints;
private final List<CachedLocation> points;
private final BlockingQueue<CachedLocation> pendingPoints;
private boolean trackDrawingEnabled;
private int lastHeading = 0;
private Location myLocation;
private boolean showEndMarker = true;
// TODO: Remove it completely after completing performance tests.
private boolean alwaysVisible = true;
private GeoPoint lastReferencePoint;
private Rect lastViewRect;
private boolean lastPathExists;
private Location lastLocation;
private TrackPathPainter trackPathPainter;
/**
* Represents a pre-processed {@code Location} to speed up drawing.
* This class is more like a data object and doesn't provide accessors.
*/
public static class CachedLocation {
public final boolean valid;
public final GeoPoint geoPoint;
public final int speed;
/**
* Constructor for an invalid cached location.
*/
public CachedLocation() {
this.valid = false;
this.geoPoint = null;
this.speed = -1;
}
/**
* Constructor for a potentially valid cached location.
*/
public CachedLocation(Location location) {
this.valid = LocationUtils.isValidLocation(location);
this.geoPoint = valid ? LocationUtils.getGeoPoint(location) : null;
this.speed = (int) Math.floor(location.getSpeed() * 3.6);
}
};
public MapOverlay(Context context) {
this.context = context;
this.waypoints = new ArrayList<Waypoint>();
this.points = new ArrayList<CachedLocation>(1024);
this.pendingPoints = new ArrayBlockingQueue<CachedLocation>(
Constants.MAX_DISPLAYED_TRACK_POINTS, true);
// TODO: Can we use a FrameAnimation or similar here rather
// than individual resources for each arrow direction?
final Resources resources = context.getResources();
arrows = new Drawable[] {
resources.getDrawable(R.drawable.arrow_0),
resources.getDrawable(R.drawable.arrow_20),
resources.getDrawable(R.drawable.arrow_40),
resources.getDrawable(R.drawable.arrow_60),
resources.getDrawable(R.drawable.arrow_80),
resources.getDrawable(R.drawable.arrow_100),
resources.getDrawable(R.drawable.arrow_120),
resources.getDrawable(R.drawable.arrow_140),
resources.getDrawable(R.drawable.arrow_160),
resources.getDrawable(R.drawable.arrow_180),
resources.getDrawable(R.drawable.arrow_200),
resources.getDrawable(R.drawable.arrow_220),
resources.getDrawable(R.drawable.arrow_240),
resources.getDrawable(R.drawable.arrow_260),
resources.getDrawable(R.drawable.arrow_280),
resources.getDrawable(R.drawable.arrow_300),
resources.getDrawable(R.drawable.arrow_320),
resources.getDrawable(R.drawable.arrow_340)
};
arrowWidth = arrows[lastHeading].getIntrinsicWidth();
arrowHeight = arrows[lastHeading].getIntrinsicHeight();
for (Drawable arrow : arrows) {
arrow.setBounds(0, 0, arrowWidth, arrowHeight);
}
statsMarker = resources.getDrawable(R.drawable.ylw_pushpin);
markerWidth = statsMarker.getIntrinsicWidth();
markerHeight = statsMarker.getIntrinsicHeight();
statsMarker.setBounds(0, 0, markerWidth, markerHeight);
startMarker = resources.getDrawable(R.drawable.green_dot);
startMarker.setBounds(0, 0, markerWidth, markerHeight);
endMarker = resources.getDrawable(R.drawable.red_dot);
endMarker.setBounds(0, 0, markerWidth, markerHeight);
waypointMarker = resources.getDrawable(R.drawable.blue_pushpin);
waypointMarker.setBounds(0, 0, markerWidth, markerHeight);
errorCirclePaint = TrackPathUtilities.getPaint(R.color.blue, context);
errorCirclePaint.setAlpha(127);
trackPathPainter = TrackPathPainterFactory.getTrackPathPainter(context);
context.getSharedPreferences(Constants.SETTINGS_NAME, 0)
.registerOnSharedPreferenceChangeListener(this);
}
/**
* Add a location to the map overlay.
*
* NOTE: This method doesn't take ownership of the given location, so it is
* safe to reuse the same location while calling this method.
*
* @param l the location to add.
*/
public void addLocation(Location l) {
// Queue up in the pending queue until it's merged with {@code #points}.
if (!pendingPoints.offer(new CachedLocation(l))) {
Log.e(TAG, "Unable to add pending points");
}
}
/**
* Adds a segment split to the map overlay.
*/
public void addSegmentSplit() {
if (!pendingPoints.offer(new CachedLocation())) {
Log.e(TAG, "Unable to add pending points");
}
}
public void addWaypoint(Waypoint wpt) {
// Note: We don't cache waypoints, because it's not worth the effort.
if (wpt != null && wpt.getLocation() != null) {
synchronized (waypoints) {
waypoints.add(wpt);
}
}
}
public int getNumLocations() {
synchronized (points) {
return points.size() + pendingPoints.size();
}
}
// Visible for testing.
public int getNumWaypoints() {
synchronized (waypoints) {
return waypoints.size();
}
}
public void clearPoints() {
synchronized (getPoints()) {
getPoints().clear();
pendingPoints.clear();
lastPathExists = false;
lastViewRect = null;
trackPathPainter.clear();
}
}
public void clearWaypoints() {
synchronized (waypoints) {
waypoints.clear();
}
}
public void setTrackDrawingEnabled(boolean trackDrawingEnabled) {
this.trackDrawingEnabled = trackDrawingEnabled;
}
public void setShowEndMarker(boolean showEndMarker) {
this.showEndMarker = showEndMarker;
}
@Override
public void draw(Canvas canvas, MapView mapView, boolean shadow) {
if (shadow) {
return;
}
// It's safe to keep projection within a single draw operation.
final Projection projection = getMapProjection(mapView);
if (projection == null) {
Log.w(TAG, "No projection, unable to draw");
return;
}
// Get the current viewing window.
if (trackDrawingEnabled) {
Rect viewRect = getMapViewRect(mapView);
// Draw the selected track:
drawTrack(canvas, projection, viewRect);
// Draw the "Start" and "End" markers:
drawMarkers(canvas, projection);
// Draw the waypoints:
drawWaypoints(canvas, projection);
}
// Draw the current location
drawMyLocation(canvas, projection);
}
private void drawMarkers(Canvas canvas, Projection projection) {
// Draw the "End" marker.
if (showEndMarker) {
for (int i = getPoints().size() - 1; i >= 0; --i) {
if (getPoints().get(i).valid) {
drawElement(canvas, projection, getPoints().get(i).geoPoint, endMarker,
-markerWidth / 2, -markerHeight);
break;
}
}
}
// Draw the "Start" marker.
for (int i = 0; i < getPoints().size(); ++i) {
if (getPoints().get(i).valid) {
drawElement(canvas, projection, getPoints().get(i).geoPoint, startMarker,
-markerWidth / 2, -markerHeight);
break;
}
}
}
// Visible for testing.
Projection getMapProjection(MapView mapView) {
return mapView.getProjection();
}
// Visible for testing.
Rect getMapViewRect(MapView mapView) {
int w = mapView.getLongitudeSpan();
int h = mapView.getLatitudeSpan();
int cx = mapView.getMapCenter().getLongitudeE6();
int cy = mapView.getMapCenter().getLatitudeE6();
return new Rect(cx - w / 2, cy - h / 2, cx + w / 2, cy + h / 2);
}
// For use in testing only.
public TrackPathPainter getTrackPathPainter() {
return trackPathPainter;
}
// For use in testing only.
public void setTrackPathPainter(TrackPathPainter trackPathPainter) {
this.trackPathPainter = trackPathPainter;
}
private void drawWaypoints(Canvas canvas, Projection projection) {
synchronized (waypoints) {;
for (Waypoint wpt : waypoints) {
Location loc = wpt.getLocation();
drawElement(canvas, projection, LocationUtils.getGeoPoint(loc),
wpt.getType() == Waypoint.TYPE_STATISTICS ? statsMarker
: waypointMarker, -(markerWidth / 2) + 3, -markerHeight);
}
}
}
private void drawMyLocation(Canvas canvas, Projection projection) {
// Draw the arrow icon.
if (myLocation == null) {
return;
}
Point pt = drawElement(canvas, projection,
LocationUtils.getGeoPoint(myLocation), arrows[lastHeading],
-(arrowWidth / 2) + 3, -(arrowHeight / 2));
// Draw the error circle.
float radius = projection.metersToEquatorPixels(myLocation.getAccuracy());
canvas.drawCircle(pt.x, pt.y, radius, errorCirclePaint);
}
private void drawTrack(Canvas canvas, Projection projection, Rect viewRect)
{
boolean draw;
synchronized (points) {
// Merge the pending points with the list of cached locations.
final GeoPoint referencePoint = projection.fromPixels(0, 0);
int newPoints = pendingPoints.drainTo(points);
boolean newProjection = !viewRect.equals(lastViewRect) ||
!referencePoint.equals(lastReferencePoint);
if (newPoints == 0 && lastPathExists && !newProjection) {
// No need to recreate path (same points and viewing area).
draw = true;
} else {
int numPoints = points.size();
if (numPoints < 2) {
// Not enough points to draw a path.
draw = false;
} else if (!trackPathPainter.needsRedraw() && lastPathExists && !newProjection) {
// Incremental update of the path, without repositioning the view.
draw = true;
trackPathPainter.updatePath(projection, viewRect, numPoints - newPoints, alwaysVisible, points);
} else {
// The view has changed so we have to start from scratch.
draw = true;
trackPathPainter.updatePath(projection, viewRect, 0, alwaysVisible, points);
}
}
lastReferencePoint = referencePoint;
lastViewRect = viewRect;
}
if (draw) {
trackPathPainter.drawTrack(canvas);
}
}
// Visible for testing.
Point drawElement(Canvas canvas, Projection projection, GeoPoint geoPoint,
Drawable element, int offsetX, int offsetY) {
Point pt = new Point();
projection.toPixels(geoPoint, pt);
canvas.save();
canvas.translate(pt.x + offsetX, pt.y + offsetY);
element.draw(canvas);
canvas.restore();
return pt;
}
/**
* Sets the pointer location (will be drawn on next invalidate).
*/
public void setMyLocation(Location myLocation) {
this.myLocation = myLocation;
}
/**
* Sets the pointer heading in degrees (will be drawn on next invalidate).
*
* @return true if the visible heading changed (i.e. a redraw of pointer is
* potentially necessary)
*/
public boolean setHeading(float heading) {
int newhdg = Math.round(-heading / 360 * 18 + 180);
while (newhdg < 0)
newhdg += 18;
while (newhdg > 17)
newhdg -= 18;
if (newhdg != lastHeading) {
lastHeading = newhdg;
return true;
} else {
return false;
}
}
@Override
public boolean onTap(GeoPoint p, MapView mapView) {
if (p.equals(mapView.getMapCenter())) {
// There is (unfortunately) no good way to determine whether the tap was
// caused by an actual tap on the screen or the track ball. If the
// location is equal to the map center,then it was a track ball press with
// very high likelihood.
return false;
}
final Location tapLocation = LocationUtils.getLocation(p);
double dmin = Double.MAX_VALUE;
Waypoint waypoint = null;
synchronized (waypoints) {
for (int i = 0; i < waypoints.size(); i++) {
final Location waypointLocation = waypoints.get(i).getLocation();
if (waypointLocation == null) {
continue;
}
final double d = waypointLocation.distanceTo(tapLocation);
if (d < dmin) {
dmin = d;
waypoint = waypoints.get(i);
}
}
}
if (waypoint != null &&
dmin < 15000000 / Math.pow(2, mapView.getZoomLevel())) {
Intent intent = new Intent(context, WaypointDetails.class);
intent.putExtra(WaypointDetails.WAYPOINT_ID_EXTRA, waypoint.getId());
context.startActivity(intent);
return true;
}
return super.onTap(p, mapView);
}
/**
* @return the points
*/
public List<CachedLocation> getPoints() {
return points;
}
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
Log.d(TAG, "MapOverlay: onSharedPreferences changed " + key);
if (key != null) {
if (key.equals(context.getString(R.string.track_color_mode_key))) {
trackPathPainter = TrackPathPainterFactory.getTrackPathPainter(context);
}
}
}
}
@@ -0,0 +1,97 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks;
import com.google.android.maps.mytracks.R;
import android.app.Activity;
import android.content.Intent;
import android.view.Menu;
import android.view.MenuItem;
/**
* Manage the application menus.
*
* @author Sandor Dornbush
*/
class MenuManager {
private final MyTracks activity;
public MenuManager(MyTracks activity) {
this.activity = activity;
}
public boolean onCreateOptionsMenu(Menu menu) {
activity.getMenuInflater().inflate(R.menu.main, menu);
return true;
}
public void onPrepareOptionsMenu(Menu menu, boolean hasRecorded,
boolean isRecording, boolean hasSelectedTrack) {
menu.findItem(R.id.menu_list_tracks);
menu.findItem(R.id.menu_list_markers)
.setEnabled(hasRecorded && hasSelectedTrack);
menu.findItem(R.id.menu_start_recording)
.setEnabled(!isRecording)
.setVisible(!isRecording);
menu.findItem(R.id.menu_stop_recording)
.setEnabled(isRecording)
.setVisible(isRecording);
}
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_start_recording: {
activity.startRecording();
return true;
}
case R.id.menu_stop_recording: {
activity.stopRecording();
return true;
}
case R.id.menu_list_tracks: {
activity.startActivityForResult(new Intent(activity, TrackList.class),
Constants.SHOW_TRACK);
return true;
}
case R.id.menu_list_markers: {
Intent startIntent = new Intent(activity, WaypointsList.class);
startIntent.putExtra("trackid", activity.getSelectedTrackId());
activity.startActivityForResult(startIntent, Constants.SHOW_WAYPOINT);
return true;
}
case R.id.menu_sensor_state: {
return startActivity(SensorStateActivity.class);
}
case R.id.menu_settings: {
return startActivity(SettingsActivity.class);
}
case R.id.menu_aggregated_stats: {
return startActivity(AggregatedStatsActivity.class);
}
case R.id.menu_help: {
return startActivity(WelcomeActivity.class);
}
}
return false;
}
private boolean startActivity(Class<? extends Activity> activityClass) {
activity.startActivity(new Intent(activity, activityClass));
return true;
}
}
@@ -0,0 +1,175 @@
/*
* 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 static com.google.android.apps.mytracks.Constants.TAG;
import com.google.android.apps.mytracks.io.AuthManager;
import com.google.android.apps.mytracks.io.AuthManager.AuthCallback;
import com.google.android.apps.mytracks.io.AuthManagerFactory;
import com.google.android.apps.mytracks.io.mymaps.MapsFacade;
import com.google.android.apps.mytracks.io.mymaps.MyMapsConstants;
import com.google.android.maps.mytracks.R;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ListView;
import android.widget.TextView;
/**
* Activity which displays the list of current My Maps tracks for the user.
* Returns RESULT_OK if the user picked a map, and returns "mapid" as an extra.
*
* @author Rodrigo Damazio
*/
public class MyMapsList extends Activity implements MapsFacade.MapsListCallback {
private static final int GET_LOGIN = 1;
public static final String EXTRA_ACCOUNT_NAME = "accountName";
public static final String EXTRA_ACCOUNT_TYPE = "accountType";
private MapsFacade mapsClient;
private AuthManager auth;
private MyMapsListAdapter listAdapter;
private final OnItemClickListener clickListener =
new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View v, int position,
long id) {
Intent result = new Intent();
result.putExtra("mapid", (String) listAdapter.getItem(position));
setResult(RESULT_OK, result);
finish();
}
};
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
auth = AuthManagerFactory.getAuthManager(this, GET_LOGIN, null, true,
MyMapsConstants.SERVICE_NAME);
setContentView(R.layout.list);
listAdapter = new MyMapsListAdapter(this);
ListView list = (ListView) findViewById(R.id.maplist);
list.setOnItemClickListener(clickListener);
list.setAdapter(listAdapter);
startLogin();
}
private void startLogin() {
// Starts in the UI thread.
// TODO fix this for non-froyo devices.
if (AuthManagerFactory.useModernAuthManager()) {
Intent intent = getIntent();
String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
if (accountName == null || accountType == null) {
Log.e(TAG, "Didn't receive account name or type");
setResult(RESULT_CANCELED);
finish();
return;
}
doLogin(auth.getAccountObject(accountName, accountType));
} else {
doLogin(null);
}
}
private void doLogin(final Object account) {
// Starts in the UI thread.
auth.doLogin(new AuthCallback() {
@Override
public void onAuthResult(boolean success) {
if (!success) {
setResult(RESULT_CANCELED);
finish();
return;
}
// Runs in UI thread.
mapsClient = new MapsFacade(MyMapsList.this, auth);
startLookup();
}
}, account);
}
private void startLookup() {
// Starts in the UI thread.
new Thread() {
@Override
public void run() {
// Communication with Maps happens in its own thread.
// This will call onReceivedMapListing below.
final boolean success = mapsClient.getMapsList(MyMapsList.this);
runOnUiThread(new Runnable() {
@Override
public void run() {
// Updating the UI when done happens in the UI thread.
onLookupDone(success);
}
});
}
}.start();
}
@Override
public void onReceivedMapListing(final String mapId, final String title,
final String description, final boolean isPublic) {
// Starts in the communication thread.
runOnUiThread(new Runnable() {
@Override
public void run() {
// Updating the list with new contents happens in the UI thread.
listAdapter.addMapListing(mapId, title, description, isPublic);
}
});
}
private void onLookupDone(boolean success) {
// Starts in the UI thread.
findViewById(R.id.loading).setVisibility(View.GONE);
if (!success) {
findViewById(R.id.failed).setVisibility(View.VISIBLE);
}
TextView emptyView = (TextView) findViewById(R.id.mapslist_empty);
ListView list = (ListView) findViewById(R.id.maplist);
list.setEmptyView(emptyView);
}
@Override
protected void onActivityResult(int requestCode, int resultCode,
Intent data) {
if (requestCode == GET_LOGIN) {
auth.authResult(resultCode, data);
}
super.onActivityResult(requestCode, resultCode, data);
}
}
@@ -0,0 +1,135 @@
package com.google.android.apps.mytracks;
import com.google.android.maps.mytracks.R;
import android.app.Activity;
import android.database.DataSetObserver;
import android.graphics.Color;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ListAdapter;
import android.widget.TextView;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.Vector;
public class MyMapsListAdapter implements ListAdapter {
private Vector<String[]> mapsList;
private Vector<Boolean> publicList;
private Set<DataSetObserver> observerSet;
private final Activity activity;
public MyMapsListAdapter(Activity activity) {
this.activity = activity;
mapsList = new Vector<String[]>();
publicList = new Vector<Boolean>();
observerSet = new HashSet<DataSetObserver>();
}
public void addMapListing(String mapId, String title,
String description, boolean isPublic) {
synchronized (mapsList) {
// Search through the maps list to see if it has the mapid and
// remove it if so, so that we can replace it with updated info
for (int i = 0; i < mapsList.size(); ++i) {
if (mapId.equals(mapsList.get(i)[0])) {
mapsList.remove(i);
publicList.remove(i);
--i;
}
}
mapsList.add(new String[] { mapId, title, description });
publicList.add(isPublic);
}
Iterator<DataSetObserver> iter = observerSet.iterator();
while (iter.hasNext()) {
iter.next().onChanged();
}
}
public String[] getMapListingArray(int position) {
return mapsList.get(position);
}
@Override
public boolean areAllItemsEnabled() {
return true;
}
@Override
public boolean isEnabled(int position) {
return true;
}
@Override
public int getCount() {
return mapsList.size();
}
@Override
public Object getItem(int position) {
return mapsList.get(position)[0];
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public int getItemViewType(int position) {
return 0;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView =
activity.getLayoutInflater().inflate(R.layout.listitem, parent, false);
}
String[] map = mapsList.get(position);
((TextView) convertView.findViewById(R.id.maplistitem)).setText(map[1]);
((TextView) convertView.findViewById(R.id.maplistdesc)).setText(map[2]);
TextView publicUnlisted =
(TextView) convertView.findViewById(R.id.maplistpublic);
if (publicList.get(position)) {
publicUnlisted.setTextColor(Color.RED);
publicUnlisted.setText(R.string.public_map);
} else {
publicUnlisted.setTextColor(Color.GREEN);
publicUnlisted.setText(R.string.unlisted_map);
}
return convertView;
}
@Override
public int getViewTypeCount() {
return 1;
}
@Override
public boolean hasStableIds() {
return false;
}
@Override
public boolean isEmpty() {
return mapsList.isEmpty();
}
@Override
public void registerDataSetObserver(DataSetObserver observer) {
observerSet.add(observer);
}
@Override
public void unregisterDataSetObserver(DataSetObserver observer) {
observerSet.remove(observer);
}
}
@@ -0,0 +1,449 @@
/*
* 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.analytics.GoogleAnalyticsTracker;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.TrackDataHub;
import com.google.android.apps.mytracks.content.TracksColumns;
import com.google.android.apps.mytracks.content.WaypointCreationRequest;
import com.google.android.apps.mytracks.io.file.TempFileCleaner;
import com.google.android.apps.mytracks.services.ITrackRecordingService;
import com.google.android.apps.mytracks.services.ServiceUtils;
import com.google.android.apps.mytracks.services.TrackRecordingServiceConnection;
import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerFactory;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.apps.mytracks.util.SystemUtils;
import com.google.android.apps.mytracks.util.UriUtils;
import com.google.android.maps.mytracks.R;
import android.app.Activity;
import android.app.TabActivity;
import android.content.ContentUris;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.net.Uri;
import android.os.Bundle;
import android.os.RemoteException;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;
import android.view.ViewGroup.LayoutParams;
import android.view.Window;
import android.widget.RelativeLayout;
import android.widget.TabHost;
import android.widget.Toast;
/**
* The super activity that embeds our sub activities.
*
* @author Leif Hendrik Wilden
* @author Rodrigo Damazio
*/
@SuppressWarnings("deprecation")
public class MyTracks extends TabActivity implements OnTouchListener {
private TrackDataHub dataHub;
/**
* Menu manager.
*/
private MenuManager menuManager;
/**
* Preferences.
*/
private SharedPreferences preferences;
/**
* True if a new track should be created after the track recording service
* binds.
*/
private boolean startNewTrackRequested = false;
/**
* Utilities to deal with the database.
*/
private MyTracksProviderUtils providerUtils;
/**
* Google Analytics tracker
*/
private GoogleAnalyticsTracker tracker;
/*
* Tabs/View navigation:
*/
private NavControls navControls;
private final Runnable changeTab = new Runnable() {
public void run() {
getTabHost().setCurrentTab(navControls.getCurrentIcons());
}
};
/*
* Recording service interaction:
*/
private final Runnable serviceBindCallback = new Runnable() {
@Override
public void run() {
synchronized (serviceConnection) {
ITrackRecordingService service = serviceConnection.getServiceIfBound();
if (startNewTrackRequested && service != null) {
Log.i(TAG, "Starting recording");
startNewTrackRequested = false;
startRecordingNewTrack(service);
} else if (startNewTrackRequested) {
Log.w(TAG, "Not yet starting recording");
}
}
}
};
private TrackRecordingServiceConnection serviceConnection;
/*
* Application lifetime events:
* ============================
*/
@Override
protected void onCreate(Bundle savedInstanceState) {
Log.d(TAG, "MyTracks.onCreate");
super.onCreate(savedInstanceState);
ApiFeatures apiFeatures = ApiFeatures.getInstance();
if (!SystemUtils.isRelease(this)) {
apiFeatures.getApiAdapter().enableStrictMode();
}
tracker = GoogleAnalyticsTracker.getInstance();
// Start the tracker in manual dispatch mode...
tracker.start(getString(R.string.google_analytics_id), getApplicationContext());
tracker.setProductVersion("android-mytracks", SystemUtils.getMyTracksVersion(this));
tracker.trackPageView("/appstart");
tracker.dispatch();
providerUtils = MyTracksProviderUtils.Factory.get(this);
preferences = getSharedPreferences(Constants.SETTINGS_NAME, 0);
dataHub = ((MyTracksApplication) getApplication()).getTrackDataHub();
menuManager = new MenuManager(this);
serviceConnection = new TrackRecordingServiceConnection(this, serviceBindCallback);
// The volume we want to control is the Text-To-Speech volume
int volumeStream =
new StatusAnnouncerFactory(apiFeatures).getVolumeStream();
setVolumeControlStream(volumeStream);
// We don't need a window title bar:
requestWindowFeature(Window.FEATURE_NO_TITLE);
final Resources res = getResources();
final TabHost tabHost = getTabHost();
tabHost.addTab(tabHost.newTabSpec("tab1")
.setIndicator("Map", res.getDrawable(
android.R.drawable.ic_menu_mapmode))
.setContent(new Intent(this, MapActivity.class)));
tabHost.addTab(tabHost.newTabSpec("tab2")
.setIndicator("Stats", res.getDrawable(R.drawable.menu_stats))
.setContent(new Intent(this, StatsActivity.class)));
tabHost.addTab(tabHost.newTabSpec("tab3")
.setIndicator("Chart", res.getDrawable(R.drawable.menu_elevation))
.setContent(new Intent(this, ChartActivity.class)));
// Hide the tab widget itself. We'll use overlayed prev/next buttons to
// switch between the tabs:
tabHost.getTabWidget().setVisibility(View.GONE);
RelativeLayout layout = new RelativeLayout(this);
LayoutParams params =
new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT);
layout.setLayoutParams(params);
navControls =
new NavControls(this, layout,
getResources().obtainTypedArray(R.array.left_icons),
getResources().obtainTypedArray(R.array.right_icons),
changeTab);
navControls.show();
tabHost.addView(layout);
layout.setOnTouchListener(this);
// This will show the eula until the user accepts or quits the app.
Eula.showEulaRequireAcceptance(this);
}
@Override
protected void onStart() {
Log.d(TAG, "MyTracks.onStart");
super.onStart();
dataHub.start();
// Ensure that service is running and bound if we're supposed to be recording
if (ServiceUtils.isRecording(this, null, preferences)) {
serviceConnection.startAndBind();
}
Intent intent = getIntent();
String action = intent.getAction();
Uri data = intent.getData();
if ((Intent.ACTION_VIEW.equals(action) || Intent.ACTION_EDIT.equals(action))
&& TracksColumns.CONTENT_ITEMTYPE.equals(intent.getType())
&& UriUtils.matchesContentUri(data, TracksColumns.CONTENT_URI)) {
long trackId = ContentUris.parseId(data);
dataHub.loadTrack(trackId);
}
}
@Override
protected void onResume() {
// Called when the current activity is being displayed or re-displayed
// to the user.
Log.d(TAG, "MyTracks.onResume");
serviceConnection.bindIfRunning();
super.onResume();
}
@Override
protected void onPause() {
// Called when activity is going into the background, but has not (yet) been
// killed. Shouldn't block longer than approx. 2 seconds.
Log.d(TAG, "MyTracks.onPause");
super.onPause();
}
@Override
protected void onStop() {
Log.d(TAG, "MyTracks.onStop");
dataHub.stop();
tracker.dispatch();
tracker.stop();
// Clean up any temporary track files.
TempFileCleaner.clean();
super.onStop();
}
@Override
protected void onDestroy() {
Log.d(TAG, "MyTracks.onDestroy");
serviceConnection.unbind();
super.onDestroy();
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
return menuManager.onCreateOptionsMenu(menu);
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
menuManager.onPrepareOptionsMenu(menu, providerUtils.getLastTrack() != null,
ServiceUtils.isRecording(this, serviceConnection.getServiceIfBound(), preferences),
dataHub.isATrackSelected());
return super.onPrepareOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
return menuManager.onOptionsItemSelected(item)
? true
: super.onOptionsItemSelected(item);
}
/*
* Key events:
* ===========
*/
@Override
public boolean onTrackballEvent(MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
if (ServiceUtils.isRecording(this, serviceConnection.getServiceIfBound(), preferences)) {
try {
insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS);
} catch (RemoteException e) {
Log.e(TAG, "Cannot insert statistics marker.", e);
} catch (IllegalStateException e) {
Log.e(TAG, "Cannot insert statistics marker.", e);
}
return true;
}
}
return super.onTrackballEvent(event);
}
@Override
public void onActivityResult(int requestCode, int resultCode,
final Intent results) {
Log.d(TAG, "MyTracks.onActivityResult");
long trackId = dataHub.getSelectedTrackId();
if (results != null) {
trackId = results.getLongExtra("trackid", trackId);
}
switch (requestCode) {
case Constants.SHOW_TRACK: {
if (results != null) {
if (trackId >= 0) {
dataHub.loadTrack(trackId);
// The track list passed the requested action as result code. Hand
// it off to the onActivityResult for further processing:
if (resultCode != Constants.SHOW_TRACK) {
onActivityResult(resultCode, Activity.RESULT_OK, results);
}
}
}
break;
}
case Constants.SHOW_WAYPOINT: {
if (results != null) {
final long waypointId = results.getLongExtra("waypointid", -1);
if (waypointId >= 0) {
MapActivity map =
(MapActivity) getLocalActivityManager().getActivity("tab1");
if (map != null) {
getTabHost().setCurrentTab(0);
map.showWaypoint(waypointId);
}
}
}
break;
}
case Constants.WELCOME: {
CheckUnits.check(this);
break;
}
default: {
Log.w(TAG, "Warning unhandled request code: " + requestCode);
}
}
}
@Override
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
navControls.show();
}
return false;
}
/**
* Inserts a waypoint marker.
*
* TODO: Merge with WaypointsList#insertWaypoint.
*
* @return Id of the inserted statistics marker.
* @throws RemoteException If the call on the service failed.
*/
private long insertWaypoint(WaypointCreationRequest request) throws RemoteException {
ITrackRecordingService trackRecordingService = serviceConnection.getServiceIfBound();
if (trackRecordingService == null) {
throw new IllegalStateException("The recording service is not bound.");
}
try {
long waypointId = trackRecordingService.insertWaypoint(request);
if (waypointId >= 0) {
Toast.makeText(this, R.string.status_marker_inserted,
Toast.LENGTH_LONG).show();
}
return waypointId;
} catch (RemoteException e) {
Toast.makeText(this, R.string.error_unable_to_insert_marker,
Toast.LENGTH_LONG).show();
throw e;
}
}
private void startRecordingNewTrack(
ITrackRecordingService trackRecordingService) {
try {
long recordingTrackId = trackRecordingService.startNewTrack();
// Select the recording track.
dataHub.loadTrack(recordingTrackId);
Toast.makeText(this, getString(R.string.status_now_recording),
Toast.LENGTH_SHORT).show();
// TODO: We catch Exception, because after eliminating the service process
// all exceptions it may throw are no longer wrapped in a RemoteException.
} catch (Exception e) {
Toast.makeText(this,
getString(R.string.error_unable_to_start_recording),
Toast.LENGTH_SHORT).show();
Log.w(TAG, "Unable to start recording.", e);
}
}
/**
* Starts the track recording service (if not already running) and binds to
* it. Starts recording a new track.
*/
void startRecording() {
synchronized (serviceConnection) {
startNewTrackRequested = true;
serviceConnection.startAndBind();
// Binding was already requested before, it either already happened
// (in which case running the callback manually triggers the actual recording start)
// or it will happen in the future
// (in which case running the callback now will have no effect).
serviceBindCallback.run();
}
}
/**
* Stops the track recording service and unbinds from it. Will display a toast
* "Stopped recording" and pop up the Track Details activity.
*/
void stopRecording() {
// Save the track id as the shared preference will overwrite the recording track id.
SharedPreferences sharedPreferences = getSharedPreferences(Constants.SETTINGS_NAME, 0);
long currentTrackId = sharedPreferences.getLong(getString(R.string.recording_track_key), -1);
ITrackRecordingService trackRecordingService = serviceConnection.getServiceIfBound();
if (trackRecordingService != null) {
try {
trackRecordingService.endCurrentTrack();
} catch (Exception e) {
Log.e(TAG, "Unable to stop recording.", e);
}
}
serviceConnection.stop();
if (currentTrackId > 0) {
Intent intent = new Intent(MyTracks.this, TrackDetails.class);
intent.putExtra("trackid", currentTrackId);
intent.putExtra("hasCancelButton", false);
startActivity(intent);
}
}
long getSelectedTrackId() {
return dataHub.getSelectedTrackId();
}
}
@@ -0,0 +1,42 @@
/*
* Copyright 2008 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks;
import com.google.android.apps.mytracks.content.TrackDataHub;
import android.app.Application;
/**
* MyTracksApplication for keeping global state.
*
* @author jshih@google.com (Jimmy Shih)
*/
public class MyTracksApplication extends Application {
private TrackDataHub trackDataHub;
/**
* Gets the application's TrackDataHub.
*
* Note: use synchronized to make sure only one instance is created per application.
*/
public synchronized TrackDataHub getTrackDataHub() {
if (trackDataHub == null) {
trackDataHub = TrackDataHub.newInstance(getApplicationContext());
}
return trackDataHub;
}
}
@@ -0,0 +1,214 @@
/*
* 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 android.content.Context;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.RelativeLayout.LayoutParams;
/**
* Creates previous and next arrows for a given activity.
*
* @author Leif Hendrik Wilden
*/
public class NavControls {
private static final int KEEP_VISIBLE_MILLIS = 4000;
private static final boolean FADE_CONTROLS = true;
private static final Animation SHOW_NEXT_ANIMATION =
new AlphaAnimation(0F, 1F);
private static final Animation HIDE_NEXT_ANIMATION =
new AlphaAnimation(1F, 0F);
private static final Animation SHOW_PREV_ANIMATION =
new AlphaAnimation(0F, 1F);
private static final Animation HIDE_PREV_ANIMATION =
new AlphaAnimation(1F, 0F);
/**
* A touchable image view.
* When touched it changes the navigation control icons accordingly.
*/
private class TouchLayout extends RelativeLayout implements Runnable {
private final boolean isLeft;
private final ImageView icon;
public TouchLayout(Context context, boolean isLeft) {
super(context);
this.isLeft = isLeft;
this.icon = new ImageView(context);
icon.setVisibility(View.GONE);
addView(icon);
}
public void setIcon(Drawable drawable) {
icon.setImageDrawable(drawable);
icon.setVisibility(View.VISIBLE);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
setPressed(true);
shiftIcons(isLeft);
// Call the user back
handler.post(this);
break;
case MotionEvent.ACTION_UP:
setPressed(false);
break;
}
return super.onTouchEvent(event);
}
@Override
public void run() {
touchRunnable.run();
setPressed(false);
}
}
private final Handler handler = new Handler();
private final Runnable dismissControls = new Runnable() {
public void run() {
hide();
}
};
private final TouchLayout prevImage;
private final TouchLayout nextImage;
private final TypedArray leftIcons;
private final TypedArray rightIcons;
private final Runnable touchRunnable;
private boolean isVisible = false;
private int currentIcons;
public NavControls(Context context, ViewGroup container,
TypedArray leftIcons, TypedArray rightIcons,
Runnable touchRunnable) {
this.leftIcons = leftIcons;
this.rightIcons = rightIcons;
this.touchRunnable = touchRunnable;
if (leftIcons.length() != rightIcons.length() || leftIcons.length() < 1) {
throw new IllegalArgumentException("Invalid icons specified");
}
if (touchRunnable == null) {
throw new NullPointerException("Runnable cannot be null");
}
LayoutParams prevParams = new LayoutParams(
LayoutParams.WRAP_CONTENT,
LayoutParams.WRAP_CONTENT);
LayoutParams nextParams = new LayoutParams(
LayoutParams.WRAP_CONTENT,
LayoutParams.WRAP_CONTENT);
prevParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT);
nextParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT);
prevParams.addRule(RelativeLayout.CENTER_VERTICAL);
nextParams.addRule(RelativeLayout.CENTER_VERTICAL);
nextImage = new TouchLayout(context, false);
prevImage = new TouchLayout(context, true);
nextImage.setLayoutParams(nextParams);
prevImage.setLayoutParams(prevParams);
nextImage.setVisibility(View.INVISIBLE);
prevImage.setVisibility(View.INVISIBLE);
container.addView(prevImage);
container.addView(nextImage);
prevImage.setIcon(leftIcons.getDrawable(0));
nextImage.setIcon(rightIcons.getDrawable(0));
this.currentIcons = 0;
}
private void keepVisible() {
if (isVisible && FADE_CONTROLS) {
handler.removeCallbacks(dismissControls);
handler.postDelayed(dismissControls, KEEP_VISIBLE_MILLIS);
}
}
public void show() {
if (!isVisible) {
SHOW_PREV_ANIMATION.setDuration(500);
SHOW_PREV_ANIMATION.startNow();
prevImage.setPressed(false);
prevImage.setAnimation(SHOW_PREV_ANIMATION);
prevImage.setVisibility(View.VISIBLE);
SHOW_NEXT_ANIMATION.setDuration(500);
SHOW_NEXT_ANIMATION.startNow();
nextImage.setPressed(false);
nextImage.setAnimation(SHOW_NEXT_ANIMATION);
nextImage.setVisibility(View.VISIBLE);
isVisible = true;
keepVisible();
} else {
keepVisible();
}
}
public void hideNow() {
handler.removeCallbacks(dismissControls);
isVisible = false;
prevImage.clearAnimation();
prevImage.setVisibility(View.INVISIBLE);
nextImage.clearAnimation();
nextImage.setVisibility(View.INVISIBLE);
}
public void hide() {
isVisible = false;
prevImage.setAnimation(HIDE_PREV_ANIMATION);
HIDE_PREV_ANIMATION.setDuration(500);
HIDE_PREV_ANIMATION.startNow();
prevImage.setVisibility(View.INVISIBLE);
nextImage.setAnimation(HIDE_NEXT_ANIMATION);
HIDE_NEXT_ANIMATION.setDuration(500);
HIDE_NEXT_ANIMATION.startNow();
nextImage.setVisibility(View.INVISIBLE);
}
public int getCurrentIcons() {
return currentIcons;
}
private void shiftIcons(boolean isLeft) {
// Increment or decrement by one, with wrap around
currentIcons = (currentIcons + leftIcons.length() +
(isLeft ? -1 : 1)) % leftIcons.length();
prevImage.setIcon(leftIcons.getDrawable(currentIcons));
nextImage.setIcon(rightIcons.getDrawable(currentIcons));
}
}
@@ -0,0 +1,31 @@
/*
* 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;
/**
* Interface that allows to set a progress dialog message and progress value
* in percent.
*
* @author Leif Hendrik Wilden
*/
public interface ProgressIndicator {
public void setProgressMessage(int resId);
public void clearProgressMessage();
public void setProgressValue(int percent);
}
@@ -0,0 +1,319 @@
/*
* 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 com.google.android.apps.mytracks.Constants.TAG;
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.maps.mytracks.R;
import com.google.protobuf.InvalidProtocolBufferException;
import android.app.Activity;
import android.os.Bundle;
import android.os.RemoteException;
import android.util.Log;
import android.widget.TextView;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;
/**
* An activity that displays information about sensors.
*
* @author Sandor Dornbush
*/
public class SensorStateActivity extends Activity {
private static final SimpleDateFormat TIMESTAMP_FORMAT =
new SimpleDateFormat("HH:mm:ss");
private static final long REFRESH_PERIOD_MS = 250;
private final StatsUtilities utils;
/**
* This timer periodically invokes the refresh timer task.
*/
private Timer timer;
private final Runnable stateUpdater = new Runnable() {
public void run() {
if (isVisible) // only update when UI is visible
updateState();
}
};
/**
* Connection to the recording service.
*/
private TrackRecordingServiceConnection serviceConnection;
/**
* A task which will update the U/I.
*/
private class RefreshTask extends TimerTask {
@Override
public void run() {
runOnUiThread(stateUpdater);
}
};
/**
* A temporary sensor manager, when none is available.
*/
SensorManager tempSensorManager = null;
/**
* A state flag set to true when the activity is active/visible,
* i.e. after resume, and before pause
*
* Used to avoid updating after the pause event, because sometimes an update
* event occurs even after the timer is cancelled. In this case,
* it could cause the {@link #tempSensorManager} to be recreated, after it
* is destroyed at the pause event.
*/
boolean isVisible = false;
public SensorStateActivity() {
utils = new StatsUtilities(this);
Log.w(TAG, "SensorStateActivity()");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.w(TAG, "SensorStateActivity.onCreate");
setContentView(R.layout.sensor_state);
serviceConnection = new TrackRecordingServiceConnection(this, stateUpdater);
serviceConnection.bindIfRunning();
updateState();
}
@Override
protected void onResume() {
super.onResume();
isVisible = true;
serviceConnection.bindIfRunning();
timer = new Timer();
timer.schedule(new RefreshTask(), REFRESH_PERIOD_MS, REFRESH_PERIOD_MS);
}
@Override
protected void onPause() {
isVisible = false;
timer.cancel();
timer.purge();
timer = null;
stopTempSensorManager();
super.onPause();
}
@Override
protected void onDestroy() {
serviceConnection.unbind();
super.onDestroy();
}
protected void updateState() {
Log.d(TAG, "Updating SensorStateActivity");
ITrackRecordingService service = serviceConnection.getServiceIfBound();
// Check if service is available, and recording.
boolean isRecording = false;
if (service != null) {
try {
isRecording = service.isRecording();
} catch (RemoteException e) {
Log.e(TAG, "Unable to determine if service is recording.", e);
}
}
// If either service isn't available, or not recording.
if (!isRecording) {
updateFromTempSensorManager();
} else {
updateFromSysSensorManager();
}
}
private void updateFromTempSensorManager()
{
// Use variables to hold the sensor state and data set.
Sensor.SensorState currentState = null;
Sensor.SensorDataSet currentDataSet = null;
// If no temp sensor manager is present, create one, and start it.
if (tempSensorManager == null) {
tempSensorManager = SensorManagerFactory.getSensorManager(this);
if (tempSensorManager != null)
tempSensorManager.onStartTrack();
}
// If a temp sensor manager is available, use states from temp sensor
// manager.
if (tempSensorManager != null) {
currentState = tempSensorManager.getSensorState();
currentDataSet = tempSensorManager.getSensorDataSet();
}
// Update the sensor state, and sensor data, using the variables.
updateSensorStateAndData(currentState, currentDataSet);
}
private void updateFromSysSensorManager()
{
// Use variables to hold the sensor state and data set.
Sensor.SensorState currentState = null;
Sensor.SensorDataSet currentDataSet = null;
ITrackRecordingService service = serviceConnection.getServiceIfBound();
// If a temp sensor manager is present, shut it down,
// probably recording just started.
stopTempSensorManager();
// Get sensor details from the service.
if (service == null) {
Log.d(TAG, "Could not get track recording service.");
} else {
try {
byte[] buff = service.getSensorData();
if (buff != null) {
currentDataSet = Sensor.SensorDataSet.parseFrom(buff);
}
} catch (RemoteException e) {
Log.e(TAG, "Could not read sensor data.", e);
} catch (InvalidProtocolBufferException e) {
Log.e(TAG, "Could not read sensor data.", e);
}
try {
currentState = Sensor.SensorState.valueOf(service.getSensorState());
} catch (RemoteException e) {
Log.e(TAG, "Could not read sensor state.", e);
currentState = Sensor.SensorState.NONE;
}
}
// Update the sensor state, and sensor data, using the variables.
updateSensorStateAndData(currentState, currentDataSet);
}
/**
* Stops the temporary sensor manager, if one exists.
*/
private void stopTempSensorManager()
{
if (tempSensorManager != null) {
tempSensorManager.shutdown();
tempSensorManager = null;
}
}
private void updateSensorStateAndData(Sensor.SensorState state, Sensor.SensorDataSet dataSet)
{
updateSensorState(state == null ? Sensor.SensorState.NONE : state);
updateSensorData(dataSet);
}
private void updateSensorState(Sensor.SensorState state) {
TextView sensorTime =
((TextView) findViewById(R.id.sensor_state_register));
sensorTime.setText(SensorUtils.getStateAsString(state, this));
}
protected void updateSensorData(Sensor.SensorDataSet sds) {
if (sds == null) {
utils.setUnknown(R.id.sensor_data_time_register);
utils.setUnknown(R.id.cadence_state_register);
utils.setUnknown(R.id.power_state_register);
utils.setUnknown(R.id.heart_rate_register);
utils.setUnknown(R.id.battery_level_register);
return;
}
TextView sensorTime =
((TextView) findViewById(R.id.sensor_data_time_register));
sensorTime.setText(
TIMESTAMP_FORMAT.format(new Date(sds.getCreationTime())));
if (sds.hasPower() && sds.getPower().hasValue()
&& sds.getPower().getState() == Sensor.SensorState.SENDING) {
utils.setText(R.id.power_state_register,
Integer.toString(sds.getPower().getValue()));
} else {
utils.setText(R.id.power_state_register,
SensorUtils.getStateAsString(
sds.hasPower()
? sds.getPower().getState()
: Sensor.SensorState.NONE,
this));
}
if (sds.hasCadence() && sds.getCadence().hasValue()
&& sds.getCadence().getState() == Sensor.SensorState.SENDING) {
utils.setText(R.id.cadence_state_register,
Integer.toString(sds.getCadence().getValue()));
} else {
utils.setText(R.id.cadence_state_register,
SensorUtils.getStateAsString(
sds.hasCadence()
? sds.getCadence().getState()
: Sensor.SensorState.NONE,
this));
}
if (sds.hasHeartRate() && sds.getHeartRate().hasValue()
&& sds.getHeartRate().getState() == Sensor.SensorState.SENDING) {
utils.setText(R.id.heart_rate_register,
Integer.toString(sds.getHeartRate().getValue()));
} else {
utils.setText(R.id.heart_rate_register,
SensorUtils.getStateAsString(
sds.hasHeartRate()
? sds.getHeartRate().getState()
: Sensor.SensorState.NONE,
this));
}
if (sds.hasBatteryLevel() && sds.getBatteryLevel().hasValue()
&& sds.getBatteryLevel().getState() == Sensor.SensorState.SENDING) {
utils.setText(R.id.battery_level_register,
Integer.toString(sds.getBatteryLevel().getValue()));
} else {
utils.setText(R.id.battery_level_register,
SensorUtils.getStateAsString(
sds.hasBatteryLevel()
? sds.getBatteryLevel().getState()
: Sensor.SensorState.NONE,
this));
}
}
}
@@ -0,0 +1,565 @@
/*
* 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.io.backup.BackupActivityHelper;
import com.google.android.apps.mytracks.io.backup.BackupPreferencesListener;
import com.google.android.apps.mytracks.services.sensors.ant.AntUtils;
import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerFactory;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.apps.mytracks.util.BluetoothDeviceUtils;
import com.google.android.apps.mytracks.util.UnitConversions;
import com.google.android.maps.mytracks.R;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.preference.CheckBoxPreference;
import android.preference.EditTextPreference;
import android.preference.ListPreference;
import android.preference.Preference;
import android.preference.Preference.OnPreferenceChangeListener;
import android.preference.Preference.OnPreferenceClickListener;
import android.preference.PreferenceActivity;
import android.preference.PreferenceCategory;
import android.preference.PreferenceManager;
import android.preference.PreferenceScreen;
import android.provider.Settings;
import android.util.Log;
import android.widget.Toast;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* An activity that let's the user see and edit the settings.
*
* This activity has two entry points, "root" and "display" preference screen.
* If bundle.getString("open_settings_screen") is set to "display_settings_screen_key", then
* the "display" preference screen is shown, otherwise, the "root" display preference is shown.
*
* @author Leif Hendrik Wilden
* @author Rodrigo Damazio
*/
public class SettingsActivity extends PreferenceActivity {
private BackupPreferencesListener backupListener;
private SharedPreferences preferences;
/** Called when the activity is first created. */
@Override
protected void onCreate(Bundle icicle) {
super.onCreate(icicle);
initActivityCommons();
// If we only need the display setting screen nothing else needs to load.
if (processIntent())
return;
initActivitySpecifics();
}
private void initActivityCommons() {
// The volume we want to control is the Text-To-Speech volume
ApiFeatures apiFeatures = ApiFeatures.getInstance();
int volumeStream =
new StatusAnnouncerFactory(apiFeatures).getVolumeStream();
setVolumeControlStream(volumeStream);
// Tell it where to read/write preferences
PreferenceManager preferenceManager = getPreferenceManager();
preferenceManager.setSharedPreferencesName(Constants.SETTINGS_NAME);
preferenceManager.setSharedPreferencesMode(0);
// Set up automatic preferences backup
backupListener = apiFeatures.getApiAdapter().getBackupPreferencesListener(this);
preferences = preferenceManager.getSharedPreferences();
preferences.registerOnSharedPreferenceChangeListener(backupListener);
// Load the preferences to be displayed
addPreferencesFromResource(R.xml.preferences);
// Disable TTS announcement preference if not available
if (!apiFeatures.hasTextToSpeech()) {
IntegerListPreference announcementFrequency =
(IntegerListPreference) findPreference(
getString(R.string.announcement_frequency_key));
announcementFrequency.setEnabled(false);
announcementFrequency.setValue("-1");
announcementFrequency.setSummary(
R.string.settings_not_available_summary);
}
}
private void initActivitySpecifics() {
// Hook up switching of displayed list entries between metric and imperial
// units
CheckBoxPreference metricUnitsPreference =
(CheckBoxPreference) findPreference(
getString(R.string.metric_units_key));
metricUnitsPreference.setOnPreferenceChangeListener(
new OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference,
Object newValue) {
boolean isMetric = (Boolean) newValue;
updatePreferenceUnits(isMetric);
return true;
}
});
updatePreferenceUnits(metricUnitsPreference.isChecked());
customizeSensorOptionsPreferences();
customizeTrackColorModePreferences();
// Hook up action for resetting all settings
Preference resetPreference = findPreference(getString(R.string.reset_key));
resetPreference.setOnPreferenceClickListener(new OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference arg0) {
onResetPreferencesClick();
return true;
}
});
// Add a confirmation dialog for the "Allow access" preference.
final CheckBoxPreference allowAccessPreference = (CheckBoxPreference) findPreference(
getString(R.string.allow_access_key));
allowAccessPreference.setOnPreferenceChangeListener(new OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
if ((Boolean) newValue) {
AlertDialog dialog = new AlertDialog.Builder(SettingsActivity.this)
.setCancelable(true)
.setTitle(getString(R.string.settings_allow_access))
.setMessage(getString(R.string.settings_allow_access_dialog_message))
.setPositiveButton(android.R.string.ok, new OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int button) {
allowAccessPreference.setChecked(true);
}
})
.setNegativeButton(android.R.string.cancel, null)
.create();
dialog.show();
return false;
} else {
return true;
}
}
});
}
private boolean processIntent() {
boolean showDisplaySettings = false;
Bundle bundle = getIntent().getExtras();
PreferenceScreen preferenceScreen;
String intentString = getString(R.string.open_settings_screen);
if (bundle != null) {
preferenceScreen = (PreferenceScreen) findPreference(bundle.getString(intentString));
if (preferenceScreen != null) {
showDisplaySettings = true;
setPreferenceScreen(preferenceScreen);
}
}
return showDisplaySettings;
}
private void customizeSensorOptionsPreferences() {
ListPreference sensorTypePreference =
(ListPreference) findPreference(getString(R.string.sensor_type_key));
sensorTypePreference.setOnPreferenceChangeListener(
new OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference,
Object newValue) {
updateSensorSettings((String) newValue);
return true;
}
});
updateSensorSettings(sensorTypePreference.getValue());
if (!AntUtils.hasAntSupport(this)) {
// The sensor options screen has a few ANT-specific options which we
// need to remove. First, we need to remove the ANT sensor types.
// Second, we need to remove the ANT unpairing options.
Set<Integer> toRemove = new HashSet<Integer>();
String[] antValues = getResources().getStringArray(R.array.ant_sensor_type_values);
for (String antValue : antValues) {
toRemove.add(sensorTypePreference.findIndexOfValue(antValue));
}
CharSequence[] entries = sensorTypePreference.getEntries();
CharSequence[] entryValues = sensorTypePreference.getEntryValues();
CharSequence[] filteredEntries = new CharSequence[entries.length - toRemove.size()];
CharSequence[] filteredEntryValues = new CharSequence[filteredEntries.length];
for (int i = 0, last = 0; i < entries.length; i++) {
if (!toRemove.contains(i)) {
filteredEntries[last] = entries[i];
filteredEntryValues[last++] = entryValues[i];
}
}
sensorTypePreference.setEntries(filteredEntries);
sensorTypePreference.setEntryValues(filteredEntryValues);
PreferenceScreen sensorOptionsScreen =
(PreferenceScreen) findPreference(getString(R.string.sensor_options_key));
sensorOptionsScreen.removePreference(findPreference(getString(R.string.ant_options_key)));
}
}
private void customizeTrackColorModePreferences() {
ListPreference trackColorModePreference =
(ListPreference) findPreference(getString(R.string.track_color_mode_key));
trackColorModePreference.setOnPreferenceChangeListener(
new OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference,
Object newValue) {
updateTrackColorModeSettings((String) newValue);
return true;
}
});
updateTrackColorModeSettings(trackColorModePreference.getValue());
setTrackColorModePreferenceListeners();
PreferenceCategory speedOptionsCategory =
(PreferenceCategory) findPreference(getString(R.string.track_color_mode_fixed_speed_options_key));
speedOptionsCategory.removePreference(findPreference(getString(R.string.track_color_mode_fixed_speed_slow_key)));
speedOptionsCategory.removePreference(findPreference(getString(R.string.track_color_mode_fixed_speed_medium_key)));
}
@Override
protected void onResume() {
super.onResume();
// If we only need the display setting screen nothing else needs to load.
if (processIntent())
return;
configureBluetoothPreferences();
Preference backupNowPreference =
findPreference(getString(R.string.backup_to_sd_key));
Preference restoreNowPreference =
findPreference(getString(R.string.restore_from_sd_key));
Preference resetPreference = findPreference(getString(R.string.reset_key));
// If recording, disable backup/restore/reset
// (we don't want to get to inconsistent states)
boolean recording =
preferences.getLong(getString(R.string.recording_track_key), -1) != -1;
backupNowPreference.setEnabled(!recording);
restoreNowPreference.setEnabled(!recording);
resetPreference.setEnabled(!recording);
backupNowPreference.setSummary(
recording ? R.string.settings_not_while_recording
: R.string.settings_backup_to_sd_summary);
restoreNowPreference.setSummary(
recording ? R.string.settings_not_while_recording
: R.string.settings_restore_from_sd_summary);
resetPreference.setSummary(
recording ? R.string.settings_not_while_recording
: R.string.settings_reset_summary);
// Add actions to the backup preferences
backupNowPreference.setOnPreferenceClickListener(
new OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
BackupActivityHelper backupHelper =
new BackupActivityHelper(SettingsActivity.this);
backupHelper.writeBackup();
return true;
}
});
restoreNowPreference.setOnPreferenceClickListener(
new OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
BackupActivityHelper backupHelper =
new BackupActivityHelper(SettingsActivity.this);
backupHelper.restoreBackup();
return true;
}
});
}
@Override
protected void onDestroy() {
getPreferenceManager().getSharedPreferences()
.unregisterOnSharedPreferenceChangeListener(backupListener);
super.onPause();
}
private void updateSensorSettings(String sensorType) {
boolean usesBluetooth =
getString(R.string.sensor_type_value_zephyr).equals(sensorType)
|| getString(R.string.sensor_type_value_polar).equals(sensorType);
findPreference(
getString(R.string.bluetooth_sensor_key)).setEnabled(usesBluetooth);
findPreference(
getString(R.string.bluetooth_pairing_key)).setEnabled(usesBluetooth);
// Update the ANT+ sensors.
// TODO: Only enable on phones that have ANT+.
Preference antHrm = findPreference(getString(R.string.ant_heart_rate_sensor_id_key));
Preference antSrm = findPreference(getString(R.string.ant_srm_bridge_sensor_id_key));
if (antHrm != null && antSrm != null) {
antHrm
.setEnabled(getString(R.string.sensor_type_value_ant).equals(sensorType));
antSrm
.setEnabled(getString(R.string.sensor_type_value_srm_ant_bridge).equals(sensorType));
}
}
private void updateTrackColorModeSettings(String trackColorMode) {
boolean usesFixedSpeed = trackColorMode.equals(getString(R.string.track_color_mode_value_fixed));
boolean usesDynamicSpeed = trackColorMode.equals(getString(R.string.track_color_mode_value_dynamic));
findPreference(
getString(R.string.track_color_mode_fixed_speed_slow_display_key)).setEnabled(usesFixedSpeed);
findPreference(
getString(R.string.track_color_mode_fixed_speed_medium_display_key)).setEnabled(usesFixedSpeed);
findPreference(
getString(R.string.track_color_mode_dynamic_speed_variation_key)).setEnabled(usesDynamicSpeed);
}
/**
* Updates all the preferences which give options with distance units to use
* the proper unit the user has selected.
*
* @param isMetric true to use metric units, false to use imperial
*/
private void updatePreferenceUnits(boolean isMetric) {
final ListPreference minRecordingDistance =
(ListPreference) findPreference(
getString(R.string.min_recording_distance_key));
final ListPreference maxRecordingDistance =
(ListPreference) findPreference(
getString(R.string.max_recording_distance_key));
final ListPreference minRequiredAccuracy =
(ListPreference) findPreference(
getString(R.string.min_required_accuracy_key));
minRecordingDistance.setEntries(isMetric
? R.array.min_recording_distance_options
: R.array.min_recording_distance_options_ft);
maxRecordingDistance.setEntries(isMetric
? R.array.max_recording_distance_options
: R.array.max_recording_distance_options_ft);
minRequiredAccuracy.setEntries(isMetric
? R.array.min_required_accuracy_options
: R.array.min_required_accuracy_options_ft);
setTaskOptions(isMetric, R.string.announcement_frequency_key);
setTaskOptions(isMetric, R.string.split_frequency_key);
}
/**
* Set the user visible options for a periodic task.
*/
private void setTaskOptions(boolean isMetric, int listId) {
final ListPreference taskFrequency =
(ListPreference) findPreference(getString(listId));
taskFrequency.setEntries(isMetric
? R.array.task_frequency_options
: R.array.task_frequency_options_ft);
}
/**
* Configures preference actions related to bluetooth.
*/
private void configureBluetoothPreferences() {
if (BluetoothDeviceUtils.isBluetoothMethodSupported()) {
// Populate the list of bluetooth devices
populateBluetoothDeviceList();
// Make the pair devices preference go to the system preferences
findPreference(getString(R.string.bluetooth_pairing_key))
.setOnPreferenceClickListener(new OnPreferenceClickListener() {
public boolean onPreferenceClick(Preference preference) {
Intent settingsIntent = new Intent(Settings.ACTION_BLUETOOTH_SETTINGS);
startActivity(settingsIntent);
return false;
}
});
}
}
/**
* Populates the list preference with all available bluetooth devices.
*/
private void populateBluetoothDeviceList() {
// Build the list of entries and their values
List<String> entries = new ArrayList<String>();
List<String> entryValues = new ArrayList<String>();
// The actual devices
BluetoothDeviceUtils.getInstance().populateDeviceLists(entries, entryValues);
CharSequence[] entriesArray = entries.toArray(new CharSequence[entries.size()]);
CharSequence[] entryValuesArray = entryValues.toArray(new CharSequence[entryValues.size()]);
ListPreference devicesPreference =
(ListPreference) findPreference(getString(R.string.bluetooth_sensor_key));
devicesPreference.setEntryValues(entryValuesArray);
devicesPreference.setEntries(entriesArray);
}
/** Callback for when user asks to reset all settings. */
private void onResetPreferencesClick() {
AlertDialog dialog = new AlertDialog.Builder(this)
.setCancelable(true)
.setTitle(R.string.settings_reset)
.setMessage(R.string.settings_reset_dialog_message)
.setPositiveButton(android.R.string.ok,
new OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int button) {
onResetPreferencesConfirmed();
}
})
.setNegativeButton(android.R.string.cancel, null)
.create();
dialog.show();
}
/** Callback for when user confirms resetting all settings. */
private void onResetPreferencesConfirmed() {
// Change preferences in a separate thread.
new Thread() {
@Override
public void run() {
Log.i(TAG, "Resetting all settings");
// Actually wipe preferences (and save synchronously).
preferences.edit().clear().commit();
// Give UI feedback in the UI thread.
runOnUiThread(new Runnable() {
@Override
public void run() {
// Give feedback to the user.
Toast.makeText(
SettingsActivity.this,
R.string.settings_reset_done,
Toast.LENGTH_SHORT).show();
// Restart the settings activity so all changes are loaded.
Intent intent = getIntent();
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
}
});
}
}.start();
}
/**
* Set the given edit text preference text.
* If the units are not metric convert the value before displaying.
*/
private void viewTrackColorModeSettings(EditTextPreference preference, int id) {
CheckBoxPreference metricUnitsPreference = (CheckBoxPreference) findPreference(
getString(R.string.metric_units_key));
if(metricUnitsPreference.isChecked()) {
return;
}
// Convert miles/h to km/h
SharedPreferences prefs = getPreferenceManager().getSharedPreferences();
String metricspeed = prefs.getString(getString(id), null);
int englishspeed;
try {
englishspeed = (int) (Double.parseDouble(metricspeed) * UnitConversions.KMH_TO_MPH);
} catch (NumberFormatException e) {
englishspeed = 0;
}
preference.getEditText().setText(String.valueOf(englishspeed));
}
/**
* Saves the given edit text preference value.
* If the units are not metric convert the value before saving.
*/
private void validateTrackColorModeSettings(EditTextPreference preference,
String newValue, int id) {
CheckBoxPreference metricUnitsPreference = (CheckBoxPreference) findPreference(
getString(R.string.metric_units_key));
String metricspeed;
if(!metricUnitsPreference.isChecked()) {
// Convert miles/h to km/h
try {
metricspeed = String.valueOf((int) (Double.parseDouble(newValue) * UnitConversions.MPH_TO_KMH) + 1);
} catch (NumberFormatException e) {
metricspeed = "0";
}
} else {
metricspeed = newValue;
}
SharedPreferences prefs = getPreferenceManager().getSharedPreferences();
prefs.edit().putString(getString(id), metricspeed).commit();
}
/**
* Sets the TrackColorMode preference listeners.
*/
private void setTrackColorModePreferenceListeners() {
setTrackColorModePreferenceListener(R.string.track_color_mode_fixed_speed_slow_display_key,
R.string.track_color_mode_fixed_speed_slow_key);
setTrackColorModePreferenceListener(R.string.track_color_mode_fixed_speed_medium_display_key,
R.string.track_color_mode_fixed_speed_medium_key);
}
/**
* Sets a TrackColorMode preference listener.
*/
private void setTrackColorModePreferenceListener(int displayKey, final int metricKey) {
EditTextPreference trackColorModePreference =
(EditTextPreference) findPreference(getString(displayKey));
trackColorModePreference.setOnPreferenceChangeListener(
new OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference,
Object newValue) {
validateTrackColorModeSettings((EditTextPreference) preference, (String) newValue, metricKey);
return true;
}
});
trackColorModePreference.setOnPreferenceClickListener(
new OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
viewTrackColorModeSettings((EditTextPreference) preference, metricKey);
return true;
}
});
}
}
@@ -0,0 +1,326 @@
/*
* 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.Track;
import com.google.android.apps.mytracks.content.TrackDataHub;
import com.google.android.apps.mytracks.content.TrackDataHub.ListenerDataType;
import com.google.android.apps.mytracks.content.TrackDataListener;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.services.ServiceUtils;
import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerFactory;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.maps.mytracks.R;
import android.app.Activity;
import android.content.SharedPreferences;
import android.location.Location;
import android.os.Bundle;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.Window;
import android.widget.ScrollView;
import android.widget.TextView;
import java.util.EnumSet;
/**
* An activity that displays track statistics to the user.
*
* @author Sandor Dornbush
* @author Rodrigo Damazio
*/
public class StatsActivity extends Activity implements TrackDataListener {
/**
* A runnable for posting to the UI thread. Will update the total time field.
*/
private final Runnable updateResults = new Runnable() {
public void run() {
if (dataHub != null && dataHub.isRecordingSelected()) {
utils.setTime(R.id.total_time_register,
System.currentTimeMillis() - startTime);
}
}
};
private StatsUtilities utils;
private UIUpdateThread thread;
/**
* The start time of the selected track.
*/
private long startTime = -1;
private TrackDataHub dataHub;
private SharedPreferences preferences;
/**
* A thread that updates the total time field every second.
*/
private class UIUpdateThread extends Thread {
public UIUpdateThread() {
super();
Log.i(TAG, "Created UI update thread");
}
@Override
public void run() {
Log.i(TAG, "Started UI update thread");
while (ServiceUtils.isRecording(StatsActivity.this, null, preferences)) {
runOnUiThread(updateResults);
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
Log.w(TAG, "StatsActivity: Caught exception on sleep.", e);
break;
}
}
Log.w(TAG, "UIUpdateThread finished.");
}
}
/** Called when the activity is first created. */
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
preferences = getSharedPreferences(Constants.SETTINGS_NAME, 0);
utils = new StatsUtilities(this);
// The volume we want to control is the Text-To-Speech volume
int volumeStream =
new StatusAnnouncerFactory(ApiFeatures.getInstance()).getVolumeStream();
setVolumeControlStream(volumeStream);
// We don't need a window title bar:
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.stats);
ScrollView sv = ((ScrollView) findViewById(R.id.scrolly));
sv.setScrollBarStyle(ScrollView.SCROLLBARS_OUTSIDE_INSET);
showUnknownLocation();
DisplayMetrics metrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(metrics);
if (metrics.heightPixels > 600) {
((TextView) findViewById(R.id.speed_register)).setTextSize(80.0f);
}
}
@Override
protected void onResume() {
super.onResume();
dataHub = ((MyTracksApplication) getApplication()).getTrackDataHub();
dataHub.registerTrackDataListener(this, EnumSet.of(
ListenerDataType.SELECTED_TRACK_CHANGED,
ListenerDataType.TRACK_UPDATES,
ListenerDataType.LOCATION_UPDATES,
ListenerDataType.DISPLAY_PREFERENCES));
}
@Override
protected void onPause() {
dataHub.unregisterTrackDataListener(this);
dataHub = null;
if (thread != null) {
thread.interrupt();
thread = null;
}
super.onStop();
}
@Override
public boolean onUnitsChanged(boolean metric) {
// Ignore if unchanged.
if (metric == utils.isMetricUnits()) return false;
utils.setMetricUnits(metric);
updateLabels();
return true; // Reload data
}
@Override
public boolean onReportSpeedChanged(boolean displaySpeed) {
// Ignore if unchanged.
if (displaySpeed == utils.isReportSpeed()) return false;
utils.setReportSpeed(displaySpeed);
updateLabels();
return true; // Reload data
}
private void updateLabels() {
runOnUiThread(new Runnable() {
@Override
public void run() {
utils.updateUnits();
utils.setSpeedLabel(R.id.speed_label, R.string.speed, R.string.pace_label);
utils.setSpeedLabels();
}
});
}
/**
* Updates the given location fields (latitude, longitude, altitude) and all
* other fields.
*
* @param l may be null (will set location fields to unknown)
*/
private void showLocation(Location l) {
utils.setAltitude(R.id.elevation_register, l.getAltitude());
utils.setLatLong(R.id.latitude_register, l.getLatitude());
utils.setLatLong(R.id.longitude_register, l.getLongitude());
utils.setSpeed(R.id.speed_register, l.getSpeed() * 3.6);
}
private void showUnknownLocation() {
utils.setUnknown(R.id.elevation_register);
utils.setUnknown(R.id.latitude_register);
utils.setUnknown(R.id.longitude_register);
utils.setUnknown(R.id.speed_register);
}
@Override
public void onSelectedTrackChanged(Track track, boolean isRecording) {
/*
* Checks if this activity needs to update live track data or not.
* If so, make sure that:
* a) a thread keeps updating the total time
* b) a location listener is registered
* c) a content observer is registered
* Otherwise unregister listeners, observers, and kill update thread.
*/
final boolean startThread = (thread == null) && isRecording;
final boolean killThread = (thread != null) && (!isRecording);
if (startThread) {
thread = new UIUpdateThread();
thread.start();
} else if (killThread) {
thread.interrupt();
thread = null;
}
}
@Override
public void onCurrentLocationChanged(final Location loc) {
TrackDataHub localDataHub = dataHub;
if (localDataHub != null && localDataHub.isRecordingSelected()) {
runOnUiThread(new Runnable() {
@Override
public void run() {
if (loc != null) {
showLocation(loc);
} else {
showUnknownLocation();
}
}
});
}
}
@Override
public void onCurrentHeadingChanged(double heading) {
// We don't care.
}
@Override
public void onProviderStateChange(ProviderState state) {
switch (state) {
case DISABLED:
case NO_FIX:
runOnUiThread(new Runnable() {
@Override
public void run() {
showUnknownLocation();
}
});
break;
}
}
@Override
public void onTrackUpdated(final Track track) {
TrackDataHub localDataHub = dataHub;
final boolean recordingSelected = localDataHub != null && localDataHub.isRecordingSelected();
runOnUiThread(new Runnable() {
@Override
public void run() {
if (track == null || track.getStatistics() == null) {
utils.setAllToUnknown();
return;
}
startTime = track.getStatistics().getStartTime();
if (!recordingSelected) {
utils.setTime(R.id.total_time_register,
track.getStatistics().getTotalTime());
showUnknownLocation();
}
utils.setAllStats(track.getStatistics());
}
});
}
@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 void clearTrackPoints() {
// We don't care.
}
@Override
public void onNewTrackPoint(Location loc) {
// We don't care.
}
@Override
public void onSegmentSplit() {
// We don't care.
}
@Override
public void onSampledOutTrackPoint(Location loc) {
// We don't care.
}
@Override
public void onNewTrackPointsDone() {
// We don't care.
}
}
@@ -0,0 +1,263 @@
/*
* 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.TripStatistics;
import com.google.android.apps.mytracks.util.StringUtils;
import com.google.android.apps.mytracks.util.UnitConversions;
import com.google.android.maps.mytracks.R;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import android.app.Activity;
import android.util.Log;
import android.widget.TextView;
/**
* Various utility functions for views that display statistics information.
*
* @author Sandor Dornbush
*/
public class StatsUtilities {
private final Activity activity;
private static final NumberFormat LAT_LONG_FORMAT =
new DecimalFormat("##,###.00000");
private static final NumberFormat ALTITUDE_FORMAT =
new DecimalFormat("###,###");
private static final NumberFormat SPEED_FORMAT =
new DecimalFormat("#,###,###.00");
private static final NumberFormat GRADE_FORMAT =
new DecimalFormat("##.0%");
/**
* True if distances should be displayed in metric units (from shared
* preferences).
*/
private boolean metricUnits = true;
/**
* True - report speed
* False - report pace
*/
private boolean reportSpeed = true;
public StatsUtilities(Activity a) {
this.activity = a;
}
public boolean isMetricUnits() {
return metricUnits;
}
public void setMetricUnits(boolean metricUnits) {
this.metricUnits = metricUnits;
}
public boolean isReportSpeed() {
return reportSpeed;
}
public void setReportSpeed(boolean reportSpeed) {
this.reportSpeed = reportSpeed;
}
public void setUnknown(int id) {
((TextView) activity.findViewById(id)).setText(R.string.unknown);
}
public void setText(int id, double d, NumberFormat format) {
if (!Double.isNaN(d) && !Double.isInfinite(d)) {
setText(id, format.format(d));
} else {
setUnknown(id);
}
}
public void setText(int id, String s) {
int lengthLimit = 8;
String displayString = s.length() > lengthLimit
? s.substring(0, lengthLimit - 3) + "..."
: s;
((TextView) activity.findViewById(id)).setText(displayString);
}
public void setLatLong(int id, double d) {
TextView msgTextView = (TextView) activity.findViewById(id);
msgTextView.setText(LAT_LONG_FORMAT.format(d));
}
public void setAltitude(int id, double d) {
setText(id, (metricUnits ? d : (d * UnitConversions.M_TO_FT)),
ALTITUDE_FORMAT);
}
public void setDistance(int id, double d) {
setText(id, (metricUnits ? d : (d * UnitConversions.KM_TO_MI)),
SPEED_FORMAT);
}
public void setSpeed(int id, double d) {
if (d == 0) {
setUnknown(id);
return;
}
double speed = metricUnits ? d : d * UnitConversions.KM_TO_MI;
if (reportSpeed) {
setText(id, speed, SPEED_FORMAT);
} else {
// Format as milliseconds per unit
long pace = (long) (3600000.0 / speed);
setTime(id, pace);
}
}
public void setAltitudeUnits(int unitLabelId) {
TextView unitTextView = (TextView) activity.findViewById(unitLabelId);
unitTextView.setText(metricUnits ? R.string.meter : R.string.feet);
}
public void setDistanceUnits(int unitLabelId) {
TextView unitTextView = (TextView) activity.findViewById(unitLabelId);
unitTextView.setText(metricUnits ? R.string.kilometer : R.string.mile);
}
public void setSpeedUnits(int unitLabelId, int unitLabelBottomId) {
TextView unitTextView = (TextView) activity.findViewById(unitLabelId);
unitTextView.setText(reportSpeed
? (metricUnits ? R.string.kilometer : R.string.mile)
: R.string.min);
unitTextView = (TextView) activity.findViewById(unitLabelBottomId);
unitTextView.setText(reportSpeed
? R.string.hr
: (metricUnits ? R.string.kilometer : R.string.mile));
}
public void setTime(int id, long l) {
setText(id, StringUtils.formatTime(l));
}
public void setGrade(int id, double d) {
setText(id, d, GRADE_FORMAT);
}
/**
* Updates the unit fields.
*/
public void updateUnits() {
setSpeedUnits(R.id.speed_unit_label_top, R.id.speed_unit_label_bottom);
updateWaypointUnits();
}
/**
* Updates the units fields used by waypoints.
*/
public void updateWaypointUnits() {
setSpeedUnits(R.id.average_moving_speed_unit_label_top,
R.id.average_moving_speed_unit_label_bottom);
setSpeedUnits(R.id.average_speed_unit_label_top,
R.id.average_speed_unit_label_bottom);
setDistanceUnits(R.id.total_distance_unit_label);
setSpeedUnits(R.id.max_speed_unit_label_top,
R.id.max_speed_unit_label_bottom);
setAltitudeUnits(R.id.elevation_unit_label);
setAltitudeUnits(R.id.elevation_gain_unit_label);
setAltitudeUnits(R.id.min_elevation_unit_label);
setAltitudeUnits(R.id.max_elevation_unit_label);
}
/**
* Sets all fields to "-" (unknown).
*/
public void setAllToUnknown() {
// "Instant" values:
setUnknown(R.id.elevation_register);
setUnknown(R.id.latitude_register);
setUnknown(R.id.longitude_register);
setUnknown(R.id.speed_register);
// Values from provider:
setUnknown(R.id.total_time_register);
setUnknown(R.id.moving_time_register);
setUnknown(R.id.total_distance_register);
setUnknown(R.id.average_speed_register);
setUnknown(R.id.average_moving_speed_register);
setUnknown(R.id.max_speed_register);
setUnknown(R.id.min_elevation_register);
setUnknown(R.id.max_elevation_register);
setUnknown(R.id.elevation_gain_register);
setUnknown(R.id.min_grade_register);
setUnknown(R.id.max_grade_register);
}
public void setAllStats(long movingTime, double totalDistance,
double averageSpeed, double averageMovingSpeed, double maxSpeed,
double minElevation, double maxElevation, double elevationGain,
double minGrade, double maxGrade) {
setTime(R.id.moving_time_register, movingTime);
setDistance(R.id.total_distance_register, totalDistance / 1000);
setSpeed(R.id.average_speed_register, averageSpeed * 3.6);
setSpeed(R.id.average_moving_speed_register, averageMovingSpeed * 3.6);
setSpeed(R.id.max_speed_register, maxSpeed * 3.6);
setAltitude(R.id.min_elevation_register, minElevation);
setAltitude(R.id.max_elevation_register, maxElevation);
setAltitude(R.id.elevation_gain_register, elevationGain);
setGrade(R.id.min_grade_register, minGrade);
setGrade(R.id.max_grade_register, maxGrade);
}
public void setAllStats(TripStatistics stats) {
setTime(R.id.moving_time_register, stats.getMovingTime());
setDistance(R.id.total_distance_register, stats.getTotalDistance() / 1000);
setSpeed(R.id.average_speed_register, stats.getAverageSpeed() * 3.6);
setSpeed(R.id.average_moving_speed_register,
stats.getAverageMovingSpeed() * 3.6);
setSpeed(R.id.max_speed_register, stats.getMaxSpeed() * 3.6);
setAltitude(R.id.min_elevation_register, stats.getMinElevation());
setAltitude(R.id.max_elevation_register, stats.getMaxElevation());
setAltitude(R.id.elevation_gain_register, stats.getTotalElevationGain());
setGrade(R.id.min_grade_register, stats.getMinGrade());
setGrade(R.id.max_grade_register, stats.getMaxGrade());
setTime(R.id.total_time_register, stats.getTotalTime());
}
public void setSpeedLabel(int id, int speedString, int paceString) {
Log.w(Constants.TAG, "Setting view " + id +
" to " + reportSpeed +
" speed: " + speedString +
" pace: " + paceString);
TextView tv = ((TextView) activity.findViewById(id));
if (tv != null) {
tv.setText(reportSpeed ? speedString : paceString);
} else {
Log.w(Constants.TAG, "Could not find id: " + id);
}
}
public void setSpeedLabels() {
setSpeedLabel(R.id.average_speed_label,
R.string.average_speed_label,
R.string.average_pace_label);
setSpeedLabel(R.id.average_moving_speed_label,
R.string.average_moving_speed_label,
R.string.average_moving_pace_label);
setSpeedLabel(R.id.max_speed_label,
R.string.max_speed_label,
R.string.min_pace_label);
}
}
@@ -0,0 +1,124 @@
/*
* 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.maps.mytracks.R;
import android.app.Activity;
import android.content.ContentValues;
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 that let's the user see and edit the user editable track meta
* data such as name, activity type and description.
*
* @author Leif Hendrik Wilden
*/
public class TrackDetails extends Activity implements OnClickListener {
/**
* The id of the track being edited (taken from bundle, "trackid")
*/
private Long trackId;
private EditText name;
private EditText description;
private AutoCompleteTextView category;
@Override
protected void onCreate(Bundle bundle) {
super.onCreate(bundle);
setContentView(R.layout.mytracks_detail);
// Required extra when launching this intent:
trackId = getIntent().getLongExtra("trackid", -1);
if (trackId < 0) {
Log.d(Constants.TAG,
"MyTracksDetails intent was launched w/o track id.");
finish();
return;
}
// Optional extra that can be used to suppress the cancel button:
boolean hasCancelButton =
getIntent().getBooleanExtra("hasCancelButton", true);
name = (EditText) findViewById(R.id.trackdetails_name);
description = (EditText) findViewById(R.id.trackdetails_description);
category = (AutoCompleteTextView) findViewById(R.id.trackdetails_category);
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(
this,
R.array.activity_types,
android.R.layout.simple_dropdown_item_1line);
category.setAdapter(adapter);
Button cancel = (Button) findViewById(R.id.trackdetails_cancel);
if (hasCancelButton) {
cancel.setOnClickListener(this);
cancel.setVisibility(View.VISIBLE);
} else {
cancel.setVisibility(View.INVISIBLE);
}
Button save = (Button) findViewById(R.id.trackdetails_save);
save.setOnClickListener(this);
fillDialog();
}
private void fillDialog() {
Track track = MyTracksProviderUtils.Factory.get(this).getTrack(trackId);
if (track != null) {
name.setText(track.getName());
description.setText(track.getDescription());
category.setText(track.getCategory());
}
}
private void saveDialog() {
ContentValues values = new ContentValues();
values.put(TracksColumns.NAME, name.getText().toString());
values.put(TracksColumns.DESCRIPTION, description.getText().toString());
values.put(TracksColumns.CATEGORY, category.getText().toString());
getContentResolver().update(
TracksColumns.CONTENT_URI,
values,
"_id = " + trackId,
null/*selectionArgs*/);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.trackdetails_cancel:
finish();
break;
case R.id.trackdetails_save:
saveDialog();
finish();
break;
}
}
}
@@ -0,0 +1,349 @@
/*
* 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.TracksColumns;
import com.google.android.apps.mytracks.io.file.SaveActivity;
import com.google.android.apps.mytracks.io.sendtogoogle.SendActivity;
import com.google.android.apps.mytracks.services.ServiceUtils;
import com.google.android.apps.mytracks.services.TrackRecordingServiceConnection;
import com.google.android.apps.mytracks.util.StringUtils;
import com.google.android.apps.mytracks.util.UnitConversions;
import com.google.android.maps.mytracks.R;
import android.app.ListActivity;
import android.content.ContentUris;
import android.content.Intent;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.MenuItem;
import android.view.SubMenu;
import android.view.View;
import android.view.View.OnCreateContextMenuListener;
import android.view.Window;
import android.widget.AdapterView;
import android.widget.ListView;
import android.widget.SimpleCursorAdapter;
import android.widget.TextView;
/**
* A list activity displaying all the recorded tracks. There's a context
* menu (via long press) displaying various options such as showing, editing,
* deleting, sending to MyMaps, or writing to SD card.
*
* @author Leif Hendrik Wilden
*/
public class TrackList extends ListActivity
implements SharedPreferences.OnSharedPreferenceChangeListener,
View.OnClickListener {
private int contextPosition = -1;
private long trackId = -1;
private ListView listView = null;
private boolean metricUnits = true;
private Cursor tracksCursor = null;
/**
* The id of the currently recording track.
*/
private long recordingTrackId = -1;
private final OnCreateContextMenuListener contextMenuListener =
new OnCreateContextMenuListener() {
@Override
public void onCreateContextMenu(ContextMenu menu, View v,
ContextMenuInfo menuInfo) {
menu.setHeaderTitle(R.string.tracklist_this_track);
AdapterView.AdapterContextMenuInfo info =
(AdapterView.AdapterContextMenuInfo) menuInfo;
contextPosition = info.position;
trackId = TrackList.this.listView.getAdapter().getItemId(
contextPosition);
menu.add(0, Constants.MENU_SHOW, 0,
R.string.tracklist_show_track);
menu.add(0, Constants.MENU_EDIT, 0,
R.string.tracklist_edit_track);
if (!isRecording() || trackId != recordingTrackId) {
menu.add(0, Constants.MENU_SEND_TO_GOOGLE, 0,
R.string.tracklist_send_to_google);
SubMenu share = menu.addSubMenu(0, Constants.MENU_SHARE, 0,
R.string.tracklist_share_track);
share.add(0, Constants.MENU_SHARE_LINK, 0,
R.string.tracklist_share_link);
share.add(0, Constants.MENU_SHARE_GPX_FILE, 0,
R.string.tracklist_share_gpx_file);
share.add(0, Constants.MENU_SHARE_KML_FILE, 0,
R.string.tracklist_share_kml_file);
share.add(0, Constants.MENU_SHARE_CSV_FILE, 0,
R.string.tracklist_share_csv_file);
share.add(0, Constants.MENU_SHARE_TCX_FILE, 0,
R.string.tracklist_share_tcx_file);
SubMenu save = menu.addSubMenu(0,
Constants.MENU_WRITE_TO_SD_CARD, 0,
R.string.tracklist_write_to_sd);
save.add(0, Constants.MENU_SAVE_GPX_FILE, 0,
R.string.tracklist_save_as_gpx);
save.add(0, Constants.MENU_SAVE_KML_FILE, 0,
R.string.tracklist_save_as_kml);
save.add(0, Constants.MENU_SAVE_CSV_FILE, 0,
R.string.tracklist_save_as_csv);
save.add(0, Constants.MENU_SAVE_TCX_FILE, 0,
R.string.tracklist_save_as_tcx);
menu.add(0, Constants.MENU_DELETE, 0,
R.string.tracklist_delete_track);
}
}
};
private final Runnable serviceBindingChanged = new Runnable() {
@Override
public void run() {
updateButtonsEnabled();
}
};
private TrackRecordingServiceConnection serviceConnection;
private SharedPreferences preferences;
@Override
public void onSharedPreferenceChanged(
SharedPreferences sharedPreferences, String key) {
if (key == null) {
return;
}
if (key.equals(getString(R.string.metric_units_key))) {
metricUnits = sharedPreferences.getBoolean(
getString(R.string.metric_units_key), true);
if (tracksCursor != null && !tracksCursor.isClosed()) {
tracksCursor.requery();
}
}
if (key.equals(getString(R.string.recording_track_key))) {
recordingTrackId = sharedPreferences.getLong(
getString(R.string.recording_track_key), -1);
}
}
@Override
protected void onListItemClick(ListView l, View v, int position, long id) {
Intent result = new Intent();
result.putExtra("trackid", id);
setResult(Constants.SHOW_TRACK, result);
finish();
}
@Override
public boolean onMenuItemSelected(int featureId, MenuItem item) {
switch (item.getItemId()) {
case Constants.MENU_SHOW: {
onListItemClick(null, null, 0, trackId);
return true;
}
case Constants.MENU_EDIT: {
Intent intent = new Intent(this, TrackDetails.class);
intent.putExtra("trackid", trackId);
startActivity(intent);
return true;
}
case Constants.MENU_SHARE:
case Constants.MENU_WRITE_TO_SD_CARD:
return false;
case Constants.MENU_SEND_TO_GOOGLE:
SendActivity.sendToGoogle(this, trackId, false);
return true;
case Constants.MENU_SHARE_LINK:
SendActivity.sendToGoogle(this, trackId, true);
return true;
case Constants.MENU_SAVE_GPX_FILE:
case Constants.MENU_SAVE_KML_FILE:
case Constants.MENU_SAVE_CSV_FILE:
case Constants.MENU_SAVE_TCX_FILE:
case Constants.MENU_SHARE_GPX_FILE:
case Constants.MENU_SHARE_KML_FILE:
case Constants.MENU_SHARE_CSV_FILE:
case Constants.MENU_SHARE_TCX_FILE:
SaveActivity.handleExportTrackAction(this, trackId,
Constants.getActionFromMenuId(item.getItemId()));
return true;
case Constants.MENU_DELETE: {
Intent intent = new Intent(Intent.ACTION_DELETE);
Uri uri = ContentUris.withAppendedId(TracksColumns.CONTENT_URI, trackId);
intent.setDataAndType(uri, TracksColumns.CONTENT_ITEMTYPE);
startActivity(intent);
return true;
}
default:
Log.w(TAG, "Unknown menu item: " + item.getItemId() + "(" + item.getTitle() + ")");
return super.onMenuItemSelected(featureId, item);
}
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.tracklist_btn_delete_all: {
Handler h = new DeleteAllTracks(this, null);
h.handleMessage(null);
break;
}
case R.id.tracklist_btn_export_all: {
new ExportAllTracks(this);
break;
}
case R.id.tracklist_btn_import_all: {
new ImportAllTracks(this);
break;
}
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// We don't need a window title bar:
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.mytracks_list);
listView = getListView();
listView.setOnCreateContextMenuListener(contextMenuListener);
preferences = getSharedPreferences(Constants.SETTINGS_NAME, 0);
serviceConnection = new TrackRecordingServiceConnection(this, serviceBindingChanged);
View deleteAll = findViewById(R.id.tracklist_btn_delete_all);
deleteAll.setOnClickListener(this);
View exportAll = findViewById(R.id.tracklist_btn_export_all);
exportAll.setOnClickListener(this);
updateButtonsEnabled();
findViewById(R.id.tracklist_btn_import_all).setOnClickListener(this);
preferences.registerOnSharedPreferenceChangeListener(this);
metricUnits =
preferences.getBoolean(getString(R.string.metric_units_key), true);
recordingTrackId =
preferences.getLong(getString(R.string.recording_track_key), -1);
tracksCursor = getContentResolver().query(
TracksColumns.CONTENT_URI, null, null, null, "_id DESC");
startManagingCursor(tracksCursor);
setListAdapter();
}
@Override
protected void onStart() {
super.onStart();
serviceConnection.bindIfRunning();
}
@Override
protected void onDestroy() {
serviceConnection.unbind();
super.onDestroy();
}
private void updateButtonsEnabled() {
View deleteAll = findViewById(R.id.tracklist_btn_delete_all);
View exportAll = findViewById(R.id.tracklist_btn_export_all);
boolean notRecording = !isRecording();
deleteAll.setEnabled(notRecording);
exportAll.setEnabled(notRecording);
}
private void setListAdapter() {
// Get a cursor with all tracks
SimpleCursorAdapter adapter = new SimpleCursorAdapter(
this,
R.layout.mytracks_list_item,
tracksCursor,
new String[] { TracksColumns.NAME, TracksColumns.STARTTIME,
TracksColumns.TOTALDISTANCE, TracksColumns.DESCRIPTION,
TracksColumns.CATEGORY },
new int[] { R.id.trackdetails_item_name, R.id.trackdetails_item_time,
R.id.trackdetails_item_stats, R.id.trackdetails_item_description,
R.id.trackdetails_item_category });
final int startTimeIdx =
tracksCursor.getColumnIndexOrThrow(TracksColumns.STARTTIME);
final int totalTimeIdx =
tracksCursor.getColumnIndexOrThrow(TracksColumns.TOTALTIME);
final int totalDistanceIdx =
tracksCursor.getColumnIndexOrThrow(TracksColumns.TOTALDISTANCE);
adapter.setViewBinder(new SimpleCursorAdapter.ViewBinder() {
@Override
public boolean setViewValue(View view, Cursor cursor, int columnIndex) {
TextView textView = (TextView) view;
if (columnIndex == startTimeIdx) {
long time = cursor.getLong(startTimeIdx);
textView.setText(String.format("%tc", time));
} else if (columnIndex == totalDistanceIdx) {
double length = cursor.getDouble(totalDistanceIdx);
String lengthUnit = null;
if (metricUnits) {
if (length > 1000) {
length /= 1000;
lengthUnit = getString(R.string.kilometer);
} else {
lengthUnit = getString(R.string.meter);
}
} else {
if (length > UnitConversions.MI_TO_M) {
length /= UnitConversions.MI_TO_M;
lengthUnit = getString(R.string.mile);
} else {
length *= UnitConversions.M_TO_FT;
lengthUnit = getString(R.string.feet);
}
}
textView.setText(String.format("%s %.2f %s",
StringUtils.formatTime(cursor.getLong(totalTimeIdx)),
length,
lengthUnit));
} else {
textView.setText(cursor.getString(columnIndex));
if (textView.getText().length() < 1) {
textView.setVisibility(View.GONE);
} else {
textView.setVisibility(View.VISIBLE);
}
}
return true;
}
});
setListAdapter(adapter);
}
private boolean isRecording() {
return ServiceUtils.isRecording(TrackList.this, serviceConnection.getServiceIfBound(), preferences);
}
}
@@ -0,0 +1,177 @@
/*
* 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.content.WaypointsColumns;
import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.android.maps.mytracks.R;
import android.app.Activity;
import android.content.ContentValues;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.inputmethod.EditorInfo;
import android.widget.ArrayAdapter;
import android.widget.AutoCompleteTextView;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageView;
/**
* Screen in which the user enters details about a waypoint.
*
* @author Leif Hendrik Wilden
*/
public class WaypointDetails extends Activity
implements OnClickListener {
public static final String WAYPOINT_ID_EXTRA = "com.google.android.apps.mytracks.WAYPOINT_ID";
/**
* The id of the way point being edited (taken from bundle, "waypointid")
*/
private Long waypointId;
private EditText name;
private EditText description;
private AutoCompleteTextView category;
private View detailsView;
private View statsView;
private StatsUtilities utils;
private Waypoint waypoint;
@Override
protected void onCreate(Bundle bundle) {
super.onCreate(bundle);
setContentView(R.layout.mytracks_waypoint_details);
utils = new StatsUtilities(this);
SharedPreferences preferences =
getSharedPreferences(Constants.SETTINGS_NAME, 0);
if (preferences != null) {
boolean useMetric =
preferences.getBoolean(getString(R.string.metric_units_key), true);
utils.setMetricUnits(useMetric);
boolean displaySpeed =
preferences.getBoolean(getString(R.string.report_speed_key), true);
utils.setReportSpeed(displaySpeed);
utils.updateWaypointUnits();
utils.setSpeedLabels();
}
// Required extra when launching this intent:
waypointId = getIntent().getLongExtra(WAYPOINT_ID_EXTRA, -1);
if (waypointId < 0) {
Log.d(Constants.TAG,
"MyTracksWaypointsDetails intent was launched w/o waypoint id.");
finish();
return;
}
// Optional extra that can be used to suppress the cancel button:
boolean hasCancelButton =
getIntent().getBooleanExtra("hasCancelButton", true);
name = (EditText) findViewById(R.id.waypointdetails_name);
description = (EditText) findViewById(R.id.waypointdetails_description);
category =
(AutoCompleteTextView) findViewById(R.id.waypointdetails_category);
statsView = findViewById(R.id.waypoint_stats);
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(
this,
R.array.waypoint_types,
android.R.layout.simple_dropdown_item_1line);
category.setAdapter(adapter);
detailsView = findViewById(R.id.waypointdetails_description_layout);
Button cancel = (Button) findViewById(R.id.waypointdetails_cancel);
if (hasCancelButton) {
cancel.setOnClickListener(this);
cancel.setVisibility(View.VISIBLE);
} else {
cancel.setVisibility(View.INVISIBLE);
}
Button save = (Button) findViewById(R.id.waypointdetails_save);
save.setOnClickListener(this);
fillDialog();
}
private void fillDialog() {
waypoint = MyTracksProviderUtils.Factory.get(this).getWaypoint(waypointId);
if (waypoint != null) {
name.setText(waypoint.getName());
ImageView icon = (ImageView) findViewById(R.id.waypointdetails_icon);
int iconId = -1;
switch(waypoint.getType()) {
case Waypoint.TYPE_WAYPOINT:
description.setText(waypoint.getDescription());
detailsView.setVisibility(View.VISIBLE);
category.setText(waypoint.getCategory());
statsView.setVisibility(View.GONE);
iconId = R.drawable.blue_pushpin;
break;
case Waypoint.TYPE_STATISTICS:
detailsView.setVisibility(View.GONE);
statsView.setVisibility(View.VISIBLE);
iconId = R.drawable.ylw_pushpin;
TripStatistics waypointStats = waypoint.getStatistics();
utils.setAllStats(waypointStats);
utils.setAltitude(
R.id.elevation_register, waypoint.getLocation().getAltitude());
name.setImeOptions(EditorInfo.IME_ACTION_DONE);
break;
}
icon.setImageDrawable(getResources().getDrawable(iconId));
}
}
private void saveDialog() {
ContentValues values = new ContentValues();
values.put(WaypointsColumns.NAME, name.getText().toString());
if (waypoint != null && waypoint.getType() == Waypoint.TYPE_WAYPOINT) {
values.put(WaypointsColumns.DESCRIPTION,
description.getText().toString());
values.put(WaypointsColumns.CATEGORY, category.getText().toString());
}
getContentResolver().update(
WaypointsColumns.CONTENT_URI,
values,
"_id = " + waypointId,
null /*selectionArgs*/);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.waypointdetails_cancel:
finish();
break;
case R.id.waypointdetails_save:
saveDialog();
finish();
break;
}
}
}
@@ -0,0 +1,320 @@
/*
* 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 static com.google.android.apps.mytracks.Constants.TAG;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.content.WaypointCreationRequest;
import com.google.android.apps.mytracks.content.WaypointsColumns;
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.AlertDialog;
import android.app.ListActivity;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.os.Bundle;
import android.os.RemoteException;
import android.util.Log;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnCreateContextMenuListener;
import android.view.Window;
import android.widget.AdapterView;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.SimpleCursorAdapter;
import android.widget.TextView;
import android.widget.Toast;
/**
* Activity which shows the list of waypoints in a track.
*
* @author Leif Hendrik Wilden
*/
public class WaypointsList extends ListActivity
implements View.OnClickListener {
private int contextPosition = -1;
private long trackId = -1;
private long selectedWaypointId = -1;
private ListView listView = null;
private Button insertWaypointButton = null;
private Button insertStatisticsButton = null;
private long recordingTrackId = -1;
private MyTracksProviderUtils providerUtils;
private TrackRecordingServiceConnection serviceConnection;
private Cursor waypointsCursor = null;
private final OnCreateContextMenuListener contextMenuListener =
new OnCreateContextMenuListener() {
public void onCreateContextMenu(ContextMenu menu, View v,
ContextMenuInfo menuInfo) {
menu.setHeaderTitle(R.string.waypointslist_this_waypoint);
AdapterView.AdapterContextMenuInfo info =
(AdapterView.AdapterContextMenuInfo) menuInfo;
contextPosition = info.position;
selectedWaypointId = WaypointsList.this.listView.getAdapter()
.getItemId(contextPosition);
Waypoint waypoint = providerUtils.getWaypoint(info.id);
if (waypoint != null) {
int type = waypoint.getType();
menu.add(0, Constants.MENU_SHOW, 0,
R.string.waypointslist_show_waypoint);
menu.add(0, Constants.MENU_EDIT, 0,
R.string.waypointslist_edit_waypoint);
menu.add(0, Constants.MENU_DELETE, 0,
R.string.waypointslist_delete_waypoint).setEnabled(
recordingTrackId < 0 || type == Waypoint.TYPE_WAYPOINT ||
info.id != providerUtils.getLastWaypointId(recordingTrackId));
}
}
};
@Override
protected void onListItemClick(ListView l, View v, int position, long id) {
editWaypoint(id);
}
@Override
public boolean onMenuItemSelected(int featureId, MenuItem item) {
if (!super.onMenuItemSelected(featureId, item)) {
switch (item.getItemId()) {
case Constants.MENU_SHOW: {
Intent result = new Intent();
result.putExtra("trackid", trackId);
result.putExtra(WaypointDetails.WAYPOINT_ID_EXTRA, selectedWaypointId);
setResult(RESULT_OK, result);
finish();
return true;
}
case Constants.MENU_EDIT: {
editWaypoint(selectedWaypointId);
return true;
}
case Constants.MENU_DELETE: {
deleteWaypoint(selectedWaypointId);
}
}
}
return false;
}
private void editWaypoint(long waypointId) {
Intent intent = new Intent(this, WaypointDetails.class);
intent.putExtra("trackid", trackId);
intent.putExtra(WaypointDetails.WAYPOINT_ID_EXTRA, waypointId);
startActivity(intent);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
providerUtils = MyTracksProviderUtils.Factory.get(this);
serviceConnection = new TrackRecordingServiceConnection(this, null);
// We don't need a window title bar:
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.mytracks_waypoints_list);
listView = getListView();
listView.setOnCreateContextMenuListener(contextMenuListener);
insertWaypointButton =
(Button) findViewById(R.id.waypointslist_btn_insert_waypoint);
insertWaypointButton.setOnClickListener(this);
insertStatisticsButton =
(Button) findViewById(R.id.waypointslist_btn_insert_statistics);
insertStatisticsButton.setOnClickListener(this);
SharedPreferences preferences =
getSharedPreferences(Constants.SETTINGS_NAME, 0);
// TODO: Get rid of selected and recording track IDs
long selectedTrackId = -1;
if (preferences != null) {
recordingTrackId =
preferences.getLong(getString(R.string.recording_track_key), -1);
selectedTrackId =
preferences.getLong(getString(R.string.selected_track_key), -1);
}
boolean selectedRecording = selectedTrackId > 0
&& selectedTrackId == recordingTrackId;
insertWaypointButton.setEnabled(selectedRecording);
insertStatisticsButton.setEnabled(selectedRecording);
if (getIntent() != null && getIntent().getExtras() != null) {
trackId = getIntent().getExtras().getLong("trackid", -1);
} else {
trackId = -1;
}
final long firstWaypointId = providerUtils.getFirstWaypointId(trackId);
waypointsCursor = getContentResolver().query(
WaypointsColumns.CONTENT_URI, null,
WaypointsColumns.TRACKID + "=" + trackId + " AND "
+ WaypointsColumns._ID + "!=" + firstWaypointId, null, null);
startManagingCursor(waypointsCursor);
setListAdapter();
}
@Override
protected void onResume() {
super.onResume();
serviceConnection.bindIfRunning();
}
@Override
protected void onDestroy() {
serviceConnection.unbind();
super.onDestroy();
}
@Override
public void onClick(View v) {
WaypointCreationRequest request;
switch (v.getId()) {
case R.id.waypointslist_btn_insert_waypoint:
request = WaypointCreationRequest.DEFAULT_MARKER;
break;
case R.id.waypointslist_btn_insert_statistics:
request = WaypointCreationRequest.DEFAULT_STATISTICS;
break;
default:
return;
}
long id = insertWaypoint(request);
if (id < 0) {
Toast.makeText(this, R.string.error_unable_to_insert_marker,
Toast.LENGTH_LONG).show();
Log.e(Constants.TAG, "Failed to insert marker.");
return;
}
Intent intent = new Intent(this, WaypointDetails.class);
intent.putExtra(WaypointDetails.WAYPOINT_ID_EXTRA, id);
startActivity(intent);
}
private long insertWaypoint(WaypointCreationRequest request) {
try {
ITrackRecordingService trackRecordingService = serviceConnection.getServiceIfBound();
if (trackRecordingService != null) {
long waypointId = trackRecordingService.insertWaypoint(request);
if (waypointId >= 0) {
Toast.makeText(this, R.string.status_marker_inserted,
Toast.LENGTH_LONG).show();
return waypointId;
}
} else {
Log.e(TAG, "Not connected to service, not inserting waypoint");
}
} catch (RemoteException e) {
Log.e(Constants.TAG, "Cannot insert marker.", e);
} catch (IllegalStateException e) {
Log.e(Constants.TAG, "Cannot insert marker.", e);
}
return -1;
}
private void setListAdapter() {
// Get a cursor with all tracks
SimpleCursorAdapter adapter = new SimpleCursorAdapter(
this,
R.layout.mytracks_marker_item,
waypointsCursor,
new String[] { WaypointsColumns.NAME, WaypointsColumns.TIME,
WaypointsColumns.CATEGORY, WaypointsColumns.TYPE },
new int[] { R.id.waypointslist_item_name,
R.id.waypointslist_item_time,
R.id.waypointslist_item_category,
R.id.waypointslist_item_icon });
final int timeIdx =
waypointsCursor.getColumnIndexOrThrow(WaypointsColumns.TIME);
final int typeIdx =
waypointsCursor.getColumnIndexOrThrow(WaypointsColumns.TYPE);
adapter.setViewBinder(new SimpleCursorAdapter.ViewBinder() {
@Override
public boolean setViewValue(View view, Cursor cursor, int columnIndex) {
if (columnIndex == timeIdx) {
long time = cursor.getLong(timeIdx);
TextView textView = (TextView) view;
textView.setText(String.format("%tc", time));
textView.setVisibility(
textView.getText().length() < 1 ? View.GONE : View.VISIBLE);
} else if (columnIndex == typeIdx) {
int type = cursor.getInt(typeIdx);
ImageView imageView = (ImageView) view;
imageView.setImageDrawable(getResources().getDrawable(
type == Waypoint.TYPE_STATISTICS
? R.drawable.ylw_pushpin
: R.drawable.blue_pushpin));
} else {
TextView textView = (TextView) view;
textView.setText(cursor.getString(columnIndex));
textView.setVisibility(
textView.getText().length() < 1 ? View.GONE : View.VISIBLE);
}
return true;
}
});
setListAdapter(adapter);
}
/**
* Deletes the way point with the given id.
* Prompts the user if he want to really delete it.
*/
public void deleteWaypoint(final long waypointId) {
AlertDialog dialog = null;
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setMessage(getString(R.string.marker_will_be_permanently_deleted));
builder.setTitle(getString(R.string.confirmation_title_are_you_sure));
builder.setIcon(android.R.drawable.ic_dialog_alert);
builder.setPositiveButton(getString(R.string.yes),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
dialogInterface.dismiss();
providerUtils.deleteWaypoint(waypointId,
new StringUtils(WaypointsList.this));
}
});
builder.setNegativeButton(getString(R.string.no),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
dialogInterface.dismiss();
}
});
dialog = builder.create();
dialog.show();
}
}
@@ -0,0 +1,87 @@
/*
* 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.util.SystemUtils;
import com.google.android.maps.mytracks.R;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.Window;
import android.webkit.WebView;
import android.widget.TextView;
/**
* An activity that displays a welcome screen.
*
* @author Sandor Dornbush
*/
public class WelcomeActivity extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
requestWindowFeature(Window.FEATURE_PROGRESS);
setContentView(R.layout.welcome);
WebView web = (WebView) findViewById(R.id.welcome_web);
web.loadUrl("file:///android_asset/welcome.html");
findViewById(R.id.welcome_ok).setOnClickListener(
new OnClickListener() {
public void onClick(View v) {
finish();
}
});
findViewById(R.id.welcome_about).setOnClickListener(new OnClickListener() {
public void onClick(View v) {
about();
}
});
}
/**
* Shows the "about" dialog.
*
* TODO: Add a menu option for showing the same thing.
*/
public void about() {
LayoutInflater li = LayoutInflater.from(this);
View view = li.inflate(R.layout.about, null);
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setView(view);
builder.setPositiveButton(R.string.ok, null);
builder.setNegativeButton(R.string.license, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Eula.showEula(WelcomeActivity.this);
}
});
builder.setIcon(R.drawable.arrow_icon);
AlertDialog dialog = builder.create();
dialog.show();
((TextView) dialog.findViewById(R.id.about_version_register)).
setText(SystemUtils.getMyTracksVersion(this));
}
}
@@ -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.content;
/**
* Utilities for serializing primitive types.
*
* @author Rodrigo Damazio
*/
public class ContentTypeIds {
public static final byte BOOLEAN_TYPE_ID = 0;
public static final byte LONG_TYPE_ID = 1;
public static final byte INT_TYPE_ID = 2;
public static final byte FLOAT_TYPE_ID = 3;
public static final byte DOUBLE_TYPE_ID = 4;
public static final byte STRING_TYPE_ID = 5;
public static final byte BLOB_TYPE_ID = 6;
private ContentTypeIds() { /* Not instantiable */ }
}
@@ -0,0 +1,265 @@
package com.google.android.apps.mytracks.content;
import static com.google.android.apps.mytracks.Constants.TAG;
import com.google.android.apps.mytracks.content.TrackDataHub.ListenerDataType;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.database.ContentObserver;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.location.LocationProvider;
import android.os.Bundle;
import android.os.Handler;
import android.util.Log;
import java.util.EnumSet;
import java.util.Set;
/**
* External data source manager, which converts system-level events into My Tracks data events.
*
* @author Rodrigo Damazio
*/
class DataSourceManager {
/** Single interface for receiving system events that were registered for. */
interface DataSourceListener {
void notifyTrackUpdated();
void notifyWaypointUpdated();
void notifyPointsUpdated();
void notifyPreferenceChanged(String key);
void notifyLocationProviderEnabled(boolean enabled);
void notifyLocationProviderAvailable(boolean available);
void notifyLocationChanged(Location loc);
void notifyHeadingChanged(float heading);
}
private final DataSourceListener listener;
/** Observer for when the tracks table is updated. */
private class TrackObserver extends ContentObserver {
public TrackObserver() {
super(contentHandler);
}
@Override
public void onChange(boolean selfChange) {
listener.notifyTrackUpdated();
}
}
/** Observer for when the waypoints table is updated. */
private class WaypointObserver extends ContentObserver {
public WaypointObserver() {
super(contentHandler);
}
@Override
public void onChange(boolean selfChange) {
listener.notifyWaypointUpdated();
}
}
/** Observer for when the points table is updated. */
private class PointObserver extends ContentObserver {
public PointObserver() {
super(contentHandler);
}
@Override
public void onChange(boolean selfChange) {
listener.notifyPointsUpdated();
}
}
/** Listener for when preferences change. */
private class HubSharedPreferenceListener implements OnSharedPreferenceChangeListener {
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
listener.notifyPreferenceChanged(key);
}
}
/** Listener for the current location (independent from track data). */
private class CurrentLocationListener implements
LocationListener {
@Override
public void onStatusChanged(String provider, int status, Bundle extras) {
if (!LocationManager.GPS_PROVIDER.equals(provider)) return;
listener.notifyLocationProviderAvailable(status == LocationProvider.AVAILABLE);
}
@Override
public void onProviderEnabled(String provider) {
if (!LocationManager.GPS_PROVIDER.equals(provider)) return;
listener.notifyLocationProviderEnabled(true);
}
@Override
public void onProviderDisabled(String provider) {
if (!LocationManager.GPS_PROVIDER.equals(provider)) return;
listener.notifyLocationProviderEnabled(false);
}
@Override
public void onLocationChanged(Location location) {
listener.notifyLocationChanged(location);
}
}
/** Listener for compass readings. */
private class CompassListener implements
SensorEventListener {
@Override
public void onSensorChanged(SensorEvent event) {
listener.notifyHeadingChanged(event.values[0]);
}
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
// Do nothing
}
}
/** Wrapper for registering internal listeners. */
private final DataSourcesWrapper dataSources;
// Internal listeners (to receive data from the system)
private final Set<ListenerDataType> registeredListeners =
EnumSet.noneOf(ListenerDataType.class);
private final Handler contentHandler;
private final ContentObserver pointObserver;
private final ContentObserver waypointObserver;
private final ContentObserver trackObserver;
private final LocationListener locationListener;
private final OnSharedPreferenceChangeListener preferenceListener;
private final SensorEventListener compassListener;
DataSourceManager(DataSourceListener listener, DataSourcesWrapper dataSources) {
this.listener = listener;
this.dataSources = dataSources;
contentHandler = new Handler();
pointObserver = new PointObserver();
waypointObserver = new WaypointObserver();
trackObserver = new TrackObserver();
compassListener = new CompassListener();
locationListener = new CurrentLocationListener();
preferenceListener = new HubSharedPreferenceListener();
}
/** Updates the internal (sensor, position, etc) listeners. */
void updateAllListeners(EnumSet<ListenerDataType> externallyNeededListeners) {
EnumSet<ListenerDataType> neededListeners = EnumSet.copyOf(externallyNeededListeners);
// Special case - map sampled-out points type to points type since they
// correspond to the same internal listener.
if (neededListeners.contains(ListenerDataType.SAMPLED_OUT_POINT_UPDATES)) {
neededListeners.remove(ListenerDataType.SAMPLED_OUT_POINT_UPDATES);
neededListeners.add(ListenerDataType.POINT_UPDATES);
}
Log.d(TAG, "Updating internal listeners to types " + neededListeners);
// Unnecessary = registered - needed
Set<ListenerDataType> unnecessaryListeners = EnumSet.copyOf(registeredListeners);
unnecessaryListeners.removeAll(neededListeners);
// Missing = needed - registered
Set<ListenerDataType> missingListeners = EnumSet.copyOf(neededListeners);
missingListeners.removeAll(registeredListeners);
// Remove all unnecessary listeners.
for (ListenerDataType type : unnecessaryListeners) {
unregisterListener(type);
}
// Add all missing listeners.
for (ListenerDataType type : missingListeners) {
registerListener(type);
}
// Now all needed types are registered.
registeredListeners.clear();
registeredListeners.addAll(neededListeners);
}
private void registerListener(ListenerDataType type) {
switch (type) {
case COMPASS_UPDATES: {
// Listen to compass
Sensor compass = dataSources.getSensor(Sensor.TYPE_ORIENTATION);
if (compass != null) {
Log.d(TAG, "TrackDataHub: Now registering sensor listener.");
dataSources.registerSensorListener(compassListener, compass, SensorManager.SENSOR_DELAY_UI);
}
break;
}
case LOCATION_UPDATES:
dataSources.requestLocationUpdates(locationListener);
break;
case POINT_UPDATES:
dataSources.registerContentObserver(
TrackPointsColumns.CONTENT_URI, false, pointObserver);
break;
case TRACK_UPDATES:
dataSources.registerContentObserver(TracksColumns.CONTENT_URI, false, trackObserver);
break;
case WAYPOINT_UPDATES:
dataSources.registerContentObserver(
WaypointsColumns.CONTENT_URI, false, waypointObserver);
break;
case DISPLAY_PREFERENCES:
dataSources.registerOnSharedPreferenceChangeListener(preferenceListener);
break;
case SAMPLED_OUT_POINT_UPDATES:
throw new IllegalArgumentException("Should have been mapped to point updates");
}
}
private void unregisterListener(ListenerDataType type) {
switch (type) {
case COMPASS_UPDATES:
dataSources.unregisterSensorListener(compassListener);
break;
case LOCATION_UPDATES:
dataSources.removeLocationUpdates(locationListener);
break;
case POINT_UPDATES:
dataSources.unregisterContentObserver(pointObserver);
break;
case TRACK_UPDATES:
dataSources.unregisterContentObserver(trackObserver);
break;
case WAYPOINT_UPDATES:
dataSources.unregisterContentObserver(waypointObserver);
break;
case DISPLAY_PREFERENCES:
dataSources.unregisterOnSharedPreferenceChangeListener(preferenceListener);
break;
case SAMPLED_OUT_POINT_UPDATES:
throw new IllegalArgumentException("Should have been mapped to point updates");
}
}
/** Unregisters all internal (sensor, position, etc.) listeners. */
void unregisterAllListeners() {
dataSources.removeLocationUpdates(locationListener);
dataSources.unregisterSensorListener(compassListener);
dataSources.unregisterContentObserver(trackObserver);
dataSources.unregisterContentObserver(waypointObserver);
dataSources.unregisterContentObserver(pointObserver);
dataSources.unregisterOnSharedPreferenceChangeListener(preferenceListener);
}
}
@@ -0,0 +1,54 @@
/*
* Copyright 2011 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.content;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.database.ContentObserver;
import android.hardware.Sensor;
import android.hardware.SensorEventListener;
import android.location.Location;
import android.location.LocationListener;
import android.net.Uri;
/**
* Interface for abstracting registration of external data source listeners.
*
* @author Rodrigo Damazio
*/
interface DataSourcesWrapper {
// Preferences
void registerOnSharedPreferenceChangeListener(
OnSharedPreferenceChangeListener listener);
void unregisterOnSharedPreferenceChangeListener(
OnSharedPreferenceChangeListener listener);
// Content provider
void registerContentObserver(Uri contentUri, boolean descendents,
ContentObserver observer);
void unregisterContentObserver(ContentObserver observer);
// Sensors
Sensor getSensor(int type);
void registerSensorListener(SensorEventListener listener,
Sensor sensor, int sensorDelay);
void unregisterSensorListener(SensorEventListener listener);
// Location
boolean isLocationProviderEnabled(String provider);
void requestLocationUpdates(LocationListener listener);
void removeLocationUpdates(LocationListener listener);
Location getLastKnownLocation();
}
@@ -0,0 +1,166 @@
/*
* 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_LOCATION_AGE_MS;
import static com.google.android.apps.mytracks.Constants.MAX_NETWORK_AGE_MS;
import com.google.android.apps.mytracks.Constants;
import com.google.android.maps.mytracks.R;
import android.content.ContentResolver;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.database.ContentObserver;
import android.hardware.Sensor;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.location.LocationProvider;
import android.net.Uri;
import android.util.Log;
import android.widget.Toast;
/**
* Real implementation of the data sources, which talks to system services.
*
* @author Rodrigo Damazio
*/
class DataSourcesWrapperImpl implements DataSourcesWrapper {
// System services
private final SensorManager sensorManager;
private final LocationManager locationManager;
private final ContentResolver contentResolver;
private final SharedPreferences sharedPreferences;
private final Context context;
DataSourcesWrapperImpl(Context context, SharedPreferences sharedPreferences) {
this.context = context;
this.sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
this.locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
this.contentResolver = context.getContentResolver();
this.sharedPreferences = sharedPreferences;
}
@Override
public void registerOnSharedPreferenceChangeListener(
OnSharedPreferenceChangeListener listener) {
sharedPreferences.registerOnSharedPreferenceChangeListener(listener);
}
@Override
public void unregisterOnSharedPreferenceChangeListener(
OnSharedPreferenceChangeListener listener) {
sharedPreferences.unregisterOnSharedPreferenceChangeListener(listener);
}
@Override
public void registerContentObserver(Uri contentUri, boolean descendents,
ContentObserver observer) {
contentResolver.registerContentObserver(contentUri, descendents, observer);
}
@Override
public void unregisterContentObserver(ContentObserver observer) {
contentResolver.unregisterContentObserver(observer);
}
@Override
public Sensor getSensor(int type) {
return sensorManager.getDefaultSensor(type);
}
@Override
public void registerSensorListener(SensorEventListener listener,
Sensor sensor, int sensorDelay) {
sensorManager.registerListener(listener, sensor, sensorDelay);
}
@Override
public void unregisterSensorListener(SensorEventListener listener) {
sensorManager.unregisterListener(listener);
}
@Override
public boolean isLocationProviderEnabled(String provider) {
return locationManager.isProviderEnabled(provider);
}
@Override
public void requestLocationUpdates(LocationListener listener) {
// Check if the provider exists.
LocationProvider gpsProvider = locationManager.getProvider(LocationManager.GPS_PROVIDER);
if (gpsProvider == null) {
listener.onProviderDisabled(LocationManager.GPS_PROVIDER);
locationManager.removeUpdates(listener);
return;
}
// Listen to GPS location.
String providerName = gpsProvider.getName();
Log.d(Constants.TAG, "TrackDataHub: Using location provider " + providerName);
locationManager.requestLocationUpdates(providerName,
0 /*minTime*/, 0 /*minDist*/, listener);
// Give an initial update on provider state.
if (locationManager.isProviderEnabled(providerName)) {
listener.onProviderEnabled(providerName);
} else {
listener.onProviderDisabled(providerName);
}
// Listen to network location
try {
locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER,
1000 * 60 * 5 /*minTime*/, 0 /*minDist*/, listener);
} catch (RuntimeException e) {
// If anything at all goes wrong with getting a cell location do not
// abort. Cell location is not essential to this app.
Log.w(Constants.TAG, "Could not register network location listener.", e);
}
}
@Override
public Location getLastKnownLocation() {
// TODO: Let's look at more advanced algorithms to determine the best
// current location.
Location loc = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER);
final long now = System.currentTimeMillis();
if (loc == null || loc.getTime() < now - MAX_LOCATION_AGE_MS) {
// We don't have a recent GPS fix, just use cell towers if available
loc = locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER);
int toastResId = R.string.status_approximate_current_location;
if (loc == null || loc.getTime() < now - MAX_NETWORK_AGE_MS) {
// We don't have a recent cell tower location, let the user know:
toastResId = R.string.status_no_current_location;
}
// Let the user know we have only an approximate location:
Toast.makeText(context, context.getString(toastResId), Toast.LENGTH_LONG).show();
}
return loc;
}
@Override
public void removeLocationUpdates(LocationListener listener) {
locationManager.removeUpdates(listener);
}
}
@@ -0,0 +1,474 @@
/*
* 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.Constants;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.maps.mytracks.R;
import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.SharedPreferences;
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;
/**
* A provider that handles recorded (GPS) tracks and their track points.
*
* @author Leif Hendrik Wilden
*/
public class MyTracksProvider extends ContentProvider {
private static final String DATABASE_NAME = "mytracks.db";
private static final int DATABASE_VERSION = 19;
private static final int TRACKPOINTS = 1;
private static final int TRACKPOINTS_ID = 2;
private static final int TRACKS = 3;
private static final int TRACKS_ID = 4;
private static final int WAYPOINTS = 5;
private static final int WAYPOINTS_ID = 6;
private static final String TRACKPOINTS_TABLE = "trackpoints";
private static final String TRACKS_TABLE = "tracks";
private static final String WAYPOINTS_TABLE = "waypoints";
public static final String TAG = "MyTracksProvider";
/**
* Helper which creates or upgrades the database if necessary.
*/
private static class DatabaseHelper extends SQLiteOpenHelper {
public DatabaseHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL("CREATE TABLE " + TRACKPOINTS_TABLE + " ("
+ TrackPointsColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
+ TrackPointsColumns.TRACKID + " INTEGER, "
+ TrackPointsColumns.LONGITUDE + " INTEGER, "
+ TrackPointsColumns.LATITUDE + " INTEGER, "
+ TrackPointsColumns.TIME + " INTEGER, "
+ TrackPointsColumns.ALTITUDE + " FLOAT, "
+ TrackPointsColumns.ACCURACY + " FLOAT, "
+ TrackPointsColumns.SPEED + " FLOAT, "
+ TrackPointsColumns.BEARING + " FLOAT, "
+ TrackPointsColumns.SENSOR + " BLOB);");
db.execSQL("CREATE TABLE " + TRACKS_TABLE + " ("
+ TracksColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
+ TracksColumns.NAME + " STRING, "
+ TracksColumns.DESCRIPTION + " STRING, "
+ TracksColumns.CATEGORY + " STRING, "
+ TracksColumns.STARTID + " INTEGER, "
+ TracksColumns.STOPID + " INTEGER, "
+ TracksColumns.STARTTIME + " INTEGER, "
+ TracksColumns.STOPTIME + " INTEGER, "
+ TracksColumns.NUMPOINTS + " INTEGER, "
+ TracksColumns.TOTALDISTANCE + " FLOAT, "
+ TracksColumns.TOTALTIME + " INTEGER, "
+ TracksColumns.MOVINGTIME + " INTEGER, "
+ TracksColumns.MINLAT + " INTEGER, "
+ TracksColumns.MAXLAT + " INTEGER, "
+ TracksColumns.MINLON + " INTEGER, "
+ TracksColumns.MAXLON + " INTEGER, "
+ TracksColumns.AVGSPEED + " FLOAT, "
+ TracksColumns.AVGMOVINGSPEED + " FLOAT, "
+ TracksColumns.MAXSPEED + " FLOAT, "
+ TracksColumns.MINELEVATION + " FLOAT, "
+ TracksColumns.MAXELEVATION + " FLOAT, "
+ TracksColumns.ELEVATIONGAIN + " FLOAT, "
+ TracksColumns.MINGRADE + " FLOAT, "
+ TracksColumns.MAXGRADE + " FLOAT, "
+ TracksColumns.MAPID + " STRING, "
+ TracksColumns.TABLEID + " STRING);");
db.execSQL("CREATE TABLE " + WAYPOINTS_TABLE + " ("
+ WaypointsColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
+ WaypointsColumns.NAME + " STRING, "
+ WaypointsColumns.DESCRIPTION + " STRING, "
+ WaypointsColumns.CATEGORY + " STRING, "
+ WaypointsColumns.ICON + " STRING, "
+ WaypointsColumns.TRACKID + " INTEGER, "
+ WaypointsColumns.TYPE + " INTEGER, "
+ WaypointsColumns.LENGTH + " FLOAT, "
+ WaypointsColumns.DURATION + " INTEGER, "
+ WaypointsColumns.STARTTIME + " INTEGER, "
+ WaypointsColumns.STARTID + " INTEGER, "
+ WaypointsColumns.STOPID + " INTEGER, "
+ WaypointsColumns.LONGITUDE + " INTEGER, "
+ WaypointsColumns.LATITUDE + " INTEGER, "
+ WaypointsColumns.TIME + " INTEGER, "
+ WaypointsColumns.ALTITUDE + " FLOAT, "
+ WaypointsColumns.ACCURACY + " FLOAT, "
+ WaypointsColumns.SPEED + " FLOAT, "
+ WaypointsColumns.BEARING + " FLOAT, "
+ WaypointsColumns.TOTALDISTANCE + " FLOAT, "
+ WaypointsColumns.TOTALTIME + " INTEGER, "
+ WaypointsColumns.MOVINGTIME + " INTEGER, "
+ WaypointsColumns.AVGSPEED + " FLOAT, "
+ WaypointsColumns.AVGMOVINGSPEED + " FLOAT, "
+ WaypointsColumns.MAXSPEED + " FLOAT, "
+ WaypointsColumns.MINELEVATION + " FLOAT, "
+ WaypointsColumns.MAXELEVATION + " FLOAT, "
+ WaypointsColumns.ELEVATIONGAIN + " FLOAT, "
+ WaypointsColumns.MINGRADE + " FLOAT, "
+ WaypointsColumns.MAXGRADE + " FLOAT);");
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
if (oldVersion < 17) {
// Wipe the old data.
Log.w(TAG, "Upgrading database from version " + oldVersion + " to "
+ newVersion + ", which will destroy all old data");
db.execSQL("DROP TABLE IF EXISTS " + TRACKPOINTS_TABLE);
db.execSQL("DROP TABLE IF EXISTS " + TRACKS_TABLE);
db.execSQL("DROP TABLE IF EXISTS " + WAYPOINTS_TABLE);
onCreate(db);
} else {
// Incremental updates go here.
// Each time you increase the DB version, add a corresponding if clause.
Log.w(TAG, "Upgrading database from version " + oldVersion + " to "
+ newVersion);
// Sensor data.
if (oldVersion <= 17) {
Log.w(TAG, "Upgrade DB: Adding sensor column.");
db.execSQL("ALTER TABLE " + TRACKPOINTS_TABLE
+ " ADD " + TrackPointsColumns.SENSOR + " BLOB");
}
if (oldVersion <= 18) {
Log.w(TAG, "Upgrade DB: Adding tableid column.");
db.execSQL("ALTER TABLE " + TRACKS_TABLE
+ " ADD " + TracksColumns.TABLEID + " STRING");
}
}
}
}
private final UriMatcher urlMatcher;
private SQLiteDatabase db;
public MyTracksProvider() {
urlMatcher = new UriMatcher(UriMatcher.NO_MATCH);
urlMatcher.addURI(MyTracksProviderUtils.AUTHORITY,
"trackpoints", TRACKPOINTS);
urlMatcher.addURI(MyTracksProviderUtils.AUTHORITY,
"trackpoints/#", TRACKPOINTS_ID);
urlMatcher.addURI(MyTracksProviderUtils.AUTHORITY, "tracks", TRACKS);
urlMatcher.addURI(MyTracksProviderUtils.AUTHORITY, "tracks/#", TRACKS_ID);
urlMatcher.addURI(MyTracksProviderUtils.AUTHORITY, "waypoints", WAYPOINTS);
urlMatcher.addURI(MyTracksProviderUtils.AUTHORITY,
"waypoints/#", WAYPOINTS_ID);
}
private boolean canAccess() {
if (Binder.getCallingPid() == Process.myPid()) {
return true;
} else {
Context context = getContext();
SharedPreferences sharedPreferences = context.getSharedPreferences(
Constants.SETTINGS_NAME, 0);
return sharedPreferences.getBoolean(context.getString(R.string.allow_access_key), false);
}
}
@Override
public boolean onCreate() {
if (!canAccess()) {
return false;
}
DatabaseHelper dbHelper = new DatabaseHelper(getContext());
try {
db = dbHelper.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 (urlMatcher.match(url)) {
case TRACKPOINTS:
table = TRACKPOINTS_TABLE;
break;
case TRACKS:
table = TRACKS_TABLE;
shouldVacuum = true;
break;
case WAYPOINTS:
table = WAYPOINTS_TABLE;
break;
default:
throw new IllegalArgumentException("Unknown URL " + url);
}
Log.w(MyTracksProvider.TAG, "provider delete in " + table + "!");
int count = db.delete(table, where, selectionArgs);
getContext().getContentResolver().notifyChange(url, null, true);
if (shouldVacuum) {
// If a potentially large amount of data was deleted, we want to 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 (urlMatcher.match(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;
}
Log.d(MyTracksProvider.TAG, "MyTracksProvider.insert");
ContentValues values;
if (initialValues != null) {
values = initialValues;
} else {
values = new ContentValues();
}
int urlMatchType = urlMatcher.match(url);
return insertType(url, urlMatchType, values);
}
private Uri insertType(Uri url, int urlMatchType, ContentValues values) {
switch (urlMatchType) {
case TRACKPOINTS:
return insertTrackPoint(url, values);
case TRACKS:
return insertTrack(url, values);
case WAYPOINTS:
return insertWaypoint(url, values);
default:
throw new IllegalArgumentException("Unknown URL " + url);
}
}
@Override
public int bulkInsert(Uri url, ContentValues[] valuesBulk) {
if (!canAccess()) {
return 0;
}
Log.d(MyTracksProvider.TAG, "MyTracksProvider.bulkInsert");
int numInserted = 0;
try {
// Use a transaction in order to make the insertions run as a single batch
db.beginTransaction();
int urlMatch = urlMatcher.match(url);
for (numInserted = 0; numInserted < valuesBulk.length; numInserted++) {
ContentValues values = valuesBulk[numInserted];
if (values == null) { values = new ContentValues(); }
insertType(url, urlMatch, values);
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
return numInserted;
}
private Uri insertTrackPoint(Uri url, ContentValues values) {
boolean hasLat = values.containsKey(TrackPointsColumns.LATITUDE);
boolean hasLong = values.containsKey(TrackPointsColumns.LONGITUDE);
boolean hasTime = values.containsKey(TrackPointsColumns.TIME);
if (!hasLat || !hasLong || !hasTime) {
throw new IllegalArgumentException(
"Latitude, longitude, and time values are required.");
}
long rowId = db.insert(TRACKPOINTS_TABLE, TrackPointsColumns._ID, values);
if (rowId >= 0) {
Uri uri = ContentUris.appendId(
TrackPointsColumns.CONTENT_URI.buildUpon(), rowId).build();
getContext().getContentResolver().notifyChange(url, null, true);
return uri;
}
throw new SQLiteException("Failed to insert row into " + url);
}
private Uri insertTrack(Uri url, ContentValues values) {
boolean hasStartTime = values.containsKey(TracksColumns.STARTTIME);
boolean hasStartId = values.containsKey(TracksColumns.STARTID);
if (!hasStartTime || !hasStartId) {
throw new IllegalArgumentException(
"Both start time and start id values are required.");
}
long rowId = db.insert(TRACKS_TABLE, TracksColumns._ID, values);
if (rowId > 0) {
Uri uri = ContentUris.appendId(
TracksColumns.CONTENT_URI.buildUpon(), rowId).build();
getContext().getContentResolver().notifyChange(url, null, true);
return uri;
}
throw new SQLException("Failed to insert row into " + url);
}
private Uri insertWaypoint(Uri url, ContentValues values) {
long rowId = db.insert(WAYPOINTS_TABLE, WaypointsColumns._ID, values);
if (rowId > 0) {
Uri uri = ContentUris.appendId(
WaypointsColumns.CONTENT_URI.buildUpon(), rowId).build();
getContext().getContentResolver().notifyChange(url, null, true);
return uri;
}
throw new SQLException("Failed to insert row into " + url);
}
@Override
public Cursor query(
Uri url, String[] projection, String selection, String[] selectionArgs,
String sort) {
if (!canAccess()) {
return null;
}
SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
int match = urlMatcher.match(url);
String sortOrder = null;
if (match == TRACKPOINTS) {
qb.setTables(TRACKPOINTS_TABLE);
if (sort != null) {
sortOrder = sort;
} else {
sortOrder = TrackPointsColumns.DEFAULT_SORT_ORDER;
}
} else if (match == TRACKPOINTS_ID) {
qb.setTables(TRACKPOINTS_TABLE);
qb.appendWhere("_id=" + url.getPathSegments().get(1));
} else if (match == TRACKS) {
qb.setTables(TRACKS_TABLE);
if (sort != null) {
sortOrder = sort;
} else {
sortOrder = TracksColumns.DEFAULT_SORT_ORDER;
}
} else if (match == TRACKS_ID) {
qb.setTables(TRACKS_TABLE);
qb.appendWhere("_id=" + url.getPathSegments().get(1));
} else if (match == WAYPOINTS) {
qb.setTables(WAYPOINTS_TABLE);
if (sort != null) {
sortOrder = sort;
} else {
sortOrder = WaypointsColumns.DEFAULT_SORT_ORDER;
}
} else if (match == WAYPOINTS_ID) {
qb.setTables(WAYPOINTS_TABLE);
qb.appendWhere("_id=" + url.getPathSegments().get(1));
} else {
throw new IllegalArgumentException("Unknown URL " + url);
}
if (ApiFeatures.getInstance().canReuseSQLiteQueryBuilder()) {
Log.i(Constants.TAG,
"Build query: " + qb.buildQuery(projection, selection, selectionArgs,
null, null, sortOrder, null));
}
Cursor c = qb.query(db, projection, selection, selectionArgs, null, null,
sortOrder);
c.setNotificationUri(getContext().getContentResolver(), url);
return c;
}
@Override
public int update(Uri url, ContentValues values, String where,
String[] selectionArgs) {
if (!canAccess()) {
return 0;
}
int count;
int match = urlMatcher.match(url);
if (match == TRACKPOINTS) {
count = db.update(TRACKPOINTS_TABLE, values, where, selectionArgs);
} else if (match == TRACKPOINTS_ID) {
String segment = url.getPathSegments().get(1);
count = db.update(TRACKPOINTS_TABLE, values, "_id=" + segment
+ (!TextUtils.isEmpty(where)
? " AND (" + where + ')'
: ""),
selectionArgs);
} else if (match == TRACKS) {
count = db.update(TRACKS_TABLE, values, where, selectionArgs);
} else if (match == TRACKS_ID) {
String segment = url.getPathSegments().get(1);
count = db.update(TRACKS_TABLE, values, "_id=" + segment
+ (!TextUtils.isEmpty(where)
? " AND (" + where + ')'
: ""),
selectionArgs);
} else if (match == WAYPOINTS) {
count = db.update(WAYPOINTS_TABLE, values, where, selectionArgs);
} else if (match == WAYPOINTS_ID) {
String segment = url.getPathSegments().get(1);
count = db.update(WAYPOINTS_TABLE, values, "_id=" + segment
+ (!TextUtils.isEmpty(where)
? " AND (" + where + ')'
: ""),
selectionArgs);
} else {
throw new IllegalArgumentException("Unknown URL " + url);
}
getContext().getContentResolver().notifyChange(url, null, true);
return count;
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,150 @@
/*
* Copyright 2011 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.content;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.Waypoint;
import android.location.Location;
/**
* Listener for track data, for both initial and incremental loading.
*
* @author Rodrigo Damazio
*/
public interface TrackDataListener {
/** States for the GPS location provider. */
public enum ProviderState {
DISABLED,
NO_FIX,
BAD_FIX,
GOOD_FIX;
}
/**
* Called when the location provider changes state.
*/
void onProviderStateChange(ProviderState state);
/**
* Called when the current location changes.
* This is meant for immediate location display only - track point data is
* delivered by other methods below, such as {@link #onNewTrackPoint}.
*
* @param loc the last known location
*/
void onCurrentLocationChanged(Location loc);
/**
* Called when the current heading changes.
*
* @param heading the current heading, already accounting magnetic declination
*/
void onCurrentHeadingChanged(double heading);
/**
* Called when the currently-selected track changes.
* This will be followed by calls to data methods such as
* {@link #onTrackUpdated}, {@link #clearTrackPoints},
* {@link #onNewTrackPoint(Location)}, etc., even if no track is currently
* selected (in which case you'll only get calls to clear the current data).
*
* @param track the selected track, or null if no track is selected
* @param isRecording whether we're currently recording the selected track
*/
void onSelectedTrackChanged(Track track, boolean isRecording);
/**
* Called when the track and/or its statistics have been updated.
*
* @param track the updated version of the track
*/
void onTrackUpdated(Track track);
/**
* Called to clear any previously-sent track points.
* This can be called at any time that we decide the data needs to be
* reloaded, such as when it needs to be resampled.
*/
void clearTrackPoints();
/**
* Called when a new interesting track point is read.
* In this case, interesting means that the point has already undergone
* sampling and invalid point filtering.
*
* @param loc the new track point
*/
void onNewTrackPoint(Location loc);
/**
* Called when a uninteresting track point is read.
* Uninteresting points are all points that get sampled out of the track.
*
* @param loc the new track point
*/
void onSampledOutTrackPoint(Location loc);
/**
* Called when an invalid point (representing a segment split) is read.
*/
void onSegmentSplit();
/**
* Called when we're done (for the time being) sending new points.
* This gets called after every batch of calls to {@link #onNewTrackPoint},
* {@link #onSampledOutTrackPoint} and {@link #onSegmentSplit}.
*/
void onNewTrackPointsDone();
/**
* Called to clear any previously-sent waypoints.
* This can be called at any time that we decide the data needs to be
* reloaded.
*/
void clearWaypoints();
/**
* Called when a new waypoint is read.
*
* @param wpt the new waypoint
*/
void onNewWaypoint(Waypoint wpt);
/**
* Called when we're done (for the time being) sending new waypoints.
* This gets called after every batch of calls to {@link #clearWaypoints} and
* {@link #onNewWaypoint}.
*/
void onNewWaypointsDone();
/**
* Called when the display units are changed by the user.
*
* @param metric true if the units are metric, false if imperial
* @return true to reload all the data, false otherwise
*/
boolean onUnitsChanged(boolean metric);
/**
* Called when the speed/pace display unit is changed by the user.
*
* @param reportSpeed true to report speed, false for pace
* @return true to reload all the data, false otherwise
*/
boolean onReportSpeedChanged(boolean reportSpeed);
}
@@ -0,0 +1,177 @@
/*
* Copyright 2011 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.content;
import static com.google.android.apps.mytracks.Constants.TAG;
import com.google.android.apps.mytracks.content.TrackDataHub.ListenerDataType;
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;
import java.util.WeakHashMap;
/**
* Manager for the external data listeners and their listening types.
*
* @author Rodrigo Damazio
*/
class TrackDataListeners {
/** Internal representation of a listener's registration. */
static class ListenerRegistration {
final TrackDataListener listener;
final EnumSet<ListenerDataType> types;
// State that was last notified to the listener, for resuming after a pause.
long lastTrackId;
long lastPointId;
int lastSamplingFrequency;
int numLoadedPoints;
public ListenerRegistration(TrackDataListener listener,
EnumSet<ListenerDataType> types) {
this.listener = listener;
this.types = types;
}
public boolean isInterestedIn(ListenerDataType type) {
return types.contains(type);
}
public void resetState() {
lastTrackId = 0L;
lastPointId = 0L;
lastSamplingFrequency = 0;
numLoadedPoints = 0;
}
@Override
public String toString() {
return "ListenerRegistration [listener=" + listener + ", types=" + types
+ ", lastTrackId=" + lastTrackId + ", lastPointId=" + lastPointId
+ ", lastSamplingFrequency=" + lastSamplingFrequency
+ ", numLoadedPoints=" + numLoadedPoints + "]";
}
}
/** Map of external listener to its registration details. */
private final Map<TrackDataListener, ListenerRegistration> registeredListeners =
new HashMap<TrackDataListener, ListenerRegistration>();
/**
* Map of external paused listener to its registration details.
* This will automatically discard listeners which are GCed.
*/
private final WeakHashMap<TrackDataListener, ListenerRegistration> oldListeners =
new WeakHashMap<TrackDataListener, ListenerRegistration>();
/** Map of data type to external listeners interested in it. */
private final Map<ListenerDataType, Set<TrackDataListener>> listenerSetsPerType =
new EnumMap<ListenerDataType, Set<TrackDataListener>>(ListenerDataType.class);
public TrackDataListeners() {
// Create sets for all data types at startup.
for (ListenerDataType type : ListenerDataType.values()) {
listenerSetsPerType.put(type, new LinkedHashSet<TrackDataListener>());
}
}
/**
* Registers a listener to send data to.
* It is ok to call this method before {@link start}, and in that case
* the data will only be passed to listeners when {@link start} is called.
*
* @param listener the listener to register
* @param dataTypes the type of data that the listener is interested in
*/
public ListenerRegistration registerTrackDataListener(final TrackDataListener listener, EnumSet<ListenerDataType> dataTypes) {
Log.d(TAG, "Registered track data listener: " + listener);
if (registeredListeners.containsKey(listener)) {
throw new IllegalStateException("Listener already registered");
}
ListenerRegistration registration = oldListeners.remove(listener);
if (registration == null) {
registration = new ListenerRegistration(listener, dataTypes);
}
registeredListeners.put(listener, registration);
for (ListenerDataType type : dataTypes) {
// This is guaranteed not to be null.
Set<TrackDataListener> typeSet = listenerSetsPerType.get(type);
typeSet.add(listener);
}
return registration;
}
/**
* Unregisters a listener to send data to.
*
* @param listener the listener to unregister
*/
public void unregisterTrackDataListener(TrackDataListener listener) {
Log.d(TAG, "Unregistered track data listener: " + listener);
// Remove and keep the corresponding registration.
ListenerRegistration match = registeredListeners.remove(listener);
if (match == null) {
Log.w(TAG, "Tried to unregister listener which is not registered.");
return;
}
// Remove it from the per-type sets
for (ListenerDataType type : match.types) {
listenerSetsPerType.get(type).remove(listener);
}
// Keep it around in case it's re-registered soon
oldListeners.put(listener, match);
}
public ListenerRegistration getRegistration(TrackDataListener listener) {
ListenerRegistration registration = registeredListeners.get(listener);
if (registration == null) {
registration = oldListeners.get(listener);
}
return registration;
}
public Set<TrackDataListener> getListenersFor(ListenerDataType type) {
return listenerSetsPerType.get(type);
}
public EnumSet<ListenerDataType> getAllRegisteredTypes() {
EnumSet<ListenerDataType> listeners = EnumSet.noneOf(ListenerDataType.class);
for (ListenerRegistration registration : this.registeredListeners.values()) {
listeners.addAll(registration.types);
}
return listeners;
}
public boolean hasListeners() {
return !registeredListeners.isEmpty();
}
public int getNumListeners() {
return registeredListeners.size();
}
}
@@ -0,0 +1,93 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io;
import android.content.Intent;
/**
* This interface describes a class that will fetch and maintain a Google
* authentication token.
*
* @author Sandor Dornbush
*/
public interface AuthManager {
/**
* Callback for authentication token retrieval operations.
*/
public interface AuthCallback {
/**
* Indicates that we're done fetching an auth token.
*
* @param success if true, indicates we have the requested auth token available
* to be retrieved using {@link AuthManager#getAuthToken}
*/
void onAuthResult(boolean success);
}
/**
* Initializes the login process. The user should be asked to login if they
* haven't already. The {@link AuthCallback} provided will be executed when the
* auth token fetching is done (successfully or not).
*
* @param whenFinished A {@link AuthCallback} to execute when the auth token
* fetching is done
*/
void doLogin(AuthCallback whenFinished, Object o);
/**
* The {@link android.app.Activity} owner of this class should call this
* function when it gets {@link android.app.Activity#onActivityResult} with
* the request code passed into the constructor. The resultCode and results
* should come directly from the {@link android.app.Activity#onActivityResult}
* function. This function will return true if an auth token was successfully
* fetched or the process is not finished.
*
* @param resultCode The result code passed in to the
* {@link android.app.Activity}'s
* {@link android.app.Activity#onActivityResult} function
* @param results The data passed in to the {@link android.app.Activity}'s
* {@link android.app.Activity#onActivityResult} function
*/
void authResult(int resultCode, Intent results);
/**
* Returns the current auth token. Response may be null if no valid auth
* token has been fetched.
*
* @return The current auth token or null if no auth token has been
* fetched
*/
String getAuthToken();
/**
* Invalidates the existing auth token and request a new one. The
* {@link Runnable} provided will be executed when the new auth token is
* successfully fetched.
*
* @param whenFinished A {@link Runnable} to execute when a new auth token
* is successfully fetched
*/
void invalidateAndRefresh(AuthCallback whenFinished);
/**
* Returns an object that represents the given account, if possible.
*
* @param accountName the name of the account
* @param accountType the type of the account
* @return the account object
*/
Object getAccountObject(String accountName, String accountType);
}
@@ -0,0 +1,57 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io;
import static com.google.android.apps.mytracks.Constants.TAG;
import android.app.Activity;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
/**
* A factory for getting the platform specific AuthManager.
*
* @author Sandor Dornbush
*/
public class AuthManagerFactory {
private AuthManagerFactory() {
}
/**
* Returns whether the modern AuthManager should be used
*/
public static boolean useModernAuthManager() {
return Integer.parseInt(Build.VERSION.SDK) >= 7;
}
/**
* Get a right {@link AuthManager} for the platform.
* @return A new AuthManager
*/
public static AuthManager getAuthManager(Activity activity, int code,
Bundle extras, boolean requireGoogle, String service) {
if (useModernAuthManager()) {
Log.i(TAG, "Creating modern auth manager: " + service);
return new ModernAuthManager(activity, service);
} else {
Log.i(TAG, "Creating legacy auth manager: " + service);
return new AuthManagerOld(activity, code, extras, requireGoogle, service);
}
}
}
@@ -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.io;
import com.google.android.googlelogindist.GoogleLoginServiceConstants;
import com.google.android.googlelogindist.GoogleLoginServiceHelper;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
/**
* AuthManager keeps track of the current auth token for a user. The advantage
* over just passing around a String is that this class can renew the auth
* token if necessary, and it will change for all classes using this
* AuthManager.
*/
public class AuthManagerOld implements AuthManager {
/** The activity that will handle auth result callbacks. */
private final Activity activity;
/** The code used to tell the activity that it is an auth result. */
private final int code;
/** Extras to pass into the getCredentials function. */
private final Bundle extras;
/** True if the account must be a Google account (not a domain account). */
private final boolean requireGoogle;
/** The name of the service to authorize for. */
private final String service;
/** The handler to call when a new auth token is fetched. */
private AuthCallback authCallback;
/** The most recently fetched auth token or null if none is available. */
private String authToken;
/**
* AuthManager requires many of the same parameters as
* {@link GoogleLoginServiceHelper#getCredentials(Activity, int, Bundle,
* boolean, String, boolean)}. The activity must have
* a handler in {@link Activity#onActivityResult} that calls
* {@link #authResult(int, Intent)} if the request code is the code given
* here.
*
* @param activity An activity with a handler in
* {@link Activity#onActivityResult} that calls
* {@link #authResult(int, Intent)} when {@literal code} is the request
* code
* @param code The request code to pass to
* {@link Activity#onActivityResult} when
* {@link #authResult(int, Intent)} should be called
* @param extras A {@link Bundle} of extras for
* {@link GoogleLoginServiceHelper}
* @param requireGoogle True if the account must be a Google account
* @param service The name of the service to authenticate as
*/
public AuthManagerOld(Activity activity, int code, Bundle extras,
boolean requireGoogle, String service) {
this.activity = activity;
this.code = code;
this.extras = extras;
this.requireGoogle = requireGoogle;
this.service = service;
}
@Override
public void doLogin(AuthCallback callback, Object o) {
authCallback = callback;
activity.runOnUiThread(new LoginRunnable());
}
/**
* Runnable which actually gets login credentials.
*/
private class LoginRunnable implements Runnable {
@Override
public void run() {
GoogleLoginServiceHelper.getCredentials(
activity, code, extras, requireGoogle, service, true);
}
}
@Override
public void authResult(int resultCode, Intent results) {
if (resultCode != Activity.RESULT_OK) {
runAuthCallback(false);
return;
}
authToken = results.getStringExtra(GoogleLoginServiceConstants.AUTHTOKEN_KEY);
if (authToken == null) {
// Retry, without prompting the user.
GoogleLoginServiceHelper.getCredentials(
activity, code, extras, requireGoogle, service, false);
} else {
// Notify all active listeners that we have a new auth token.
runAuthCallback(true);
}
}
private void runAuthCallback(boolean success) {
authCallback.onAuthResult(success);
authCallback = null;
}
@Override
public String getAuthToken() {
return authToken;
}
@Override
public void invalidateAndRefresh(AuthCallback callback) {
authCallback = callback;
activity.runOnUiThread(new Runnable() {
public void run() {
GoogleLoginServiceHelper.invalidateAuthToken(activity, code, authToken);
}
});
}
@Override
public Object getAccountObject(String accountName, String accountType) {
throw new UnsupportedOperationException("Legacy auth manager knows nothing about accounts");
}
}
@@ -0,0 +1,221 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io;
import static com.google.android.apps.mytracks.Constants.ACCOUNT_TYPE;
import static com.google.android.apps.mytracks.Constants.TAG;
import com.google.android.apps.mytracks.AccountChooser;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.AccountManagerCallback;
import android.accounts.AccountManagerFuture;
import android.accounts.AuthenticatorException;
import android.accounts.OperationCanceledException;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import java.io.IOException;
/**
* AuthManager keeps track of the current auth token for a user. The advantage
* over just passing around a String is that this class can renew the auth
* token if necessary, and it will change for all classes using this
* AuthManager.
*/
public class ModernAuthManager implements AuthManager {
/** The activity that will handle auth result callbacks. */
private final Activity activity;
/** The name of the service to authorize for. */
private final String service;
/** The most recently fetched auth token or null if none is available. */
private String authToken;
private final AccountManager accountManager;
private AuthCallback authCallback;
private Account lastAccount;
/**
* AuthManager requires many of the same parameters as
* {@link com.google.android.googlelogindist.GoogleLoginServiceHelper
* #getCredentials(Activity, int, Bundle, boolean, String, boolean)}.
* The activity must have a handler in {@link Activity#onActivityResult} that
* calls {@link #authResult(int, Intent)} if the request code is the code
* given here.
*
* @param activity An activity with a handler in
* {@link Activity#onActivityResult} that calls
* {@link #authResult(int, Intent)} when {@literal code} is the request
* code
* @param service The name of the service to authenticate as
*/
public ModernAuthManager(Activity activity, String service) {
this.activity = activity;
this.service = service;
this.accountManager = AccountManager.get(activity);
}
/**
* Call this to do the initial login. The user will be asked to login if
* they haven't already. The {@link Runnable} provided will be executed
* when the auth token is successfully fetched.
*
* @param runnable A {@link Runnable} to execute when the auth token
* has been successfully fetched and is available via
* {@link #getAuthToken()}
*/
public void doLogin(AuthCallback runnable, Object o) {
this.authCallback = runnable;
if (!(o instanceof Account)) {
throw new IllegalArgumentException("ModernAuthManager requires an account.");
}
Account account = (Account) o;
doLogin(account);
}
private void doLogin(Account account) {
// Keep the account in case we need to retry.
this.lastAccount = account;
// NOTE: Many Samsung phones have a crashing bug in
// AccountManager#getAuthToken(Account, String, boolean, AccountManagerCallback<Bundle>)
// so we use the other version of the method.
// More details here:
// http://forum.xda-developers.com/showthread.php?p=15155487
// http://android.git.kernel.org/?p=platform/frameworks/base.git;a=blob;f=core/java/android/accounts/AccountManagerService.java
accountManager.getAuthToken(account, service, null, activity,
new AccountManagerCallback<Bundle>() {
public void run(AccountManagerFuture<Bundle> future) {
try {
authToken = future.getResult().getString(
AccountManager.KEY_AUTHTOKEN);
Log.i(TAG, "Got auth token");
} catch (OperationCanceledException e) {
Log.e(TAG, "Auth token operation Canceled", e);
} catch (IOException e) {
Log.e(TAG, "Auth token IO exception", e);
} catch (AuthenticatorException e) {
Log.e(TAG, "Authentication Failed", e);
}
runAuthCallback();
}
}, null /* handler */);
}
/**
* The {@link Activity} passed into the constructor should call this
* function when it gets {@link Activity#onActivityResult} with the request
* code passed into the constructor. The resultCode and results should
* come directly from the {@link Activity#onActivityResult} function. This
* function will return true if an auth token was successfully fetched or
* the process is not finished.
*
* @param resultCode The result code passed in to the {@link Activity}'s
* {@link Activity#onActivityResult} function
* @param results The data passed in to the {@link Activity}'s
* {@link Activity#onActivityResult} function
* @return True if the auth token was fetched or we aren't done fetching
* the auth token, or False if there was an error or the request was
* canceled
*/
public void authResult(int resultCode, Intent results) {
boolean retry = false;
if (results == null) {
Log.e(TAG, "No auth token!!");
} else {
authToken = results.getStringExtra(AccountManager.KEY_AUTHTOKEN);
retry = results.getBooleanExtra("retry", false);
}
if (authToken == null && retry) {
Log.i(TAG, "Retrying to get auth result");
doLogin(lastAccount);
return;
}
runAuthCallback();
}
/**
* Returns the current auth token. Response may be null if no valid auth
* token has been fetched.
*
* @return The current auth token or null if no auth token has been
* fetched
*/
public String getAuthToken() {
return authToken;
}
/**
* Invalidates the existing auth token and request a new one. The
* {@link Runnable} provided will be executed when the new auth token is
* successfully fetched.
*
* @param runnable A {@link Runnable} to execute when a new auth token
* is successfully fetched
*/
public void invalidateAndRefresh(final AuthCallback callback) {
this.authCallback = callback;
activity.runOnUiThread(new Runnable() {
public void run() {
accountManager.invalidateAuthToken(ACCOUNT_TYPE, authToken);
authToken = null;
AccountChooser accountChooser = new AccountChooser();
accountChooser.chooseAccount(activity,
new AccountChooser.AccountHandler() {
@Override
public void onAccountSelected(Account account) {
if (account != null) {
doLogin(account);
} else {
runAuthCallback();
}
}
});
}
});
}
private void runAuthCallback() {
lastAccount = null;
if (authCallback != null) {
(new Thread() {
@Override
public void run() {
authCallback.onAuthResult(authToken != null);
authCallback = null;
}
}).start();
}
}
@Override
public Object getAccountObject(String accountName, String accountType) {
return new Account(accountName, accountType);
}
}
@@ -0,0 +1,301 @@
/*
* Copyright 2008 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io;
import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.ProgressIndicator;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.io.docs.DocsHelper;
import com.google.android.apps.mytracks.io.gdata.GDataClientFactory;
import com.google.android.apps.mytracks.io.gdata.GDataWrapper;
import com.google.android.common.gdata.AndroidXmlParserFactory;
import com.google.android.maps.mytracks.R;
import com.google.wireless.gdata.client.GDataClient;
import com.google.wireless.gdata.client.GDataServiceClient;
import com.google.wireless.gdata.docs.DocumentsClient;
import com.google.wireless.gdata.docs.SpreadsheetsClient;
import com.google.wireless.gdata.docs.XmlDocsGDataParserFactory;
import android.app.Activity;
import android.content.SharedPreferences;
import android.util.Log;
import java.io.IOException;
/**
* A helper class used to transmit tracks statistics to Google Docs/Trix.
*
* @author Sandor Dornbush
*/
public class SendToDocs {
/** The GData service name for Google Spreadsheets (aka Trix) */
public static final String GDATA_SERVICE_NAME_TRIX = "wise";
/** The GData service name for the Google Docs Document List */
public static final String GDATA_SERVICE_NAME_DOCLIST = "writely";
private final Activity activity;
private final AuthManager trixAuth;
private final AuthManager docListAuth;
private final ProgressIndicator progressIndicator;
private final boolean metricUnits;
private boolean createdNewSpreadSheet = false;
private boolean success = true;
private String statusMessage = "";
private Runnable onCompletion = null;
public SendToDocs(Activity activity, AuthManager trixAuth,
AuthManager docListAuth, ProgressIndicator progressIndicator) {
this.activity = activity;
this.trixAuth = trixAuth;
this.docListAuth = docListAuth;
this.progressIndicator = progressIndicator;
SharedPreferences preferences = activity.getSharedPreferences(
Constants.SETTINGS_NAME, 0);
if (preferences != null) {
metricUnits =
preferences.getBoolean(activity.getString(R.string.metric_units_key),
true);
} else {
metricUnits = true;
}
}
public void sendToDocs(final long trackId) {
Log.d(Constants.TAG,
"Sending to Google Docs: trackId = " + trackId);
new Thread("SendToGoogleDocs") {
@Override
public void run() {
doUpload(trackId);
}
}.start();
}
private void doUpload(long trackId) {
// TODO
statusMessage = activity.getString(R.string.error_sending_to_fusion_tables);
success = false;
try {
if (trackId == -1) {
Log.w(Constants.TAG, "Cannot get track id.");
return;
}
// Get the track from the provider:
Track track =
MyTracksProviderUtils.Factory.get(activity).getTrack(trackId);
if (track == null) {
Log.w(Constants.TAG, "Cannot get track.");
return;
}
// Transmit track stats via GData feed:
// -------------------------------
Log.d(Constants.TAG, "SendToDocs: Uploading to spreadsheet");
success = uploadToDocs(track);
if (success) {
if (createdNewSpreadSheet) {
statusMessage = activity.getString(
R.string.sending_to_docs_success_new_doc);
} else {
statusMessage = activity.getString(
R.string.sending_to_docs_success_existing_doc);
}
} else {
statusMessage = activity.getString(R.string.error_sending_to_docs);
}
Log.d(Constants.TAG, "SendToDocs: Done.");
} finally {
if (onCompletion != null) {
activity.runOnUiThread(onCompletion);
}
}
}
public boolean wasSuccess() {
return success;
}
public String getStatusMessage() {
return statusMessage;
}
public void setOnCompletion(Runnable onCompletion) {
this.onCompletion = onCompletion;
}
/**
* Uploads the statistics about a track to Google Docs using the docs GData
* feed.
*
* @param track the track
*/
private boolean uploadToDocs(Track track) {
GDataWrapper<GDataServiceClient> docListWrapper = new GDataWrapper<GDataServiceClient>();
docListWrapper.setAuthManager(docListAuth);
docListWrapper.setRetryOnAuthFailure(true);
GDataWrapper<GDataServiceClient> trixWrapper = new GDataWrapper<GDataServiceClient>();
trixWrapper.setAuthManager(trixAuth);
trixWrapper.setRetryOnAuthFailure(true);
DocsHelper docsHelper = new DocsHelper();
GDataClient androidClient = null;
try {
androidClient = GDataClientFactory.getGDataClient(activity);
SpreadsheetsClient gdataClient = new SpreadsheetsClient(androidClient,
new XmlDocsGDataParserFactory(new AndroidXmlParserFactory()));
trixWrapper.setClient(gdataClient);
Log.d(Constants.TAG,
"GData connection prepared: " + this.docListAuth);
String sheetTitle = "My Tracks";
if (track.getCategory() != null && !track.getCategory().equals("")) {
sheetTitle += "-" + track.getCategory();
}
DocumentsClient docsGdataClient = new DocumentsClient(androidClient,
new XmlDocsGDataParserFactory(new AndroidXmlParserFactory()));
docListWrapper.setClient(docsGdataClient);
// First try to find the spreadsheet:
String spreadsheetId = null;
try {
spreadsheetId = docsHelper.requestSpreadsheetId(docListWrapper,
sheetTitle);
} catch (IOException e) {
Log.i(Constants.TAG, "Spreadsheet lookup failed.", e);
return false;
}
if (spreadsheetId == null) {
progressIndicator.setProgressValue(65);
// Waiting a few seconds and trying again. Maybe the server just had a
// hickup (unfortunately that happens quite a lot...).
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
Log.e(Constants.TAG, "Sleep interrupted", e);
}
try {
spreadsheetId = docsHelper.requestSpreadsheetId(docListWrapper,
sheetTitle);
} catch (IOException e) {
Log.i(Constants.TAG, "2nd spreadsheet lookup failed.", e);
return false;
}
}
// We were unable to find an existing spreadsheet, so create a new one.
progressIndicator.setProgressValue(70);
if (spreadsheetId == null) {
Log.i(Constants.TAG, "Creating new spreadsheet: " + sheetTitle);
try {
spreadsheetId = docsHelper.createSpreadsheet(activity, docListWrapper,
sheetTitle);
} catch (IOException e) {
Log.i(Constants.TAG, "Failed to create new spreadsheet "
+ sheetTitle, e);
return false;
}
progressIndicator.setProgressValue(80);
createdNewSpreadSheet = true;
if (spreadsheetId == null) {
progressIndicator.setProgressValue(85);
// The previous creation might have succeeded even though GData
// reported an error. Seems to be a know bug,
// see http://code.google.com/p/gdata-issues/issues/detail?id=929
// Try to find the created spreadsheet:
Log.w(Constants.TAG,
"Create might have failed. Trying to find created document.");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
Log.e(Constants.TAG, "Sleep interrupted", e);
}
try {
spreadsheetId = docsHelper.requestSpreadsheetId(docListWrapper,
sheetTitle);
} catch (IOException e) {
Log.i(Constants.TAG, "Failed create-failed lookup", e);
return false;
}
if (spreadsheetId == null) {
progressIndicator.setProgressValue(87);
// Re-try
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
Log.e(Constants.TAG, "Sleep interrupted", e);
}
try {
spreadsheetId = docsHelper.requestSpreadsheetId(docListWrapper,
sheetTitle);
} catch (IOException e) {
Log.i(Constants.TAG, "Failed create-failed relookup", e);
return false;
}
}
if (spreadsheetId == null) {
Log.i(Constants.TAG,
"Creating new spreadsheet really failed.");
return false;
}
}
}
String worksheetId = null;
try {
worksheetId = docsHelper.getWorksheetId(trixWrapper, spreadsheetId);
if (worksheetId == null) {
throw new IOException("Worksheet ID lookup returned empty");
}
} catch (IOException e) {
Log.i(Constants.TAG, "Looking up worksheet id failed.", e);
return false;
}
progressIndicator.setProgressValue(90);
docsHelper.addTrackRow(activity, trixAuth, spreadsheetId, worksheetId,
track, metricUnits);
Log.i(Constants.TAG, "Done uploading to docs.");
} catch (IOException e) {
Log.e(Constants.TAG, "Unable to upload docs.", e);
return false;
} finally {
if (androidClient != null) {
androidClient.close();
}
}
return true;
}
}
@@ -0,0 +1,713 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io;
import static com.google.android.apps.mytracks.Constants.TAG;
import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.ProgressIndicator;
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.io.gdata.GDataWrapper;
import com.google.android.apps.mytracks.io.gdata.GDataWrapper.QueryFunction;
import com.google.android.apps.mytracks.stats.DoubleBuffer;
import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.apps.mytracks.util.LocationUtils;
import com.google.android.apps.mytracks.util.StringUtils;
import com.google.android.apps.mytracks.util.SystemUtils;
import com.google.android.apps.mytracks.util.UnitConversions;
import com.google.android.maps.mytracks.R;
import com.google.api.client.googleapis.GoogleHeaders;
import com.google.api.client.googleapis.MethodOverride;
import com.google.api.client.http.GenericUrl;
import com.google.api.client.http.HttpRequest;
import com.google.api.client.http.HttpRequestFactory;
import com.google.api.client.http.HttpResponse;
import com.google.api.client.http.HttpResponseException;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.http.InputStreamContent;
import com.google.api.client.util.Strings;
import android.app.Activity;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.location.Location;
import android.util.Log;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Vector;
/**
* A helper class used to transmit tracks to Google Fusion Tables.
* A new instance should be used for each upload.
*
* @author Leif Hendrik Wilden
*/
public class SendToFusionTables implements Runnable {
private static final String CONTENT_TYPE = "application/x-www-form-urlencoded";
/**
* Listener invoked when sending to fusion tables completes.
*/
public interface OnSendCompletedListener {
void onSendCompleted(String tableId, boolean success, int statusMessage);
}
/** The GData service id for Fusion Tables. */
public static final String SERVICE_ID = "fusiontables";
/** The path for viewing a map visualization of a table. */
private static final String FUSIONTABLES_MAP =
"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";
/** Standard base feed url for Fusion Tables. */
private static final String FUSIONTABLES_BASE_FEED_URL =
"https://www.google.com/fusiontables/api/query";
private static final int MAX_POINTS_PER_UPLOAD = 2048;
private static final String GDATA_VERSION = "2";
// This class reports upload status to the user as a completion percentage
// using a progress bar. Progress is defined as follows:
//
// 0% Getting track metadata
// 5% Creating Fusion Table (GData to FT server)
// 10%-90% Uploading the track data to Fusion Tables
// 95% Uploading waypoints
// 100% Done
private static final int PROGRESS_INITIALIZATION = 0;
private static final int PROGRESS_FUSION_TABLE_CREATE = 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;
private final Activity context;
private final AuthManager auth;
private final long trackId;
private final ProgressIndicator progressIndicator;
private final OnSendCompletedListener onCompletion;
private final StringUtils stringUtils;
private final MyTracksProviderUtils providerUtils;
// Progress status
private int totalLocationsRead;
private int totalLocationsPrepared;
private int totalLocationsUploaded;
private int totalLocations;
private int totalSegmentsUploaded;
private HttpRequestFactory httpRequestFactory;
private String tableId;
private static String MARKER_TYPE_START = "large_green";
private static String MARKER_TYPE_END = "large_red";
private static String MARKER_TYPE_WAYPOINT = "large_yellow";
public SendToFusionTables(Activity context, AuthManager auth, long trackId,
ProgressIndicator progressIndicator, OnSendCompletedListener onCompletion) {
this.context = context;
this.auth = auth;
this.trackId = trackId;
this.progressIndicator = progressIndicator;
this.onCompletion = onCompletion;
this.stringUtils = new StringUtils(context);
this.providerUtils = MyTracksProviderUtils.Factory.get(context);
HttpTransport transport = ApiFeatures.getInstance().getApiAdapter().getHttpTransport();
httpRequestFactory = transport.createRequestFactory(new MethodOverride());
}
@Override
public void run() {
Log.d(Constants.TAG, "Sending to Fusion tables: trackId = " + trackId);
doUpload();
}
public static String getMapVisualizationUrl(Track track) {
if (track == null || track.getStatistics() == null || track.getTableId() == null) {
Log.w(TAG, "Unable to get track URL");
return null;
}
// TODO(leifhendrik): Determine correct bounding box and zoom level that will show the entire track.
TripStatistics stats = track.getStatistics();
double latE6 = stats.getBottom() + (stats.getTop() - stats.getBottom()) / 2;
double lonE6 = stats.getLeft() + (stats.getRight() - stats.getLeft()) / 2;
int 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, FUSIONTABLES_MAP, track.getTableId(),
latE6 / 1.E6, lonE6 / 1.E6, z);
}
private void doUpload() {
int statusMessageId = R.string.error_sending_to_fusion_tables;
boolean success = true;
try {
progressIndicator.setProgressValue(PROGRESS_INITIALIZATION);
progressIndicator.setProgressMessage(R.string.progress_message_reading_track);
// Get the track meta-data
Track track = providerUtils.getTrack(trackId);
if (track == null) {
Log.w(Constants.TAG, "Cannot get track.");
return;
}
String originalDescription = track.getDescription();
// Create a new table:
progressIndicator.setProgressValue(PROGRESS_FUSION_TABLE_CREATE);
progressIndicator.setProgressMessage(R.string.progress_message_creating_fusion_table);
if (!createNewTable(track) || !makeTableUnlisted()) {
return;
}
progressIndicator.setProgressValue(PROGRESS_UPLOAD_DATA_MIN);
progressIndicator.setProgressMessage(R.string.progress_message_sending_fusion_tables);
// Upload all of the segments of the track plus start/end markers
if (!uploadAllTrackPoints(track, originalDescription)) {
return;
}
progressIndicator.setProgressValue(PROGRESS_UPLOAD_WAYPOINTS);
// Upload all the waypoints.
if (!uploadWaypoints(track)) {
return;
}
statusMessageId = R.string.sending_to_fusion_tables_success;
Log.d(Constants.TAG, "SendToFusionTables: Done: " + success);
progressIndicator.setProgressValue(PROGRESS_COMPLETE);
} finally {
final boolean finalSuccess = success;
final int finalStatusMessageId = statusMessageId;
context.runOnUiThread(new Runnable() {
public void run() {
if (onCompletion != null) {
onCompletion.onSendCompleted(
tableId, finalSuccess, finalStatusMessageId);
}
}
});
}
}
/**
* Creates a new table.
* If successful sets {@link #tableId}.
*
* @return true in case of success.
*/
private boolean createNewTable(Track track) {
Log.d(Constants.TAG, "Creating a new fusion table.");
String query = "CREATE TABLE '" + sqlEscape(track.getName()) +
"' (name:STRING,description:STRING,geometry:LOCATION,marker:STRING)";
return runUpdate(query);
}
private boolean makeTableUnlisted() {
Log.d(Constants.TAG, "Setting visibility to unlisted.");
String query = "UPDATE TABLE " + tableId + " SET VISIBILITY = UNLISTED";
return runUpdate(query);
}
/**
* Formats given values SQL style. Escapes single quotes with a backslash.
*
* @param values the values to format
* @return the values formatted as: ('value1','value2',...,'value_n').
*/
private static String values(String... values) {
StringBuilder builder = new StringBuilder("(");
for (int i = 0; i < values.length; i++) {
if (i > 0) {
builder.append(',');
}
builder.append('\'');
builder.append(sqlEscape(values[i]));
builder.append('\'');
}
builder.append(')');
return builder.toString();
}
private static String sqlEscape(String value) {
return value.replaceAll("'", "''");
}
/**
* Creates a new row representing a marker.
*
* @param name the marker name
* @param description the marker description
* @param location the marker location
* @return true in case of success.
*/
private boolean createNewPoint(String name, String description, Location location,
String marker) {
Log.d(Constants.TAG, "Creating a new row with a point.");
String query = "INSERT INTO " + tableId + " (name,description,geometry,marker) VALUES "
+ values(name, description, getKmlPoint(location), marker);
return runUpdate(query);
}
/**
* Creates a new row representing a line segment.
*
* @param track the track/segment to draw
* @return true in case of success.
*/
private boolean createNewLineString(Track track) {
Log.d(Constants.TAG, "Creating a new row with a point.");
String query = "INSERT INTO " + tableId
+ " (name,description,geometry) VALUES "
+ values(track.getName(), track.getDescription(), getKmlLineString(track));
return runUpdate(query);
}
private boolean uploadAllTrackPoints(final Track track, String originalDescription) {
SharedPreferences preferences = context.getSharedPreferences(Constants.SETTINGS_NAME, 0);
boolean metricUnits = true;
if (preferences != null) {
metricUnits = preferences.getBoolean(context.getString(R.string.metric_units_key), true);
}
Cursor locationsCursor = providerUtils.getLocationsCursor(track.getId(), 0, -1, false);
try {
if (locationsCursor == null || !locationsCursor.moveToFirst()) {
Log.w(Constants.TAG, "Unable to get any points to upload");
return false;
}
totalLocationsRead = 0;
totalLocationsPrepared = 0;
totalLocationsUploaded = 0;
totalLocations = locationsCursor.getCount();
totalSegmentsUploaded = 0;
// Limit the number of elevation readings. Ideally we would want around 250.
int elevationSamplingFrequency =
Math.max(1, (int) (totalLocations / 250.0));
Log.d(Constants.TAG,
"Using elevation sampling factor: " + elevationSamplingFrequency
+ " on " + totalLocations);
double totalDistance = 0;
Vector<Double> distances = new Vector<Double>();
Vector<Double> elevations = new Vector<Double>();
DoubleBuffer elevationBuffer = new DoubleBuffer(Constants.ELEVATION_SMOOTHING_FACTOR);
List<Location> locations = new ArrayList<Location>(MAX_POINTS_PER_UPLOAD);
Location lastLocation = null;
do {
if (totalLocationsRead % 100 == 0) {
updateTrackDataUploadProgress();
}
Location loc = providerUtils.createLocation(locationsCursor);
locations.add(loc);
if (totalLocationsRead == 0) {
// Put a marker at the first point of the first valid segment:
String name = track.getName() + " " + context.getString(R.string.start);
createNewPoint(name, "", loc, MARKER_TYPE_START);
}
// Add to the elevation profile.
if (loc != null && LocationUtils.isValidLocation(loc)) {
// All points go into the smoothing buffer...
elevationBuffer.setNext(metricUnits ? loc.getAltitude()
: loc.getAltitude() * UnitConversions.M_TO_FT);
if (lastLocation != null) {
double dist = lastLocation.distanceTo(loc);
totalDistance += dist;
}
// ...but only a few points are really used to keep the url short.
if (totalLocationsRead % elevationSamplingFrequency == 0) {
distances.add(totalDistance);
elevations.add(elevationBuffer.getAverage());
}
}
// If the location was not valid, it's a segment split, so make sure the
// distance between the previous segment and the new one is not accounted
// for in the next iteration.
lastLocation = loc;
// Every now and then, upload the accumulated points
if (totalLocationsRead % MAX_POINTS_PER_UPLOAD == MAX_POINTS_PER_UPLOAD - 1) {
if (!prepareAndUploadPoints(track, locations)) {
return false;
}
}
totalLocationsRead++;
} while (locationsCursor.moveToNext());
// Do a final upload with what's left
if (!prepareAndUploadPoints(track, locations)) {
return false;
}
// Put an end marker at the last point of the last valid segment:
if (lastLocation != null) {
track.setDescription("<p>" + originalDescription + "</p><p>"
+ stringUtils.generateTrackDescription(track, distances, elevations)
+ "</p>");
String name = track.getName() + " " + context.getString(R.string.end);
return createNewPoint(name, track.getDescription(), lastLocation, MARKER_TYPE_END);
}
return true;
} finally {
if (locationsCursor != null) {
locationsCursor.close();
}
}
}
/**
* Appends the given location to the string in the format:
* longitude,latitude[,altitude]
*
* @param location the location to be added
* @param builder the string builder to use
*/
private void appendCoordinate(Location location, StringBuilder builder) {
builder
.append(location.getLongitude())
.append(",")
.append(location.getLatitude());
if (location.hasAltitude()) {
builder.append(",");
builder.append(location.getAltitude());
}
}
/**
* Gets a KML Point tag for the given location.
*
* @param location The location.
* @return the kml.
*/
private String getKmlPoint(Location location) {
StringBuilder builder = new StringBuilder("<Point><coordinates>");
appendCoordinate(location, builder);
builder.append("</coordinates></Point>");
return builder.toString();
}
/**
* Returns a KML LineString.
*
* @param track the track.
* @return the KML LineString.
*/
private String getKmlLineString(Track track) {
StringBuilder builder = new StringBuilder("<LineString><coordinates>");
for (Location location : track.getLocations()) {
appendCoordinate(location, builder);
builder.append(' ');
}
builder.append("</coordinates></LineString>");
return builder.toString();
}
private boolean prepareAndUploadPoints(Track track, List<Location> locations) {
updateTrackDataUploadProgress();
int numLocations = locations.size();
if (numLocations < 2) {
Log.d(Constants.TAG, "Not preparing/uploading too few points");
totalLocationsUploaded += numLocations;
return true;
}
// Prepare/pre-process the points
ArrayList<Track> splitTracks = prepareLocations(track, locations);
// Start uploading them
for (Track splitTrack : splitTracks) {
if (totalSegmentsUploaded > 1) {
splitTrack.setName(splitTrack.getName() + " "
+ String.format(
context.getString(R.string.track_part_format), totalSegmentsUploaded));
}
totalSegmentsUploaded++;
Log.d(Constants.TAG,
"SendToFusionTables: Prepared feature for upload w/ "
+ splitTrack.getLocations().size() + " points.");
// Transmit tracks via GData feed:
// -------------------------------
Log.d(Constants.TAG,
"SendToFusionTables: Uploading to table " + tableId + " w/ auth " + auth);
if (!uploadTrackPoints(splitTrack)) {
Log.e(Constants.TAG, "Uploading failed");
return false;
}
}
locations.clear();
totalLocationsUploaded += numLocations;
updateTrackDataUploadProgress();
return true;
}
/**
* Prepares a buffer of locations for transmission to google fusion tables.
*
* @param track the original track with meta data
* @param locations locations on the track
* @return an array of tracks each with a sub section of the points in the
* original buffer
*/
private ArrayList<Track> prepareLocations(
Track track, Iterable<Location> locations) {
ArrayList<Track> splitTracks = new ArrayList<Track>();
// Create segments from each full track:
Track segment = new Track();
TripStatistics segmentStats = segment.getStatistics();
TripStatistics trackStats = track.getStatistics();
segment.setId(track.getId());
segment.setName(track.getName());
segment.setDescription(/* track.getDescription() */ "");
segment.setCategory(track.getCategory());
segmentStats.setStartTime(trackStats.getStartTime());
segmentStats.setStopTime(trackStats.getStopTime());
boolean startNewTrackSegment = false;
for (Location loc : locations) {
if (totalLocationsPrepared % 100 == 0) {
updateTrackDataUploadProgress();
}
if (loc.getLatitude() > 90) {
startNewTrackSegment = true;
}
if (startNewTrackSegment) {
// Close up the last segment.
prepareTrackSegment(segment, splitTracks);
Log.d(Constants.TAG,
"MyTracksSendToFusionTables: Starting new track segment...");
startNewTrackSegment = false;
segment = new Track();
segment.setId(track.getId());
segment.setName(track.getName());
segment.setDescription(/* track.getDescription() */ "");
segment.setCategory(track.getCategory());
}
if (loc.getLatitude() <= 90) {
segment.addLocation(loc);
if (segmentStats.getStartTime() < 0) {
segmentStats.setStartTime(loc.getTime());
}
}
totalLocationsPrepared++;
}
prepareTrackSegment(segment, splitTracks);
return splitTracks;
}
/**
* Prepares a track segment for sending to google fusion tables.
* The main steps are:
* - correcting end time
* - decimating locations
* - splitting into smaller tracks.
*
* The final track pieces will be put in the array list splitTracks.
*
* @param segment the original segment of the track
* @param splitTracks an array of smaller track segments
*/
private void prepareTrackSegment(
Track segment, ArrayList<Track> splitTracks) {
TripStatistics segmentStats = segment.getStatistics();
if (segmentStats.getStopTime() < 0
&& segment.getLocations().size() > 0) {
segmentStats.setStopTime(segment.getLocations().size() - 1);
}
/*
* Decimate to 2 meter precision. Fusion tables doesn't like too many
* points:
*/
LocationUtils.decimate(segment, 2.0);
/* If the track still has > 2500 points, split it in pieces: */
final int maxPoints = 2500;
if (segment.getLocations().size() > maxPoints) {
splitTracks.addAll(LocationUtils.split(segment, maxPoints));
} else if (segment.getLocations().size() >= 2) {
splitTracks.add(segment);
}
}
private boolean uploadTrackPoints(Track splitTrack) {
int numLocations = splitTrack.getLocations().size();
if (numLocations < 2) {
// Need at least two points for a polyline:
Log.w(Constants.TAG, "Not uploading too few points");
return true;
}
return createNewLineString(splitTrack);
}
/**
* Uploads all of the waypoints associated with this track to a table.
*
* @param track The track to upload waypoints for.
*
* @return True on success.
*/
private boolean uploadWaypoints(final Track track) {
// TODO: Stream through the waypoints in chunks.
// I am leaving the number of waypoints very high which should not be a
// problem because we don't try to load them into objects all at the
// same time.
boolean success = true;
Cursor c = null;
try {
c = providerUtils.getWaypointsCursor(
track.getId(), 0,
Constants.MAX_LOADED_WAYPOINTS_POINTS);
if (c != null) {
if (c.moveToFirst()) {
// This will skip the 1st waypoint (it carries the stats for the
// last segment).
while (c.moveToNext()) {
Waypoint wpt = providerUtils.createWaypoint(c);
Log.d(Constants.TAG, "SendToFusionTables: Creating waypoint.");
success = createNewPoint(wpt.getName(), wpt.getDescription(), wpt.getLocation(),
MARKER_TYPE_WAYPOINT);
if (!success) {
break;
}
}
}
}
if (!success) {
Log.w(Constants.TAG, "SendToFusionTables: upload waypoints failed.");
}
return success;
} finally {
if (c != null) {
c.close();
}
}
}
private void updateTrackDataUploadProgress() {
// The percent of the total that represents the completed part of this
// segment. We calculate it as an absolute percentage, and then scale it
// to fit the completion percentage range alloted to track data upload.
double totalPercentage =
(totalLocationsRead + totalLocationsPrepared + totalLocationsUploaded)
/ (totalLocations * 3.0);
double scaledPercentage = totalPercentage
* (PROGRESS_UPLOAD_DATA_MAX - PROGRESS_UPLOAD_DATA_MIN) + PROGRESS_UPLOAD_DATA_MIN;
progressIndicator.setProgressValue((int) scaledPercentage);
}
/**
* Runs an update query. Handles authentication.
*
* @param query The given SQL like query
* @return true in case of success
*/
private boolean runUpdate(final String query) {
GDataWrapper<HttpRequestFactory> wrapper = new GDataWrapper<HttpRequestFactory>();
wrapper.setAuthManager(auth);
wrapper.setRetryOnAuthFailure(true);
wrapper.setClient(httpRequestFactory);
Log.d(Constants.TAG, "GData connection prepared: " + this.auth);
wrapper.runQuery(new QueryFunction<HttpRequestFactory>() {
@Override
public void query(HttpRequestFactory factory)
throws IOException, GDataWrapper.ParseException, GDataWrapper.HttpException,
GDataWrapper.AuthenticationException {
GenericUrl url = new GenericUrl(FUSIONTABLES_BASE_FEED_URL);
String sql = "sql=" + URLEncoder.encode(query, "UTF-8");
ByteArrayInputStream inputStream = new ByteArrayInputStream(Strings.toBytesUtf8(sql));
InputStreamContent isc = new InputStreamContent(null, inputStream );
HttpRequest request = factory.buildPostRequest(url, isc);
GoogleHeaders headers = new GoogleHeaders();
headers.setApplicationName("Google-MyTracks-" + SystemUtils.getMyTracksVersion(context));
headers.gdataVersion = GDATA_VERSION;
headers.setGoogleLogin(auth.getAuthToken());
headers.setContentType(CONTENT_TYPE);
request.setHeaders(headers);
Log.d(Constants.TAG, "Running update query " + url.toString() + ": " + sql);
HttpResponse response;
try {
response = request.execute();
} catch (HttpResponseException e) {
throw new GDataWrapper.HttpException(e.getResponse().getStatusCode(),
e.getResponse().getStatusMessage());
}
boolean success = response.isSuccessStatusCode();
if (success) {
byte[] result = new byte[1024];
int read = response.getContent().read(result);
String s = new String(result, 0, read, "UTF8");
String[] lines = s.split(Strings.LINE_SEPARATOR);
if (lines[0].equals("tableid")) {
tableId = lines[1];
Log.d(Constants.TAG, "tableId = " + tableId);
} else {
Log.w(Constants.TAG, "Unrecognized response: " + lines[0]);
}
} else {
Log.d(Constants.TAG,
"Query failed: " + response.getStatusMessage() + " ("
+ response.getStatusCode() + ")");
throw new GDataWrapper.HttpException(
response.getStatusCode(), response.getStatusMessage());
}
}
});
return wrapper.getErrorType() == GDataWrapper.ERROR_NO_ERROR;
}
}
@@ -0,0 +1,462 @@
/*
* Copyright 2008 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io;
import static com.google.android.apps.mytracks.Constants.TAG;
import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.ProgressIndicator;
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.io.mymaps.MapsFacade;
import com.google.android.apps.mytracks.stats.DoubleBuffer;
import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.android.apps.mytracks.util.LocationUtils;
import com.google.android.apps.mytracks.util.StringUtils;
import com.google.android.apps.mytracks.util.UnitConversions;
import com.google.android.maps.mytracks.R;
import android.app.Activity;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.location.Location;
import android.util.Log;
import java.util.ArrayList;
import java.util.List;
import java.util.Vector;
/**
* A helper class used to transmit tracks to Google MyMaps.
* A new instance should be used for each upload.
*
* IMPORTANT: while this code is Apache-licensed, please notice that usage of
* the Google Maps servers through this API is only allowed for the My Tracks
* application. Other applications looking to upload maps data should look
* into using the Fusion Tables API.
*
* @author Leif Hendrik Wilden
*/
public class SendToMyMaps implements Runnable {
public static final String NEW_MAP_ID = "new";
private static final int MAX_POINTS_PER_UPLOAD = 2048;
private final Activity context;
private final AuthManager auth;
private final long trackId;
private final ProgressIndicator progressIndicator;
private final OnSendCompletedListener onCompletion;
private final StringUtils stringUtils;
private final MyTracksProviderUtils providerUtils;
private String mapId;
private MapsFacade mapsClient;
// Progress status
private int totalLocationsRead;
private int totalLocationsPrepared;
private int totalLocationsUploaded;
private int totalLocations;
private int totalSegmentsUploaded;
public interface OnSendCompletedListener {
void onSendCompleted(String mapId, boolean success, int statusMessage);
}
public SendToMyMaps(Activity context, String mapId, AuthManager auth,
long trackId, ProgressIndicator progressIndicator,
OnSendCompletedListener onCompletion) {
this.context = context;
this.mapId = mapId;
this.auth = auth;
this.trackId = trackId;
this.progressIndicator = progressIndicator;
this.onCompletion = onCompletion;
this.stringUtils = new StringUtils(context);
this.providerUtils = MyTracksProviderUtils.Factory.get(context);
}
@Override
public void run() {
Log.d(TAG, "Sending to MyMaps: trackId = " + trackId);
doUpload();
}
private void doUpload() {
int statusMessageId = R.string.error_sending_to_my_maps;
boolean success = true;
try {
progressIndicator.setProgressMessage(
R.string.progress_message_reading_track);
// Get the track meta-data
Track track = providerUtils.getTrack(trackId);
if (track == null) {
Log.w(Constants.TAG, "Cannot get track.");
return;
}
String originalDescription = track.getDescription();
track.setDescription("<p>" + track.getDescription() + "</p><p>"
+ stringUtils.generateTrackDescription(track, null, null) + "</p>");
mapsClient = new MapsFacade(context, auth);
// Create a new map if necessary:
boolean isNewMap = mapId.equals(NEW_MAP_ID);
if (isNewMap) {
SharedPreferences preferences = context.getSharedPreferences(
Constants.SETTINGS_NAME, 0);
boolean mapPublic = true;
if (preferences != null) {
mapPublic = preferences.getBoolean(
context.getString(R.string.default_map_public_key), true);
}
progressIndicator.setProgressMessage(
R.string.progress_message_creating_map);
StringBuilder mapIdBuilder = new StringBuilder();
success = mapsClient.createNewMap(
track.getName(), track.getCategory(), originalDescription, mapPublic, mapIdBuilder);
mapId = mapIdBuilder.toString();
}
// Upload all of the segments of the track plus start/end markers
if (success) {
success = uploadAllTrackPoints(track, originalDescription);
}
// Put waypoints.
if (success) {
Cursor c = providerUtils.getWaypointsCursor(
track.getId(), 0,
Constants.MAX_LOADED_WAYPOINTS_POINTS);
if (c != null) {
try {
if (c.getCount() > 1 && c.moveToFirst()) {
// This will skip the 1st waypoint (it carries the stats for the
// last segment).
ArrayList<Waypoint> waypoints = new ArrayList<Waypoint>(c.getCount() - 1);
while (c.moveToNext()) {
Waypoint wpt = providerUtils.createWaypoint(c);
waypoints.add(wpt);
}
success = mapsClient.uploadWaypoints(mapId, waypoints);
}
} finally {
c.close();
}
} else {
success = false;
}
if (!success) {
Log.w(TAG, "SendToMyMaps: upload waypoints failed.");
}
}
if (success) {
statusMessageId = isNewMap
? R.string.sending_to_my_maps_success_new_map
: R.string.sending_to_my_maps_success_existing_map;
}
Log.d(TAG, "SendToMyMaps: Done: " + success);
progressIndicator.setProgressValue(100);
} finally {
if (mapsClient != null) {
mapsClient.cleanUp();
}
final boolean finalSuccess = success;
final int finalStatusMessageId = statusMessageId;
context.runOnUiThread(new Runnable() {
public void run() {
if (onCompletion != null) {
onCompletion.onSendCompleted(
mapId, finalSuccess, finalStatusMessageId);
}
}
});
}
}
private boolean uploadAllTrackPoints(
final Track track, String originalDescription) {
SharedPreferences preferences = context.getSharedPreferences(
Constants.SETTINGS_NAME, 0);
boolean metricUnits = true;
if (preferences != null) {
metricUnits =
preferences.getBoolean(context.getString(R.string.metric_units_key),
true);
}
Cursor locationsCursor =
providerUtils.getLocationsCursor(track.getId(), 0, -1, false);
try {
if (locationsCursor == null || !locationsCursor.moveToFirst()) {
Log.w(TAG, "Unable to get any points to upload");
return false;
}
totalLocationsRead = 0;
totalLocationsPrepared = 0;
totalLocationsUploaded = 0;
totalLocations = locationsCursor.getCount();
totalSegmentsUploaded = 0;
// Limit the number of elevation readings. Ideally we would want around 250.
int elevationSamplingFrequency =
Math.max(1, (int) (totalLocations / 250.0));
Log.d(TAG,
"Using elevation sampling factor: " + elevationSamplingFrequency
+ " on " + totalLocations);
double totalDistance = 0;
Vector<Double> distances = new Vector<Double>();
Vector<Double> elevations = new Vector<Double>();
DoubleBuffer elevationBuffer =
new DoubleBuffer(Constants.ELEVATION_SMOOTHING_FACTOR);
List<Location> locations = new ArrayList<Location>(MAX_POINTS_PER_UPLOAD);
progressIndicator.setProgressMessage(
R.string.progress_message_reading_track);
Location lastLocation = null;
do {
if (totalLocationsRead % 100 == 0) {
updateProgress();
}
Location loc = providerUtils.createLocation(locationsCursor);
locations.add(loc);
if (totalLocationsRead == 0) {
// Put a marker at the first point of the first valid segment:
mapsClient.uploadMarker(mapId, track.getName(), track.getDescription(), loc, true);
}
// Add to the elevation profile.
if (loc != null && LocationUtils.isValidLocation(loc)) {
// All points go into the smoothing buffer...
elevationBuffer.setNext(metricUnits ? loc.getAltitude()
: loc.getAltitude() * UnitConversions.M_TO_FT);
if (lastLocation != null) {
double dist = lastLocation.distanceTo(loc);
totalDistance += dist;
}
// ...but only a few points are really used to keep the url short.
if (totalLocationsRead % elevationSamplingFrequency == 0) {
distances.add(totalDistance);
elevations.add(elevationBuffer.getAverage());
}
}
// If the location was not valid, it's a segment split, so make sure the
// distance between the previous segment and the new one is not accounted
// for in the next iteration.
lastLocation = loc;
// Every now and then, upload the accumulated points
if (totalLocationsRead % MAX_POINTS_PER_UPLOAD
== MAX_POINTS_PER_UPLOAD - 1) {
if (!prepareAndUploadPoints(track, locations)) {
return false;
}
}
totalLocationsRead++;
} while (locationsCursor.moveToNext());
// Do a final upload with what's left
if (!prepareAndUploadPoints(track, locations)) {
return false;
}
// Put an end marker at the last point of the last valid segment:
if (lastLocation != null) {
track.setDescription("<p>" + originalDescription + "</p><p>"
+ stringUtils.generateTrackDescription(
track, distances, elevations)
+ "</p>");
return mapsClient.uploadMarker(mapId, track.getName(), track.getDescription(),
lastLocation, false);
}
return true;
} finally {
if (locationsCursor != null) {
locationsCursor.close();
}
}
}
private boolean prepareAndUploadPoints(Track track, List<Location> locations) {
progressIndicator.setProgressMessage(
R.string.progress_message_preparing_track);
updateProgress();
int numLocations = locations.size();
if (numLocations < 2) {
Log.d(TAG, "Not preparing/uploading too few points");
totalLocationsUploaded += numLocations;
return true;
}
// Prepare/pre-process the points
ArrayList<Track> splitTracks = prepareLocations(track, locations);
// Start uploading them
progressIndicator.setProgressMessage(
R.string.progress_message_sending_my_maps);
for (Track splitTrack : splitTracks) {
if (totalSegmentsUploaded > 1) {
splitTrack.setName(splitTrack.getName() + " "
+ String.format(
context.getString(R.string.track_part_format), totalSegmentsUploaded));
}
totalSegmentsUploaded++;
Log.d(TAG,
"SendToMyMaps: Prepared feature for upload w/ "
+ splitTrack.getLocations().size() + " points.");
// Transmit tracks via GData feed:
// -------------------------------
Log.d(TAG,
"SendToMyMaps: Uploading to map " + mapId + " w/ auth " + auth);
if (!mapsClient.uploadTrackPoints(mapId, splitTrack.getName(), splitTrack.getLocations())) {
Log.e(TAG, "Uploading failed");
return false;
}
}
locations.clear();
totalLocationsUploaded += numLocations;
updateProgress();
return true;
}
/**
* Prepares a buffer of locations for transmission to google maps.
*
* @param track the original track with meta data
* @param buffer a buffer of locations on the track
* @return an array of tracks each with a sub section of the points in the
* original buffer
*/
private ArrayList<Track> prepareLocations(
Track track, Iterable<Location> locations) {
ArrayList<Track> splitTracks = new ArrayList<Track>();
// Create segments from each full track:
Track segment = new Track();
TripStatistics segmentStats = segment.getStatistics();
TripStatistics trackStats = track.getStatistics();
segment.setId(track.getId());
segment.setName(track.getName());
segment.setDescription(/* track.getDescription() */ "");
segment.setCategory(track.getCategory());
segmentStats.setStartTime(trackStats.getStartTime());
segmentStats.setStopTime(trackStats.getStopTime());
boolean startNewTrackSegment = false;
for (Location loc : locations) {
if (totalLocationsPrepared % 100 == 0) {
updateProgress();
}
if (loc.getLatitude() > 90) {
startNewTrackSegment = true;
}
if (startNewTrackSegment) {
// Close up the last segment.
prepareTrackSegment(segment, splitTracks);
Log.d(TAG,
"MyTracksSendToMyMaps: Starting new track segment...");
startNewTrackSegment = false;
segment = new Track();
segment.setId(track.getId());
segment.setName(track.getName());
segment.setDescription(/* track.getDescription() */ "");
segment.setCategory(track.getCategory());
}
if (loc.getLatitude() <= 90) {
segment.addLocation(loc);
if (segmentStats.getStartTime() < 0) {
segmentStats.setStartTime(loc.getTime());
}
}
totalLocationsPrepared++;
}
prepareTrackSegment(segment, splitTracks);
return splitTracks;
}
/**
* Prepares a track segment for sending to google maps.
* The main steps are:
* - correcting end time
* - decimating locations
* - splitting into smaller tracks.
*
* The final track pieces will be put in the array list splitTracks.
*
* @param segment the original segment of the track
* @param splitTracks an array of smaller track segments
*/
private void prepareTrackSegment(
Track segment, ArrayList<Track> splitTracks) {
TripStatistics segmentStats = segment.getStatistics();
if (segmentStats.getStopTime() < 0
&& segment.getLocations().size() > 0) {
segmentStats.setStopTime(segment.getLocations().size() - 1);
}
/*
* Decimate to 2 meter precision. Mapshop doesn't like too many
* points:
*/
LocationUtils.decimate(segment, 2.0);
/* It the track still has > 500 points, split it in pieces: */
if (segment.getLocations().size() > 500) {
splitTracks.addAll(LocationUtils.split(segment, 500));
} else if (segment.getLocations().size() >= 2) {
splitTracks.add(segment);
}
}
/**
* Sets the current upload progress.
*/
private void updateProgress() {
// The percent of the total that represents the completed part of this
// segment.
int totalPercentage =
(totalLocationsRead + totalLocationsPrepared + totalLocationsUploaded) * 100
/ (totalLocations * 3);
totalPercentage = Math.min(99, totalPercentage);
progressIndicator.setProgressValue(totalPercentage);
}
}
@@ -0,0 +1,245 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.backup;
import com.google.android.apps.mytracks.MyTracks;
import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.util.FileUtils;
import com.google.android.maps.mytracks.R;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.AlertDialog.Builder;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
import android.util.Log;
import android.widget.Toast;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Date;
/**
* Helper which shows a UI for writing or restoring a backup,
* and calls the appropriate handler for actually executing those
* operations.
*
* @author Rodrigo Damazio
*/
public class BackupActivityHelper {
// Since the user sees this format, we use the local timezone
private static final SimpleDateFormat DISPLAY_BACKUP_FORMAT =
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
private static final Comparator<Date> REVERSE_DATE_ORDER =
new Comparator<Date>() {
@Override
public int compare(Date s1, Date s2) {
return s2.compareTo(s1);
}
};
private final FileUtils fileUtils;
private final ExternalFileBackup backup;
private final Activity activity;
public BackupActivityHelper(Activity activity) {
this.activity = activity;
this.fileUtils = new FileUtils();
this.backup = new ExternalFileBackup(activity, fileUtils);
}
/**
* Writes a full backup to the default file.
* This shows the results to the user.
*/
public void writeBackup() {
if (!fileUtils.isSdCardAvailable()) {
showToast(R.string.io_no_external_storage_found);
return;
}
if (!backup.isBackupsDirectoryAvailable(true)) {
showToast(R.string.io_create_dir_failed);
return;
}
final ProgressDialog progressDialog = ProgressDialog.show(
activity,
activity.getString(R.string.progress_title),
activity.getString(R.string.backup_write_progress_message),
true);
// Do the writing in another thread
new Thread() {
@Override
public void run() {
try {
backup.writeToDefaultFile();
showToast(R.string.io_write_finished);
} catch (IOException e) {
Log.e(Constants.TAG, "Failed to write backup", e);
showToast(R.string.io_write_failed);
} finally {
dismissDialog(progressDialog);
}
}
}.start();
}
/**
* Restores a full backup from the SD card.
* The user will be given a choice of which backup to restore as well as a
* confirmation dialog.
*/
public void restoreBackup() {
// Get the list of existing backups
if (!fileUtils.isSdCardAvailable()) {
showToast(R.string.io_no_external_storage_found);
return;
}
if (!backup.isBackupsDirectoryAvailable(false)) {
showToast(R.string.no_backups);
return;
}
final Date[] backupDates = backup.getAvailableBackups();
if (backupDates == null || backupDates.length == 0) {
showToast(R.string.no_backups);
return;
}
Arrays.sort(backupDates, REVERSE_DATE_ORDER);
// Show a confirmation dialog
Builder confirmationDialogBuilder = new AlertDialog.Builder(activity);
confirmationDialogBuilder.setMessage(R.string.restore_overwrites_warning);
confirmationDialogBuilder.setCancelable(true);
confirmationDialogBuilder.setPositiveButton(android.R.string.yes,
new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
pickBackupForRestore(backupDates);
}
});
confirmationDialogBuilder.setNegativeButton(android.R.string.no, null);
confirmationDialogBuilder.create().show();
}
/**
* Shows a backup list for the user to pick, then restores it.
*
* @param backupDir the backup directory
* @param backupDates the list of available backup files
*/
private void pickBackupForRestore(final Date[] backupDates) {
if (backupDates.length == 1) {
// Only one choice, don't bother showing the list
restoreFromDateAsync(backupDates[0]);
return;
}
// Make a user-visible version of the backup filenames
final String backupDateStrs[] = new String[backupDates.length];
for (int i = 0; i < backupDates.length; i++) {
backupDateStrs[i] = DISPLAY_BACKUP_FORMAT.format(backupDates[i]);
}
// Show a dialog for the user to pick which backup to restore
Builder dialogBuilder = new AlertDialog.Builder(activity);
dialogBuilder.setCancelable(true);
dialogBuilder.setTitle(R.string.select_backup_to_restore);
dialogBuilder.setItems(backupDateStrs, new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// User picked to restore this one
restoreFromDateAsync(backupDates[which]);
}
});
dialogBuilder.create().show();
}
/**
* Shows a progress dialog, then starts restoring the backup osynchronously.
*
* @param inputFile the file to restore from
*/
private void restoreFromDateAsync(final Date date) {
// Show a progress dialog
ProgressDialog.show(
activity,
activity.getString(R.string.progress_title),
activity.getString(R.string.backup_import_progress_message),
true);
// Do the actual importing in another thread (don't block the UI)
new Thread() {
@Override
public void run() {
try {
backup.restoreFromDate(date);
showToast(R.string.io_read_finished);
} catch (IOException e) {
Log.e(Constants.TAG, "Failed to restore backup", e);
showToast(R.string.io_read_failed);
} finally {
// Data may have been restored, "reboot" the app to catch it
restartApplication();
}
}
}.start();
}
/**
* Restarts My Tracks completely.
* This forces any modified data to be re-read.
*/
private void restartApplication() {
Intent intent = new Intent(activity, MyTracks.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
activity.startActivity(intent);
}
/**
* Shows a toast with the given contents.
*/
private void showToast(final int resId) {
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(activity, resId, Toast.LENGTH_LONG).show();
}
});
}
/**
* Safely dismisses the given dialog.
*/
private void dismissDialog(final Dialog dialog) {
activity.runOnUiThread(new Runnable() {
@Override
public void run() {
dialog.dismiss();
}
});
}
}
@@ -0,0 +1,70 @@
/*
* 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.content.ContentTypeIds.*;
import com.google.android.apps.mytracks.content.TrackPointsColumns;
import com.google.android.apps.mytracks.content.TracksColumns;
import com.google.android.apps.mytracks.content.WaypointsColumns;
public class BackupColumns {
/** Columns that go into the backup. */
public static final String[] POINTS_BACKUP_COLUMNS =
{ TrackPointsColumns._ID, TrackPointsColumns.TRACKID, TrackPointsColumns.LATITUDE,
TrackPointsColumns.LONGITUDE, TrackPointsColumns.ALTITUDE, TrackPointsColumns.BEARING,
TrackPointsColumns.TIME, TrackPointsColumns.ACCURACY, TrackPointsColumns.SPEED,
TrackPointsColumns.SENSOR };
public static final byte[] POINTS_BACKUP_COLUMN_TYPES =
{ LONG_TYPE_ID, LONG_TYPE_ID, INT_TYPE_ID, INT_TYPE_ID, FLOAT_TYPE_ID,
FLOAT_TYPE_ID, LONG_TYPE_ID, FLOAT_TYPE_ID, FLOAT_TYPE_ID, BLOB_TYPE_ID };
public static final String[] TRACKS_BACKUP_COLUMNS = {
TracksColumns._ID, TracksColumns.NAME, TracksColumns.DESCRIPTION, TracksColumns.CATEGORY,
TracksColumns.STARTID, TracksColumns.STOPID, TracksColumns.STARTTIME, TracksColumns.STOPTIME,
TracksColumns.NUMPOINTS, TracksColumns.TOTALDISTANCE, TracksColumns.TOTALTIME,
TracksColumns.MOVINGTIME, TracksColumns.AVGSPEED, TracksColumns.AVGMOVINGSPEED,
TracksColumns.MAXSPEED, TracksColumns.MINELEVATION, TracksColumns.MAXELEVATION,
TracksColumns.ELEVATIONGAIN, TracksColumns.MINGRADE, TracksColumns.MAXGRADE,
TracksColumns.MINLAT, TracksColumns.MAXLAT, TracksColumns.MINLON, TracksColumns.MAXLON,
TracksColumns.MAPID, TracksColumns.TABLEID};
public static final byte[] TRACKS_BACKUP_COLUMN_TYPES = {
LONG_TYPE_ID, STRING_TYPE_ID, STRING_TYPE_ID, STRING_TYPE_ID,
LONG_TYPE_ID, LONG_TYPE_ID, LONG_TYPE_ID, LONG_TYPE_ID, INT_TYPE_ID,
FLOAT_TYPE_ID, LONG_TYPE_ID, LONG_TYPE_ID, FLOAT_TYPE_ID, FLOAT_TYPE_ID,
FLOAT_TYPE_ID, FLOAT_TYPE_ID, FLOAT_TYPE_ID, FLOAT_TYPE_ID, FLOAT_TYPE_ID,
FLOAT_TYPE_ID, INT_TYPE_ID, INT_TYPE_ID, INT_TYPE_ID, INT_TYPE_ID, STRING_TYPE_ID,
STRING_TYPE_ID};
public static final String[] WAYPOINTS_BACKUP_COLUMNS = {
WaypointsColumns._ID, WaypointsColumns.TRACKID, WaypointsColumns.NAME,
WaypointsColumns.DESCRIPTION, WaypointsColumns.CATEGORY, WaypointsColumns.ICON,
WaypointsColumns.TYPE, WaypointsColumns.LENGTH, WaypointsColumns.DURATION,
WaypointsColumns.STARTTIME, WaypointsColumns.STARTID, WaypointsColumns.STOPID,
WaypointsColumns.LATITUDE, WaypointsColumns.LONGITUDE, WaypointsColumns.ALTITUDE,
WaypointsColumns.BEARING, WaypointsColumns.TIME, WaypointsColumns.ACCURACY,
WaypointsColumns.SPEED, WaypointsColumns.TOTALDISTANCE, WaypointsColumns.TOTALTIME,
WaypointsColumns.MOVINGTIME, WaypointsColumns.AVGSPEED, WaypointsColumns.AVGMOVINGSPEED,
WaypointsColumns.MAXSPEED, WaypointsColumns.MINELEVATION, WaypointsColumns.MAXELEVATION,
WaypointsColumns.ELEVATIONGAIN, WaypointsColumns.MINGRADE, WaypointsColumns.MAXGRADE };
public static final byte[] WAYPOINTS_BACKUP_COLUMN_TYPES = {
LONG_TYPE_ID, LONG_TYPE_ID, STRING_TYPE_ID, STRING_TYPE_ID,
STRING_TYPE_ID, STRING_TYPE_ID, INT_TYPE_ID, FLOAT_TYPE_ID, LONG_TYPE_ID,
LONG_TYPE_ID, LONG_TYPE_ID, LONG_TYPE_ID, INT_TYPE_ID, INT_TYPE_ID,
FLOAT_TYPE_ID, FLOAT_TYPE_ID, LONG_TYPE_ID, FLOAT_TYPE_ID, FLOAT_TYPE_ID,
FLOAT_TYPE_ID, LONG_TYPE_ID, LONG_TYPE_ID, FLOAT_TYPE_ID, FLOAT_TYPE_ID,
FLOAT_TYPE_ID, FLOAT_TYPE_ID, FLOAT_TYPE_ID, FLOAT_TYPE_ID, FLOAT_TYPE_ID,
FLOAT_TYPE_ID };
}
@@ -0,0 +1,27 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.backup;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
/**
* Shared preferences listener which notifies the backup system about new data
* being available for backup.
*
* @author Rodrigo Damazio
*/
public interface BackupPreferencesListener extends OnSharedPreferenceChangeListener {
}
@@ -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;
/**
* Implementation of {@link BackupPreferencesListener} that calls the
* {@link BackupManager}.
*
* @author Jimmy Shih
*/
public class BackupPreferencesListenerImpl implements BackupPreferencesListener {
private final BackupManager backupManager;
public BackupPreferencesListenerImpl(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,197 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.backup;
import com.google.android.apps.mytracks.content.ContentTypeIds;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.net.Uri;
import java.io.DataInputStream;
import java.io.IOException;
/**
* Database importer which reads values written by {@link DatabaseDumper}.
*
* @author Rodrigo Damazio
*/
public class DatabaseImporter {
/** Maximum number of entries in a bulk insertion */
private static final int DEFAULT_BULK_SIZE = 1024;
private final Uri destinationUri;
private final ContentResolver resolver;
private final boolean readNullFields;
private final int bulkSize;
// Metadata read from the reader
private String[] columnNames;
private byte[] columnTypes;
public DatabaseImporter(Uri destinationUri, ContentResolver resolver,
boolean readNullFields) {
this(destinationUri, resolver, readNullFields, DEFAULT_BULK_SIZE);
}
protected DatabaseImporter(Uri destinationUri, ContentResolver resolver,
boolean readNullFields, int bulkSize) {
this.destinationUri = destinationUri;
this.resolver = resolver;
this.readNullFields = readNullFields;
this.bulkSize = bulkSize;
}
/**
* Reads the header which includes metadata about the table being imported.
*
* @throws IOException if there are any problems while reading
*/
private void readHeaders(DataInputStream reader) throws IOException {
int numColumns = reader.readInt();
columnNames = new String[numColumns];
columnTypes = new byte[numColumns];
for (int i = 0; i < numColumns; i++) {
columnNames[i] = reader.readUTF();
columnTypes[i] = reader.readByte();
}
}
/**
* Imports all rows from the reader into the database.
* Insertion is done in bulks for efficiency.
*
* @throws IOException if there are any errors while reading
*/
public void importAllRows(DataInputStream reader) throws IOException {
readHeaders(reader);
ContentValues[] valueBulk = new ContentValues[bulkSize];
int numValues = 0;
int numRows = reader.readInt();
int numColumns = columnNames.length;
// For each row
for (int r = 0; r < numRows; r++) {
if (valueBulk[numValues] == null) {
valueBulk[numValues] = new ContentValues(numColumns);
} else {
// Reuse values objects
valueBulk[numValues].clear();
}
// Read the fields bitmap
long fields = reader.readLong();
for (int c = 0; c < numColumns; c++) {
if ((fields & 1) == 1) {
// Field is present, read into values
readOneCell(columnNames[c], columnTypes[c], valueBulk[numValues],
reader);
} else if (readNullFields) {
// Field not present but still written, read and discard
readOneCell(columnNames[c], columnTypes[c], null, reader);
}
fields >>= 1;
}
numValues++;
// If we have enough values, flush them as a bulk insertion
if (numValues >= bulkSize) {
doBulkInsert(valueBulk);
numValues = 0;
}
}
// Do a final bulk insert with the leftovers
if (numValues > 0) {
ContentValues[] leftovers = new ContentValues[numValues];
System.arraycopy(valueBulk, 0, leftovers, 0, numValues);
doBulkInsert(leftovers);
}
}
protected void doBulkInsert(ContentValues[] values) {
resolver.bulkInsert(destinationUri, values);
}
/**
* Reads a single cell from the reader.
*
* @param name the name of the column to be read
* @param typeId the type ID of the column to be read
* @param values the {@link ContentValues} object to put the read cell value
* in - if null, the value is just discarded
* @throws IOException if there are any problems while reading
*/
private void readOneCell(String name, byte typeId, ContentValues values,
DataInputStream reader) throws IOException {
switch (typeId) {
case ContentTypeIds.BOOLEAN_TYPE_ID: {
boolean value = reader.readBoolean();
if (values != null) { values.put(name, value); }
return;
}
case ContentTypeIds.LONG_TYPE_ID: {
long value = reader.readLong();
if (values != null) { values.put(name, value); }
return;
}
case ContentTypeIds.DOUBLE_TYPE_ID: {
double value = reader.readDouble();
if (values != null) { values.put(name, value); }
return;
}
case ContentTypeIds.FLOAT_TYPE_ID: {
Float value = reader.readFloat();
if (values != null) { values.put(name, value); }
return;
}
case ContentTypeIds.INT_TYPE_ID: {
int value = reader.readInt();
if (values != null) { values.put(name, value); }
return;
}
case ContentTypeIds.STRING_TYPE_ID: {
String value = reader.readUTF();
if (values != null) { values.put(name, value); }
return;
}
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(
"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,266 @@
/*
* 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.TimeZone;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;
/**
* Handler for writing or reading single-file backups.
*
* @author Rodrigo Damazio
*/
class ExternalFileBackup {
// Filename format - in UTC
private static final SimpleDateFormat BACKUP_FILENAME_FORMAT =
new SimpleDateFormat("'backup-'yyyy-MM-dd_HH-mm-ss'.zip'");
static {
BACKUP_FILENAME_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC"));
}
private static final String BACKUPS_SUBDIR = "backups";
private static final int BACKUP_FORMAT_VERSION = 1;
private static final String ZIP_ENTRY_NAME =
"backup.mytracks.v" + BACKUP_FORMAT_VERSION;
private static final int COMPRESSION_LEVEL = 8;
private final Context context;
private final FileUtils fileUtils;
public ExternalFileBackup(Context context, FileUtils fileUtils) {
this.context = context;
this.fileUtils = fileUtils;
}
/**
* Returns whether the backups directory is (or can be made) available.
*
* @param create whether to try creating the directory if it doesn't exist
*/
public boolean isBackupsDirectoryAvailable(boolean create) {
return getBackupsDirectory(create) != null;
}
/**
* Returns the backup directory, or null if not available.
*
* @param create whether to try creating the directory if it doesn't exist
*/
private File getBackupsDirectory(boolean create) {
String dirName = fileUtils.buildExternalDirectoryPath(BACKUPS_SUBDIR);
final File dir = new File(dirName);
Log.d(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 dir.isDirectory() ? dir : null;
}
}
/**
* Returns a list of available backups to be restored.
*/
public Date[] getAvailableBackups() {
File dir = getBackupsDirectory(false);
if (dir == null) { return null; }
String[] fileNames = dir.list();
List<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();
DatabaseDumper trackDumper = new DatabaseDumper(
BackupColumns.TRACKS_BACKUP_COLUMNS,
BackupColumns.TRACKS_BACKUP_COLUMN_TYPES,
false);
DatabaseDumper waypointDumper = new DatabaseDumper(
BackupColumns.WAYPOINTS_BACKUP_COLUMNS,
BackupColumns.WAYPOINTS_BACKUP_COLUMN_TYPES,
false);
DatabaseDumper pointDumper = new DatabaseDumper(
BackupColumns.POINTS_BACKUP_COLUMNS,
BackupColumns.POINTS_BACKUP_COLUMN_TYPES,
false);
// Open the target for writing
FileOutputStream outputStream = new FileOutputStream(outputFile);
ZipOutputStream compressedStream = new ZipOutputStream(outputStream);
compressedStream.setLevel(COMPRESSION_LEVEL);
compressedStream.putNextEntry(new ZipEntry(ZIP_ENTRY_NAME));
DataOutputStream outWriter = new DataOutputStream(compressedStream);
try {
// Dump the entire contents of each table
ContentResolver contentResolver = context.getContentResolver();
Cursor tracksCursor = contentResolver.query(
TracksColumns.CONTENT_URI, null, null, null, null);
try {
trackDumper.writeAllRows(tracksCursor, outWriter);
} finally {
tracksCursor.close();
}
Cursor waypointsCursor = contentResolver.query(
WaypointsColumns.CONTENT_URI, null, null, null, null);
try {
waypointDumper.writeAllRows(waypointsCursor, outWriter);
} finally {
waypointsCursor.close();
}
Cursor pointsCursor = contentResolver.query(
TrackPointsColumns.CONTENT_URI, null, null, null, null);
try {
pointDumper.writeAllRows(pointsCursor, outWriter);
} finally {
pointsCursor.close();
}
// Dump preferences
SharedPreferences preferences =
context.getSharedPreferences(Constants.SETTINGS_NAME, 0);
preferencesHelper.exportPreferences(preferences, outWriter);
} catch (IOException e) {
// We tried to delete the partially created file, but do nothing
// if that also fails.
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();
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, 0);
preferencesHelper.importPreferences(reader, preferences);
} finally {
compressedStream.close();
zipFile.close();
}
}
}
@@ -0,0 +1,98 @@
/*
* 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.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, 0);
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();
}
@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, 0);
PreferenceBackupHelper importer = createPreferenceBackupHelper();
importer.importPreferences(dataBuffer, preferences);
}
}
@@ -0,0 +1,176 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.backup;
import com.google.android.apps.mytracks.content.ContentTypeIds;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.Map;
/**
* Helper for backing up and restoring shared preferences.
*
* @author Rodrigo Damazio
*/
class PreferenceBackupHelper {
private static final int BUFFER_SIZE = 2048;
/**
* Exports all shared preferences from the given object as a byte array.
*
* @param preferences the preferences to export
* @return the corresponding byte array
* @throws IOException if there are any errors while writing to the byte array
*/
public byte[] exportPreferences(SharedPreferences preferences)
throws IOException {
ByteArrayOutputStream bufStream = new ByteArrayOutputStream(BUFFER_SIZE);
DataOutputStream outWriter = new DataOutputStream(bufStream);
exportPreferences(preferences, outWriter);
return bufStream.toByteArray();
}
/**
* Exports all shared preferences from the given object into the given output
* stream.
*
* @param preferences the preferences to export
* @param 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();
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
* @return whether the preference change was successful
* @throws IOException if there are any errors while reading
*/
public boolean importPreferences(byte[] data, SharedPreferences preferences)
throws IOException {
ByteArrayInputStream bufStream = new ByteArrayInputStream(data);
DataInputStream reader = new DataInputStream(bufStream);
return importPreferences(reader, preferences);
}
/**
* Imports all preferences from the given stream.
*
* @param reader the stream to read from
* @param preferences the shared preferences to edit
* @return whether the preference change was successful
* @throws IOException if there are any errors while reading
*/
public boolean importPreferences(DataInputStream reader,
SharedPreferences preferences) throws IOException {
Editor editor = preferences.edit();
editor.clear();
int numPreferences = reader.readInt();
for (int i = 0; i < numPreferences; i++) {
String name = reader.readUTF();
byte typeId = reader.readByte();
readAndSetPreference(name, typeId, reader, editor);
}
return editor.commit();
}
/**
* Reads a single preference and sets it into the given editor.
*
* @param name the name of the preference to read
* @param typeId the type ID of the preference to read
* @param reader the reader to read from
* @param editor the editor to set the preference in
* @throws IOException if there are errors while reading
*/
private void readAndSetPreference(String name, byte typeId,
DataInputStream reader, Editor editor) throws IOException {
switch (typeId) {
case ContentTypeIds.BOOLEAN_TYPE_ID:
editor.putBoolean(name, reader.readBoolean());
return;
case ContentTypeIds.LONG_TYPE_ID:
editor.putLong(name, reader.readLong());
return;
case ContentTypeIds.FLOAT_TYPE_ID:
editor.putFloat(name, reader.readFloat());
return;
case ContentTypeIds.INT_TYPE_ID:
editor.putInt(name, reader.readInt());
return;
case ContentTypeIds.STRING_TYPE_ID:
editor.putString(name, reader.readUTF());
return;
}
}
/**
* Writes a single preference.
*
* @param name the name of the preference to write
* @param value the correctly-typed value of the preference
* @param writer the writer to write to
* @throws IOException if there are errors while writing
*/
private void writePreference(String name, Object value, DataOutputStream writer)
throws IOException {
writer.writeUTF(name);
if (value instanceof Boolean) {
writer.writeByte(ContentTypeIds.BOOLEAN_TYPE_ID);
writer.writeBoolean((Boolean) value);
} else if (value instanceof Integer) {
writer.writeByte(ContentTypeIds.INT_TYPE_ID);
writer.writeInt((Integer) value);
} else if (value instanceof Long) {
writer.writeByte(ContentTypeIds.LONG_TYPE_ID);
writer.writeLong((Long) value);
} else if (value instanceof Float) {
writer.writeByte(ContentTypeIds.FLOAT_TYPE_ID);
writer.writeFloat((Float) value);
} else if (value instanceof String) {
writer.writeByte(ContentTypeIds.STRING_TYPE_ID);
writer.writeUTF((String) value);
} else {
throw new IllegalArgumentException(
"Type " + value.getClass().getName() + " not supported");
}
}
}
@@ -0,0 +1,388 @@
/*
* 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.docs;
import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.io.AuthManager;
import com.google.android.apps.mytracks.io.gdata.GDataWrapper;
import com.google.android.apps.mytracks.io.gdata.GDataWrapper.QueryFunction;
import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.android.apps.mytracks.util.ResourceUtils;
import com.google.android.apps.mytracks.util.StringUtils;
import com.google.android.maps.mytracks.R;
import com.google.wireless.gdata.client.GDataServiceClient;
import com.google.wireless.gdata.client.HttpException;
import com.google.wireless.gdata.data.Entry;
import com.google.wireless.gdata.docs.SpreadsheetsClient;
import com.google.wireless.gdata.docs.SpreadsheetsClient.WorksheetEntry;
import com.google.wireless.gdata.parser.GDataParser;
import com.google.wireless.gdata.parser.ParseException;
import com.google.wireless.gdata2.client.AuthenticationException;
import android.content.Context;
import android.text.format.DateFormat;
import android.util.Log;
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.net.URL;
import java.net.URLConnection;
import java.util.Date;
import java.util.concurrent.atomic.AtomicReference;
/**
* This class contains helper methods for interacting with Google Docs and
* Google Spreadsheets.
*
* @author Sandor Dornbush
* @author Matthew Simmons
*/
public class DocsHelper {
private static final String DOCS_FEED_URL =
"https://docs.google.com/feeds/documents/private/full";
private static final String DOCS_SPREADSHEET_URL =
"https://docs.google.com/feeds/documents/private/full/spreadsheet%3A";
private static final String DOCS_WORKSHEETS_URL_FORMAT =
"https://spreadsheets.google.com/feeds/worksheets/%s/private/full";
private static final String DOCS_MY_SPREADSHEETS_FEED_URL =
"https://docs.google.com/feeds/documents/private/full?category=mine,spreadsheet";
private static final String DOCS_SPREADSHEET_URL_FORMAT =
"https://spreadsheets.google.com/feeds/list/%s/%s/private/full";
private static final String CONTENT_TYPE_PARAM = "Content-Type";
private static final String OPENDOCUMENT_SPREADSHEET_MIME_TYPE =
"application/x-vnd.oasis.opendocument.spreadsheet";
private static final String ATOM_FEED_MIME_TYPE = "application/atom+xml";
/**
* Creates a new MyTracks spreadsheet with the given name.
*
* @param context The context associated with this request.
* @param docListWrapper The GData handle for the Document List service.
* @param name The name for the newly-created spreadsheet.
* @return The spreadsheet ID, if one is created. {@code null} will be
* returned if a GData error didn't occur, but no spreadsheet ID was
* returned.
*/
public String createSpreadsheet(final Context context,
final GDataWrapper<GDataServiceClient> docListWrapper, final String name) throws IOException {
final AtomicReference<String> idSaver = new AtomicReference<String>();
boolean success = docListWrapper.runQuery(new QueryFunction<GDataServiceClient>() {
@Override
public void query(GDataServiceClient client) throws IOException,
GDataWrapper.AuthenticationException {
// Construct and send request
URL url = new URL(DOCS_FEED_URL);
URLConnection conn = url.openConnection();
conn.addRequestProperty(CONTENT_TYPE_PARAM,
OPENDOCUMENT_SPREADSHEET_MIME_TYPE);
conn.addRequestProperty("Slug", name);
conn.addRequestProperty("Authorization",
"GoogleLogin auth=" +
docListWrapper.getAuthManager().getAuthToken());
conn.setDoOutput(true);
OutputStream os = conn.getOutputStream();
ResourceUtils.readBinaryFileToOutputStream(
context, R.raw.mytracks_empty_spreadsheet, os);
// Get the response
// TODO: The following is a horrible ugly hack.
// Hopefully we can retire it when there is a proper gdata api.
BufferedReader rd = null;
String line;
StringBuilder resultBuilder = new StringBuilder();
try {
rd = new BufferedReader(new InputStreamReader(conn.getInputStream()));
while ((line = rd.readLine()) != null) {
resultBuilder.append(line);
}
os.close();
rd.close();
} catch (FileNotFoundException e) {
// The GData API sometimes throws an error, even though creation of
// the document succeeded. In that case let's just return. The caller
// then needs to check if the doc actually exists.
return;
} finally {
os.close();
if (rd != null) {
rd.close();
}
}
String result = resultBuilder.toString();
// Try to find the id.
int idTagIndex = result.indexOf("<id>");
if (idTagIndex == -1) {
return;
}
int idTagCloseIndex = result.indexOf("</id>", idTagIndex);
if (idTagCloseIndex == -1) {
return;
}
int idStringStart = result.indexOf(DOCS_SPREADSHEET_URL, idTagIndex);
if (idStringStart == -1) {
return;
}
String id = result.substring(
idStringStart + DOCS_SPREADSHEET_URL.length(), idTagCloseIndex);
Log.i(Constants.TAG, "Created new spreadsheet: " + id);
idSaver.set(id);
}});
if (!success) {
throw newIOException(docListWrapper,
"Failed to create new spreadsheet.");
}
return idSaver.get();
}
/**
* Retrieve the ID of a spreadsheet with the given name.
*
* @param docListWrapper The GData handle for the Document List service.
* @param title The name of the spreadsheet whose ID is to be retrieved.
* @return The spreadsheet ID, if it can be retrieved. {@code null} will
* be returned if no spreadsheet exists by the given name.
* @throws IOException If an error occurs during the GData request.
*/
public String requestSpreadsheetId(final GDataWrapper<GDataServiceClient> docListWrapper,
final String title) throws IOException {
final AtomicReference<String> idSaver = new AtomicReference<String>();
boolean result = docListWrapper.runQuery(new QueryFunction<GDataServiceClient>() {
@Override
public void query(GDataServiceClient client)
throws IOException, GDataWrapper.ParseException, GDataWrapper.HttpException,
GDataWrapper.AuthenticationException {
GDataParser listParser;
try {
listParser = client.getParserForFeed(Entry.class,
DOCS_MY_SPREADSHEETS_FEED_URL,
docListWrapper.getAuthManager().getAuthToken());
listParser.init();
while (listParser.hasMoreData()) {
Entry entry = listParser.readNextEntry(null);
String entryTitle = entry.getTitle();
Log.i(Constants.TAG, "Found docs entry: " + entryTitle);
if (entryTitle.equals(title)) {
String entryId = entry.getId();
int lastSlash = entryId.lastIndexOf('/');
idSaver.set(entryId.substring(lastSlash + 15));
break;
}
}
} catch (ParseException e) {
throw new GDataWrapper.ParseException(e);
} catch (HttpException e) {
throw new GDataWrapper.HttpException(e.getStatusCode(), e.getMessage());
}
}
});
if (!result) {
throw newIOException(docListWrapper,
"Failed to retrieve spreadsheet list.");
}
return idSaver.get();
}
/**
* Retrieve the ID of the first worksheet in the named spreadsheet.
*
* @param trixWrapper The GData handle for the spreadsheet service.
* @param spreadsheetId The GData ID for the given spreadsheet.
* @return The worksheet ID, if it can be retrieved. {@code null} will be
* returned if the GData request returns without error, but without an
* ID.
* @throws IOException If an error occurs during the GData request.
*/
public String getWorksheetId(final GDataWrapper<GDataServiceClient> trixWrapper,
final String spreadsheetId) throws IOException {
final AtomicReference<String> idSaver = new AtomicReference<String>();
boolean result = trixWrapper.runQuery(new QueryFunction<GDataServiceClient>() {
@Override
public void query(GDataServiceClient client)
throws GDataWrapper.AuthenticationException, IOException, GDataWrapper.ParseException, GDataWrapper.HttpException {
String uri = String.format(DOCS_WORKSHEETS_URL_FORMAT, spreadsheetId);
GDataParser sheetParser;
try {
sheetParser = ((SpreadsheetsClient) client).getParserForWorksheetsFeed(uri,
trixWrapper.getAuthManager().getAuthToken());
sheetParser.init();
if (!sheetParser.hasMoreData()) {
Log.i(Constants.TAG, "Found no worksheets");
return;
}
// Grab the first worksheet.
WorksheetEntry worksheetEntry =
(WorksheetEntry) sheetParser.readNextEntry(new WorksheetEntry());
int lastSlash = worksheetEntry.getId().lastIndexOf('/');
idSaver.set(worksheetEntry.getId().substring(lastSlash + 1));
} catch (ParseException e) {
throw new GDataWrapper.ParseException(e);
} catch (AuthenticationException e) {
throw new GDataWrapper.AuthenticationException(e);
}
}
});
if (!result) {
throw newIOException(trixWrapper, "Failed to retrieve worksheet ID.");
}
return idSaver.get();
}
/**
* Add a row to a worksheet containing the stats for a given track.
*
* @param context The context associated with this request.
* @param trixAuth The GData authorization for the spreadsheet service.
* @param spreadsheetId The spreadsheet to be modified.
* @param worksheetId The worksheet to be modified.
* @param track The track whose stats are to be written.
* @param metricUnits True if metric units are to be used. If false,
* imperial units will be used.
* @throws IOException If an error occurs while updating the worksheet.
*/
public void addTrackRow(Context context, AuthManager trixAuth,
String spreadsheetId, String worksheetId, Track track,
boolean metricUnits) throws IOException {
String worksheetUri = String.format(DOCS_SPREADSHEET_URL_FORMAT,
spreadsheetId, worksheetId);
TripStatistics stats = track.getStatistics();
String distanceUnit = context.getString(metricUnits ?
R.string.kilometer : R.string.mile);
String speedUnit = context.getString(metricUnits ?
R.string.kilometer_per_hour : R.string.mile_per_hour);
String elevationUnit = context.getString(metricUnits ?
R.string.meter : R.string.feet);
// Prepare the Post-Text we are going to send.
DocsTagBuilder tagBuilder = new DocsTagBuilder(metricUnits)
.append("name", track.getName())
.append("description", track.getDescription())
.append("date", getDisplayDate(context, stats.getStartTime()))
.append("totaltime", StringUtils.formatTimeAlwaysShowingHours(
stats.getTotalTime()))
.append("movingtime", StringUtils.formatTimeAlwaysShowingHours(
stats.getMovingTime()))
.appendLargeUnits("distance", stats.getTotalDistance() / 1000)
.append("distanceunit", distanceUnit)
.appendLargeUnits("averagespeed", stats.getAverageSpeed() * 3.6)
.appendLargeUnits("averagemovingspeed",
stats.getAverageMovingSpeed() * 3.6)
.appendLargeUnits("maxspeed", stats.getMaxSpeed() * 3.6)
.append("speedunit", speedUnit)
.appendSmallUnits("elevationgain", stats.getTotalElevationGain())
.appendSmallUnits("minelevation", stats.getMinElevation())
.appendSmallUnits("maxelevation", stats.getMaxElevation())
.append("elevationunit", elevationUnit);
if (track.getMapId().length() > 0) {
tagBuilder.append("map", String.format("%s?msa=0&msid=%s",
Constants.MAPSHOP_BASE_URL, track.getMapId()));
}
String postText = new StringBuilder()
.append("<entry xmlns='http://www.w3.org/2005/Atom' "
+ "xmlns:gsx='http://schemas.google.com/spreadsheets/"
+ "2006/extended'>")
.append(tagBuilder.build())
.append("</entry>")
.toString();
Log.i(Constants.TAG,
"Inserting at: " + spreadsheetId + " => " + worksheetUri);
Log.i(Constants.TAG, postText);
writeRowData(trixAuth, worksheetUri, postText);
Log.i(Constants.TAG, "Post finished.");
}
/**
* Gets the display string for a time based on the phone's setting.
*
* @param context the context to obtain the phone's setting.
* @param time the time
* @return the display string of the time
*/
protected String getDisplayDate(Context context, long time) {
java.text.DateFormat dateFormat = DateFormat.getDateFormat(context);
java.text.DateFormat timeFormat = DateFormat.getTimeFormat(context);
Date startTime = new Date(time);
String dateString = dateFormat.format(startTime) + " " + timeFormat.format(startTime);
return dateString;
}
/**
* Writes spreadsheet row data to the indicated worksheet.
*
* @param trixAuth The GData authorization for the spreadsheet service.
* @param worksheetUri The URI of the worksheet to be altered.
* @param postText The XML tags describing the change to be made.
* @throws IOException Thrown if an error occurs during the write.
*/
protected void writeRowData(AuthManager trixAuth, String worksheetUri,
String postText) throws IOException {
// No need for a wrapper because we know that the authorization was good
// enough to get this far.
URL url = new URL(worksheetUri);
URLConnection conn = url.openConnection();
conn.addRequestProperty(CONTENT_TYPE_PARAM, ATOM_FEED_MIME_TYPE);
conn.addRequestProperty("Authorization",
"GoogleLogin auth=" + trixAuth.getAuthToken());
conn.setDoOutput(true);
OutputStreamWriter wr = new OutputStreamWriter(conn.getOutputStream());
wr.write(postText);
wr.flush();
// Get the response.
// TODO: Should we parse the response, rather than simply throwing it away?
BufferedReader rd =
new BufferedReader(new InputStreamReader(conn.getInputStream()));
String line;
while ((line = rd.readLine()) != null) {
// Process line.
Log.i(Constants.TAG, "r: " + line);
}
wr.close();
rd.close();
}
private static IOException newIOException(GDataWrapper<GDataServiceClient> wrapper,
String message) {
return new IOException(String.format("%s: %d: %s", message,
wrapper.getErrorType(), wrapper.getErrorMessage()));
}
}
@@ -0,0 +1,117 @@
/*
* 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.docs;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.apps.mytracks.util.StringUtils;
import com.google.android.apps.mytracks.util.UnitConversions;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.NumberFormat;
import java.util.Locale;
/**
* <p>This class builds a string of XML tags used to talk to Docs using GData.
*
* <p>Sample Usage:
*
* <code>
* String tags = new DocsTagBuilder(false)
* .append("tagName", "tagValue")
* .appendLargeUnits("bigTagName", 1.0)
* .build();
* </code>
*
* <p>results in:
*
* <code>
* <gsx:tagName><![CDATA[tagValue]]></gsx:tagName>
* <gsx:bigTagName><![CDATA[1.00]]></gsx:bigTagName>
* </code>
*
* @author Matthew Simmons
*/
class DocsTagBuilder {
private static final DecimalFormatSymbols FORMAT_SYMBOLS =
ApiFeatures.getInstance().getApiAdapter().getDecimalFormatSymbols(Locale.ENGLISH);
private static final NumberFormat LARGE_UNIT_FORMAT =
new DecimalFormat("#,###,###.00", FORMAT_SYMBOLS);
private static final NumberFormat SMALL_UNIT_FORMAT =
new DecimalFormat("###,###", FORMAT_SYMBOLS);
protected final boolean metricUnits;
protected final StringBuilder stringBuilder;
/**
* @param metricUnits True if metric units are to be used. If false,
* imperial units will be used.
*/
DocsTagBuilder(boolean metricUnits) {
this.metricUnits = metricUnits;
this.stringBuilder = new StringBuilder();
}
/** Appends a tag containing a string value */
DocsTagBuilder append(String name, String value) {
appendTag(name, value);
return this;
}
/**
* Appends a tag containing a numeric value. The value will be formatted
* according to the large distance of the specified measurement system (i.e.
* kilometers or miles).
*
* @param name The tag name.
* @param d The value to be formatted, in kilometers.
*/
DocsTagBuilder appendLargeUnits(String name, double d) {
double value = metricUnits ? d : (d * UnitConversions.KM_TO_MI);
appendTag(name, LARGE_UNIT_FORMAT.format(value));
return this;
}
/**
* Appends a tag containing a numeric value. The value will be formatted
* according to the small distance of the specified measurement system (i.e.
* meters or feet).
*
* @param name The tag name.
* @param d The value to be formatted, in meters.
*/
DocsTagBuilder appendSmallUnits(String name, double d) {
double value = metricUnits ? d : (d * UnitConversions.M_TO_FT);
appendTag(name, SMALL_UNIT_FORMAT.format(value));
return this;
}
/** Returns a string containing all tags which have been added. */
String build() {
return stringBuilder.toString();
}
private void appendTag(String name, String value) {
stringBuilder.append("<gsx:")
.append(name)
.append(">")
.append(StringUtils.stringAsCData(value))
.append("</gsx:")
.append(name)
.append(">");
}
}
@@ -0,0 +1,174 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.file;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.io.file.TrackWriterFactory.TrackFileFormat;
import android.location.Location;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.text.NumberFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.TimeZone;
/**
* Exports a track as a CSV file, according to RFC 4180.
*
* The first field is a type:
* TRACK - track description
* P - point
* WAYPOINT - waypoint
*
* For each type, the fields are:
*
* TRACK,,,,,,,,name,description,
* P,time,lat,lon,alt,bearing,accurancy,speed,,,segmentIdx
* WAYPOINT,time,lat,lon,alt,bearing,accuracy,speed,name,description,
*
* @author Rodrigo Damazio
*/
public class CsvTrackWriter implements TrackFormatWriter {
static final NumberFormat SHORT_FORMAT = NumberFormat.getInstance();
static final SimpleDateFormat TIMESTAMP_FORMAT =
new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
static {
SHORT_FORMAT.setMaximumFractionDigits(4);
TIMESTAMP_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC"));
}
private int segmentIdx = 0;
private int numFields = -1;
private PrintWriter pw;
private Track track;
@Override
public String getExtension() {
return TrackFileFormat.CSV.getExtension();
}
@SuppressWarnings("hiding")
@Override
public void prepare(Track track, OutputStream out) {
this.track = track;
this.pw = new PrintWriter(out);
}
@Override
public void writeHeader() {
writeCommaSeparatedLine("TYPE", "TIME", "LAT", "LON", "ALT", "BEARING",
"ACCURACY", "SPEED", "NAME", "DESCRIPTION", "SEGMENT");
}
@Override
public void writeBeginTrack(Location firstPoint) {
writeCommaSeparatedLine("TRACK",
null, null, null, null, null, null, null,
track.getName(), track.getDescription(),
null);
}
@Override
public void writeOpenSegment() {
// Do nothing
}
@Override
public void writeLocation(Location location) {
String timeStr = TIMESTAMP_FORMAT.format(new Date(location.getTime()));
writeCommaSeparatedLine("P",
timeStr,
Double.toString(location.getLatitude()),
Double.toString(location.getLongitude()),
Double.toString(location.getAltitude()),
Double.toString(location.getBearing()),
SHORT_FORMAT.format(location.getAccuracy()),
SHORT_FORMAT.format(location.getSpeed()),
null, null,
Integer.toString(segmentIdx));
}
@Override
public void writeWaypoint(Waypoint waypoint) {
Location location = waypoint.getLocation();
String timeStr = TIMESTAMP_FORMAT.format(new Date(location.getTime()));
writeCommaSeparatedLine("WAYPOINT",
timeStr,
Double.toString(location.getLatitude()),
Double.toString(location.getLongitude()),
Double.toString(location.getAltitude()),
Double.toString(location.getBearing()),
SHORT_FORMAT.format(location.getAccuracy()),
SHORT_FORMAT.format(location.getSpeed()),
waypoint.getName(),
waypoint.getDescription(),
null);
}
/**
* Writes a single line of a comma-separated-value file.
*
* @param strs the values to be written as comma-separated
*/
private void writeCommaSeparatedLine(String... strs) {
if (numFields == -1) {
numFields = strs.length;
} else if (strs.length != numFields) {
throw new IllegalArgumentException(
"CSV lines with different number of fields");
}
boolean isFirst = true;
for (String str : strs) {
if (!isFirst) {
pw.print(',');
}
isFirst = false;
if (str != null) {
pw.print('"');
pw.print(str.replaceAll("\"", "\"\""));
pw.print('"');
}
}
pw.println();
}
@Override
public void writeCloseSegment() {
segmentIdx++;
}
@Override
public void writeEndTrack(Location lastPoint) {
// Do nothing
}
@Override
public void writeFooter() {
// Do nothing
}
@Override
public void close() {
pw.close();
}
}
@@ -0,0 +1,548 @@
/*
* 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.Constants;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.stats.TripStatisticsBuilder;
import com.google.android.apps.mytracks.util.LocationUtils;
import com.google.android.apps.mytracks.util.StringUtils;
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 javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import org.xml.sax.Attributes;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
/**
* Imports GPX XML files to the my tracks provider.
*
* TODO: Show progress indication to the user.
*
* @author Leif Hendrik Wilden
* @author Steffen Horlacher
* @author Rodrigo Damazio
*/
public class GpxImporter extends DefaultHandler {
/*
* GPX-XML tag names and attributes.
*/
private static final String TAG_TRACK = "trk";
private static final String TAG_TRACK_POINT = "trkpt";
private static final Object TAG_TRACK_SEGMENT = "trkseg";
private static final String TAG_NAME = "name";
private static final String TAG_DESCRIPTION = "desc";
private static final String TAG_ALTITUDE = "ele";
private static final String TAG_TIME = "time";
private static final String ATT_LAT = "lat";
private static final String ATT_LON = "lon";
/**
* The maximum number of locations to buffer for bulk-insertion into the database.
*/
private static final int MAX_BUFFERED_LOCATIONS = 512;
/**
* Utilities for accessing the contnet provider.
*/
private final MyTracksProviderUtils providerUtils;
/**
* List of track ids written in the database. Only contains successfully
* written tracks.
*/
private final List<Long> tracksWritten;
/**
* Contains the current elements content.
*/
private String content;
/**
* Currently reading location.
*/
private Location location;
/**
* Previous location, required for calculations.
*/
private Location lastLocation;
/**
* Currently reading track.
*/
private Track track;
/**
* Statistics builder for the current track.
*/
private TripStatisticsBuilder statsBuilder;
/**
* Buffer of locations to be bulk-inserted into the database.
*/
private Location[] bufferedPointInserts = new Location[MAX_BUFFERED_LOCATIONS];
/**
* Number of locations buffered to be inserted into the database.
*/
private int numBufferedPointInserts = 0;
/**
* Number of locations already processed.
*/
private int numberOfLocations;
/**
* Number of segments already processed.
*/
private int numberOfSegments;
/**
* Used to identify if a track was written to the database but not yet
* finished successfully.
*/
private boolean isCurrentTrackRollbackable;
/**
* Flag to indicate if we're inside a track's xml element.
* Some sub elements like name may be used in other parts of the gpx file,
* and we use this to ignore them.
*/
private boolean isInTrackElement;
/**
* Counter to find out which child level of track we are processing.
*/
private int trackChildDepth;
/**
* SAX-Locator to get current line information.
*/
private Locator locator;
private Location lastSegmentLocation;
/**
* Reads GPS tracks from a GPX file and writes tracks and their coordinates to
* the database.
*
* @param is a input steam with gpx-xml data
* @return long[] array of track ids written in the database
* @throws SAXException a parsing error
* @throws ParserConfigurationException internal error
* @throws IOException a file reading problem
*/
public static long[] importGPXFile(final InputStream is,
final MyTracksProviderUtils providerUtils)
throws ParserConfigurationException, SAXException, IOException {
SAXParserFactory factory = SAXParserFactory.newInstance();
GpxImporter handler = new GpxImporter(providerUtils);
SAXParser parser = factory.newSAXParser();
long[] trackIds = null;
try {
long start = System.currentTimeMillis();
parser.parse(is, handler);
long end = System.currentTimeMillis();
Log.d(Constants.TAG, "Total import time: " + (end - start) + "ms");
trackIds = handler.getImportedTrackIds();
} finally {
// delete track if not finished
handler.rollbackUnfinishedTracks();
}
return trackIds;
}
/**
* Constructor, requires providerUtils for writing tracks the database.
*/
public GpxImporter(MyTracksProviderUtils providerUtils) {
this.providerUtils = providerUtils;
tracksWritten = new ArrayList<Long>();
}
@Override
public void characters(char[] ch, int start, int length) throws SAXException {
String newContent = new String(ch, start, length);
if (content == null) {
content = newContent;
} else {
// In 99% of the cases, a single call to this method will be made for each
// sequence of characters we're interested in, so we'll rarely be
// concatenating strings, thus not justifying the use of a StringBuilder.
content += newContent;
}
}
@Override
public void startElement(String uri, String localName, String name,
Attributes attributes) throws SAXException {
if (isInTrackElement) {
trackChildDepth++;
if (localName.equals(TAG_TRACK_POINT)) {
onTrackPointElementStart(attributes);
} else if (localName.equals(TAG_TRACK_SEGMENT)) {
onTrackSegmentElementStart();
} else if (localName.equals(TAG_TRACK)) {
String msg = createErrorMessage("Invalid GPX-XML detected");
throw new SAXException(msg);
}
} else if (localName.equals(TAG_TRACK)) {
isInTrackElement = true;
trackChildDepth = 0;
onTrackElementStart();
}
}
@Override
public void endElement(String uri, String localName, String name)
throws SAXException {
if (!isInTrackElement) {
content = null;
return;
}
// process these elements only as sub-elements of track
if (localName.equals(TAG_TRACK_POINT)) {
onTrackPointElementEnd();
} else if (localName.equals(TAG_ALTITUDE)) {
onAltitudeElementEnd();
} else if (localName.equals(TAG_TIME)) {
onTimeElementEnd();
} else if (localName.equals(TAG_NAME)) {
// we are only interested in the first level name element
if (trackChildDepth == 1) {
onNameElementEnd();
}
} else if (localName.equals(TAG_DESCRIPTION)) {
// we are only interested in the first level description element
if (trackChildDepth == 1) {
onDescriptionElementEnd();
}
} else if (localName.equals(TAG_TRACK_SEGMENT)) {
onTrackSegmentElementEnd();
} else if (localName.equals(TAG_TRACK)) {
onTrackElementEnd();
isInTrackElement = false;
trackChildDepth = 0;
}
trackChildDepth--;
// reset element content
content = null;
}
@Override
public void setDocumentLocator(Locator locator) {
this.locator = locator;
}
/**
* Create a new Track object and insert empty track in database. Track will be
* updated with missing values later.
*/
private void onTrackElementStart() {
track = new Track();
numberOfLocations = 0;
Uri trackUri = providerUtils.insertTrack(track);
long trackId = Long.parseLong(trackUri.getLastPathSegment());
track.setId(trackId);
isCurrentTrackRollbackable = true;
}
private void onDescriptionElementEnd() {
track.setDescription(content.toString().trim());
}
private void onNameElementEnd() {
track.setName(content.toString().trim());
}
/**
* Track segment started.
*/
private void onTrackSegmentElementStart() {
if (numberOfSegments > 0) {
// Add a segment separator:
location = new Location(LocationManager.GPS_PROVIDER);
location.setLatitude(100.0);
location.setLongitude(100.0);
location.setAltitude(0);
if (lastLocation != null) {
location.setTime(lastLocation.getTime());
}
insertTrackPoint(location);
lastLocation = location;
lastSegmentLocation = null;
location = null;
}
numberOfSegments++;
}
/**
* Reads trackpoint attributes and assigns them to the current location.
*
* @param attributes xml attributes
*/
private void onTrackPointElementStart(Attributes attributes) throws SAXException {
if (location != null) {
String errorMsg = createErrorMessage("Found a track point inside another one.");
throw new SAXException(errorMsg);
}
location = createLocationFromAttributes(attributes);
}
/**
* Creates and returns a location with the position parsed from the given
* attributes.
*
* @param attributes the attributes to parse
* @return the created location
* @throws SAXException if the attributes cannot be parsed
*/
private Location createLocationFromAttributes(Attributes attributes) throws SAXException {
String latitude = attributes.getValue(ATT_LAT);
String longitude = attributes.getValue(ATT_LON);
if (latitude == null || longitude == null) {
throw new SAXException(createErrorMessage("Point with no longitude or latitude"));
}
// create new location and set attributes
Location loc = new Location(LocationManager.GPS_PROVIDER);
try {
loc.setLatitude(Double.parseDouble(latitude));
loc.setLongitude(Double.parseDouble(longitude));
} catch (NumberFormatException e) {
String msg = createErrorMessage(
"Unable to parse lat/long: " + latitude + "/" + longitude);
throw new SAXException(msg, e);
}
return loc;
}
/**
* Track point finished, write in database.
*
* @throws SAXException - thrown if track point is invalid
*/
private void onTrackPointElementEnd() throws SAXException {
if (LocationUtils.isValidLocation(location)) {
if (statsBuilder == null) {
// first point did not have a time, start stats builder without it
statsBuilder = new TripStatisticsBuilder(0);
}
statsBuilder.addLocation(location, location.getTime());
// insert in db
insertTrackPoint(location);
// first track point?
if (lastLocation == null && numberOfSegments == 1) {
track.setStartId(getLastPointId());
}
lastLocation = location;
lastSegmentLocation = location;
location = null;
} else {
// invalid location - abort import
String msg = createErrorMessage("Invalid location detected: " + location);
throw new SAXException(msg);
}
}
private void insertTrackPoint(Location loc) {
bufferedPointInserts[numBufferedPointInserts] = loc;
numBufferedPointInserts++;
numberOfLocations++;
if (numBufferedPointInserts >= MAX_BUFFERED_LOCATIONS) {
flushPointInserts();
}
}
private void flushPointInserts() {
if (numBufferedPointInserts <= 0) { return; }
providerUtils.bulkInsertTrackPoints(bufferedPointInserts, numBufferedPointInserts, track.getId());
numBufferedPointInserts = 0;
}
/**
* Track segment finished.
*/
private void onTrackSegmentElementEnd() {
// Nothing to be done
}
/**
* Track finished - update in database.
*/
private void onTrackElementEnd() {
if (lastLocation != null) {
flushPointInserts();
// Calculate statistics for the imported track and update
statsBuilder.pauseAt(lastLocation.getTime());
track.setStopId(getLastPointId());
track.setNumberOfPoints(numberOfLocations);
track.setStatistics(statsBuilder.getStatistics());
providerUtils.updateTrack(track);
tracksWritten.add(track.getId());
isCurrentTrackRollbackable = false;
lastSegmentLocation = null;
lastLocation = null;
statsBuilder = null;
} else {
// track contains no track points makes no real
// sense to import it as we have no location
// information -> roll back
rollbackUnfinishedTracks();
}
}
/**
* Setting time and doing additional calculations as this is the last value
* required. Also sets the start time for track and statistics as there is no
* start time in the track root element.
*
* @throws SAXException on parsing errors
*/
private void onTimeElementEnd() throws SAXException {
if (location == null) { return; }
// Parse the time
long time;
try {
time = StringUtils.parseXmlDateTime(content.trim());
} catch (IllegalArgumentException e) {
String msg = createErrorMessage("Unable to parse time: " + content);
throw new SAXException(msg, e);
}
// Calculate derived attributes from previous point
if (lastSegmentLocation != null) {
long timeDifference = time - lastSegmentLocation.getTime();
// check for negative time change
if (timeDifference < 0) {
Log.w(Constants.TAG, "Found negative time change.");
} else {
// We don't have a speed and bearing in GPX, make something up from
// the last two points.
// TODO GPS points tend to have some inherent imprecision,
// speed and bearing will likely be off, so the statistics for things like
// max speed will also be off.
float speed = location.distanceTo(lastLocation) * 1000.0f / timeDifference;
location.setSpeed(speed);
location.setBearing(lastSegmentLocation.bearingTo(location));
}
}
// Fill in the time
location.setTime(time);
// initialize start time with time of first track point
if (statsBuilder == null) {
statsBuilder = new TripStatisticsBuilder(time);
}
}
private void onAltitudeElementEnd() throws SAXException {
if (location != null) {
try {
location.setAltitude(Double.parseDouble(content));
} catch (NumberFormatException e) {
String msg = createErrorMessage("Unable to parse altitude: " + content);
throw new SAXException(msg, e);
}
}
}
/**
* Deletes the last track if it was not completely imported.
*/
public void rollbackUnfinishedTracks() {
if (isCurrentTrackRollbackable) {
providerUtils.deleteTrack(track.getId());
isCurrentTrackRollbackable = false;
}
}
/**
* Get all track ids of the tracks created by this importer run.
*
* @return array of track ids
*/
private long[] getImportedTrackIds() {
// Convert from java.lang.Long for convenience
long[] result = new long[tracksWritten.size()];
for (int i = 0; i < result.length; i++) {
result[i] = tracksWritten.get(i);
}
return result;
}
/**
* Returns the ID of the last point inserted into the database.
*/
private long getLastPointId() {
flushPointInserts();
return providerUtils.getLastLocationId(track.getId());
}
/**
* Builds a parsing error message with current line information.
*
* @param details details about the error, will be appended
* @return error message string with current line information
*/
private String createErrorMessage(String details) {
StringBuffer msg = new StringBuffer();
msg.append("Parsing error at line: ");
msg.append(locator.getLineNumber());
msg.append(" column: ");
msg.append(locator.getColumnNumber());
msg.append(". ");
msg.append(details);
return msg.toString();
}
}
@@ -0,0 +1,176 @@
/*
* Copyright 2008 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.file;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.io.file.TrackWriterFactory.TrackFileFormat;
import com.google.android.apps.mytracks.util.StringUtils;
import android.location.Location;
import android.os.Build;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.nio.charset.Charset;
import java.text.NumberFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;
/**
* Log of one track.
*
* @author Sandor Dornbush
*/
public class GpxTrackWriter implements TrackFormatWriter {
private static final String TIMESTAMP_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'";
private final NumberFormat elevationFormatter;
private final NumberFormat coordinateFormatter;
private final SimpleDateFormat timestampFormatter;
private PrintWriter pw = null;
private Track track;
public GpxTrackWriter() {
// GPX readers expect to see fractional numbers with US-style punctuation.
// That is, they want periods for decimal points, rather than commas.
elevationFormatter = NumberFormat.getInstance(Locale.US);
elevationFormatter.setMaximumFractionDigits(1);
elevationFormatter.setGroupingUsed(false);
coordinateFormatter = NumberFormat.getInstance(Locale.US);
coordinateFormatter.setMaximumFractionDigits(5);
coordinateFormatter.setMaximumIntegerDigits(3);
coordinateFormatter.setGroupingUsed(false);
timestampFormatter = new SimpleDateFormat(TIMESTAMP_FORMAT);
timestampFormatter.setTimeZone(TimeZone.getTimeZone("UTC"));
}
private String formatLocation(Location l) {
return "lat=\"" + coordinateFormatter.format(l.getLatitude())
+ "\" lon=\"" + coordinateFormatter.format(l.getLongitude()) + "\"";
}
@SuppressWarnings("hiding")
@Override
public void prepare(Track track, OutputStream out) {
this.track = track;
this.pw = new PrintWriter(out);
}
@Override
public String getExtension() {
return TrackFileFormat.GPX.getExtension();
}
@Override
public void writeHeader() {
if (pw != null) {
pw.format("<?xml version=\"1.0\" encoding=\"%s\" standalone=\"yes\"?>\n",
Charset.defaultCharset().name());
pw.println("<?xml-stylesheet type=\"text/xsl\" href=\"details.xsl\"?>");
pw.println("<gpx");
pw.println(" version=\"1.1\"");
pw.format(" creator=\"My Tracks running on %s\"\n", Build.MODEL);
pw.println(" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"");
pw.println(" xmlns=\"http://www.topografix.com/GPX/1/1\"");
pw.print(" xmlns:topografix=\"http://www.topografix.com/GPX/Private/"
+ "TopoGrafix/0/1\"");
pw.print(" xsi:schemaLocation=\"http://www.topografix.com/GPX/1/1 ");
pw.print("http://www.topografix.com/GPX/1/1/gpx.xsd ");
pw.print("http://www.topografix.com/GPX/Private/TopoGrafix/0/1 ");
pw.println("http://www.topografix.com/GPX/Private/TopoGrafix/0/1/"
+ "topografix.xsd\">");
// TODO: Author etc.
}
}
@Override
public void writeFooter() {
if (pw != null) {
pw.println("</gpx>");
}
}
@Override
public void writeBeginTrack(Location firstPoint) {
if (pw != null) {
pw.println("<trk>");
pw.println("<name>" + StringUtils.stringAsCData(track.getName())
+ "</name>");
pw.println("<desc>" + StringUtils.stringAsCData(track.getDescription())
+ "</desc>");
pw.println("<number>" + track.getId() + "</number>");
pw.println("<extensions><topografix:color>c0c0c0</topografix:color></extensions>");
}
}
@Override
public void writeEndTrack(Location lastPoint) {
if (pw != null) {
pw.println("</trk>");
}
}
@Override
public void writeOpenSegment() {
pw.println("<trkseg>");
}
@Override
public void writeCloseSegment() {
pw.println("</trkseg>");
}
@Override
public void writeLocation(Location l) {
if (pw != null) {
pw.println("<trkpt " + formatLocation(l) + ">");
Date d = new Date(l.getTime());
pw.println("<ele>" + elevationFormatter.format(l.getAltitude()) + "</ele>");
pw.println("<time>" + timestampFormatter.format(d) + "</time>");
pw.println("</trkpt>");
}
}
@Override
public void close() {
if (pw != null) {
pw.close();
pw = null;
}
}
@Override
public void writeWaypoint(Waypoint waypoint) {
if (pw != null) {
Location l = waypoint.getLocation();
if (l != null) {
pw.println("<wpt " + formatLocation(l) + ">");
pw.println("<ele>" + elevationFormatter.format(l.getAltitude()) + "</ele>");
pw.println("<time>" + timestampFormatter.format(l.getTime()) + "</time>");
pw.println("<name>" + StringUtils.stringAsCData(waypoint.getName())
+ "</name>");
pw.println("<desc>"
+ StringUtils.stringAsCData(waypoint.getDescription()) + "</desc>");
pw.println("</wpt>");
}
}
}
}
@@ -0,0 +1,216 @@
/*
* 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 static com.google.android.apps.mytracks.Constants.TAG;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.TracksColumns;
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.AlertDialog.Builder;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.ContentUris;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import javax.xml.parsers.ParserConfigurationException;
import org.xml.sax.SAXException;
/**
* Helper activity which imports tracks from a file.
*
* @author Rodrigo Damazio
*/
public class ImportActivity extends Activity {
private static final int PROGRESS_DIALOG = 1;
private static final int SUCCESS_DIALOG = 2;
private static final int FAILURE_DIALOG = 3;
private MyTracksProviderUtils providerUtils;
private ProgressDialog progressDialog;
private int resultMessage;
private long importedTrackIds[];
@Override
public void onCreate(Bundle savedState) {
super.onCreate(savedState);
providerUtils = MyTracksProviderUtils.Factory.get(this);
}
@Override
public void onStart() {
super.onStart();
Intent intent = getIntent();
String action = intent.getAction();
Uri data = intent.getData();
if (!(Intent.ACTION_VIEW.equals(action) || Intent.ACTION_ATTACH_DATA.equals(action))
|| !UriUtils.isFileUri(data)) {
Log.e(TAG, "Received an intent with unsupported action or data: " + intent);
finish();
return;
}
Log.i(TAG, "Importing GPX file at " + data);
startTrackImport(data.getPath());
}
@Override
protected Dialog onCreateDialog(int id) {
switch (id) {
case PROGRESS_DIALOG:
progressDialog = new ProgressDialog(this);
progressDialog.setIcon(android.R.drawable.ic_dialog_info);
progressDialog.setTitle(R.string.progress_title);
progressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
progressDialog.setMessage(getString(R.string.progress_message_import));
return progressDialog;
case SUCCESS_DIALOG:
final Builder dialogBuilder = new AlertDialog.Builder(this);
dialogBuilder.setCancelable(true);
dialogBuilder.setPositiveButton(android.R.string.ok, new OnClickListener() {
@Override
public void onClick(DialogInterface arg0, int arg1) {
finish();
}
});
dialogBuilder.setNegativeButton(R.string.import_show_track, new OnClickListener() {
@Override
public void onClick(DialogInterface arg0, int arg1) {
showImportedTrack();
finish();
}
});
return dialogBuilder.create();
case FAILURE_DIALOG:
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setPositiveButton(R.string.ok, new OnClickListener() {
@Override
public void onClick(DialogInterface arg0, int arg1) {
finish();
}
});
builder.setIcon(android.R.drawable.ic_dialog_alert);
builder.setTitle(R.string.error);
return builder.create();
}
return null;
}
@Override
protected void onPrepareDialog(int id, Dialog dialog) {
super.onPrepareDialog(id, dialog);
switch (id) {
case SUCCESS_DIALOG:
AlertDialog successDialog = (AlertDialog) dialog;
successDialog.setMessage(getString(R.string.import_success, importedTrackIds.length));
break;
case FAILURE_DIALOG:
AlertDialog failureDialog = (AlertDialog) dialog;
failureDialog.setMessage(getString(resultMessage));
break;
}
}
protected void showImportedTrack() {
long lastTrackId = importedTrackIds[importedTrackIds.length - 1];
Uri trackUri = ContentUris.withAppendedId(TracksColumns.CONTENT_URI, lastTrackId);
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(trackUri, TracksColumns.CONTENT_ITEMTYPE);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
finish();
}
private void startTrackImport(final String fileName) {
showDialog(PROGRESS_DIALOG);
Thread t = new Thread() {
@Override
public void run() {
resultMessage = R.string.success;
importedTrackIds = null;
try {
try {
InputStream is = new FileInputStream(fileName);
importedTrackIds = GpxImporter.importGPXFile(is, providerUtils);
} catch (SAXException e) {
Log.e(TAG, "Caught an unexpected exception.", e);
resultMessage = R.string.error_generic;
} catch (ParserConfigurationException e) {
Log.e(TAG, "Caught an unexpected exception.", e);
resultMessage = R.string.error_generic;
} catch (IOException e) {
Log.e(TAG, "Caught an unexpected exception.", e);
resultMessage = R.string.error_import_unable_to_read_file;
} catch (NullPointerException e) {
Log.e(TAG, "Caught an unexpected exception.", e);
resultMessage = R.string.error_import_invalid_gpx_format;
} catch (OutOfMemoryError e) {
Log.e(TAG, "Caught an unexpected exception.", e);
resultMessage = R.string.error_import_out_of_memory;
}
boolean success = (importedTrackIds != null && importedTrackIds.length > 0);
showImportResult(success);
} finally {
runOnUiThread(new Runnable() {
public void run() {
dismissDialog(PROGRESS_DIALOG);
}
});
}
}
};
t.start();
}
private void showImportResult(boolean success) {
final int dialogToShow = success ? SUCCESS_DIALOG : FAILURE_DIALOG;
// Show a dialog telling the user about the import, and asking if he wishes
// to open the track right away.
runOnUiThread(new Runnable() {
@Override
public void run() {
showDialog(dialogToShow);
}
});
}
}
@@ -0,0 +1,235 @@
/*
* Copyright 2008 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.file;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.io.file.TrackWriterFactory.TrackFileFormat;
import com.google.android.apps.mytracks.util.StringUtils;
import android.content.Context;
import android.location.Location;
import android.os.Build;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.util.Vector;
/**
* Write track as KML to a file.
*
* @author Leif Hendrik Wilden
*/
public class KmlTrackWriter implements TrackFormatWriter {
private final Vector<Double> distances = new Vector<Double>();
private final Vector<Double> elevations = new Vector<Double>();
private final StringUtils stringUtils;
private PrintWriter pw = null;
private Track track;
public KmlTrackWriter(Context context) {
stringUtils = new StringUtils(context);
}
/**
* Testing constructor.
*/
KmlTrackWriter(StringUtils stringUtils) {
this.stringUtils = stringUtils;
}
@SuppressWarnings("hiding")
@Override
public void prepare(Track track, OutputStream out) {
this.track = track;
this.pw = new PrintWriter(out);
}
@Override
public String getExtension() {
return TrackFileFormat.KML.getExtension();
}
@Override
public void writeHeader() {
if (pw != null) {
pw.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
pw.print("<kml");
pw.print(" xmlns=\"http://earth.google.com/kml/2.0\"");
pw.println(" xmlns:atom=\"http://www.w3.org/2005/Atom\">");
pw.println("<Document>");
pw.format("<atom:author><atom:name>My Tracks running on %s"
+ "</atom:name></atom:author>\n", Build.MODEL);
pw.println("<name>" + StringUtils.stringAsCData(track.getName())
+ "</name>");
pw.println("<description>"
+ StringUtils.stringAsCData(track.getDescription())
+ "</description>");
writeStyles();
}
}
@Override
public void writeFooter() {
if (pw != null) {
pw.println("</Document>");
pw.println("</kml>");
}
}
@Override
public void writeBeginTrack(Location firstPoint) {
if (pw != null) {
writePlacemark("(Start)", track.getDescription(), "#sh_green-circle",
firstPoint);
pw.println("<Placemark>");
pw.println("<name>" + StringUtils.stringAsCData(track.getName())
+ "</name>");
pw.println("<description>"
+ StringUtils.stringAsCData(track.getDescription())
+ "</description>");
pw.println("<styleUrl>#track</styleUrl>");
pw.println("<MultiGeometry>");
}
}
@Override
public void writeEndTrack(Location lastPoint) {
if (pw != null) {
pw.println("</MultiGeometry>");
pw.println("</Placemark>");
String description = stringUtils.generateTrackDescription(
track, distances, elevations);
writePlacemark("(End)", description, "#sh_red-circle", lastPoint);
}
}
@Override
public void writeOpenSegment() {
if (pw != null) {
pw.print("<LineString><coordinates>");
}
}
@Override
public void writeCloseSegment() {
if (pw != null) {
pw.println("</coordinates></LineString>");
}
}
@Override
public void writeLocation(Location l) {
if (pw != null) {
pw.print(l.getLongitude() + "," + l.getLatitude() + ","
+ l.getAltitude() + " ");
}
}
private String getPinStyle(Waypoint waypoint) {
if (waypoint.getType() == Waypoint.TYPE_STATISTICS) {
return "#sh_ylw-pushpin";
}
// Try to find the icon color.
// The string should be of the form:
// "http://maps.google.com/mapfiles/ms/micons/XXX.png"
int slash = waypoint.getIcon().lastIndexOf('/');
int png = waypoint.getIcon().lastIndexOf('.');
if ((slash != -1) && (slash < png)) {
String color = waypoint.getIcon().substring(slash + 1, png);
return "#sh_" + color + "-pushpin";
}
return "#sh_blue-pushpin";
}
@Override
public void writeWaypoint(Waypoint waypoint) {
if (pw != null) {
writePlacemark(
waypoint.getName(),
waypoint.getDescription(),
getPinStyle(waypoint),
waypoint.getLocation());
}
}
@Override
public void close() {
if (pw != null) {
pw.close();
pw = null;
}
}
private void writeStyles() {
pw.println("<Style id=\"track\"><LineStyle><color>7f0000ff</color>"
+ "<width>4</width></LineStyle></Style>");
pw.print("<Style id=\"sh_green-circle\"><IconStyle><scale>1.3</scale>");
pw.print("<Icon><href>http://maps.google.com/mapfiles/kml/paddle/"
+ "grn-circle.png</href></Icon>");
pw.println("<hotSpot x=\"32\" y=\"1\" xunits=\"pixels\" yunits=\"pixels\"/>"
+ "</IconStyle></Style>");
pw.print("<Style id=\"sh_red-circle\"><IconStyle><scale>1.3</scale>");
pw.print("<Icon><href>http://maps.google.com/mapfiles/kml/paddle/"
+ "red-circle.png</href></Icon>");
pw.println("<hotSpot x=\"32\" y=\"1\" xunits=\"pixels\" yunits=\"pixels\"/>"
+ "</IconStyle></Style>");
pw.print("<Style id=\"sh_ylw-pushpin\"><IconStyle><scale>1.3</scale>");
pw.print("<Icon><href>http://maps.google.com/mapfiles/kml/pushpin/"
+ "ylw-pushpin.png</href></Icon>");
pw.println("<hotSpot x=\"20\" y=\"2\" xunits=\"pixels\" yunits=\"pixels\"/>"
+ "</IconStyle></Style>");
pw.print("<Style id=\"sh_blue-pushpin\"><IconStyle><scale>1.3</scale>");
pw.print("<Icon><href>http://maps.google.com/mapfiles/kml/pushpin/"
+ "blue-pushpin.png</href></Icon>");
pw.println("<hotSpot x=\"20\" y=\"2\" xunits=\"pixels\" yunits=\"pixels\"/>"
+ "</IconStyle></Style>");
pw.print("<Style id=\"sh_green-pushpin\"><IconStyle><scale>1.3</scale>");
pw.print("<Icon><href>http://maps.google.com/mapfiles/kml/pushpin/"
+ "grn-pushpin.png</href></Icon>");
pw.println("<hotSpot x=\"20\" y=\"2\" xunits=\"pixels\" yunits=\"pixels\"/>"
+ "</IconStyle></Style>");
pw.print("<Style id=\"sh_red-pushpin\"><IconStyle><scale>1.3</scale>");
pw.print("<Icon><href>http://maps.google.com/mapfiles/kml/pushpin/"
+ "red-pushpin.png</href></Icon>");
pw.println("<hotSpot x=\"20\" y=\"2\" xunits=\"pixels\" yunits=\"pixels\"/>"
+ "</IconStyle></Style>");
}
private void writePlacemark(String name, String description, String style,
Location location) {
if (location != null) {
pw.println("<Placemark>");
pw.println(" <name>" + StringUtils.stringAsCData(name) + "</name>");
pw.println(" <description>" + StringUtils.stringAsCData(description)
+ "</description>");
pw.println(" <styleUrl>" + style + "</styleUrl>");
pw.println(" <Point>");
pw.println(" <coordinates>" + location.getLongitude() + ","
+ location.getLatitude() + "</coordinates>");
pw.println(" </Point>");
pw.println("</Placemark>");
}
}
}
@@ -0,0 +1,237 @@
/*
* 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 static com.google.android.apps.mytracks.Constants.TAG;
import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.TracksColumns;
import com.google.android.apps.mytracks.io.file.TrackWriterFactory.TrackFileFormat;
import com.google.android.apps.mytracks.util.FileUtils;
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.content.ContentUris;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnCancelListener;
import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import java.io.File;
/**
* Activity for saving a track to a file (and optionally sending that file).
*
* @author Rodrigo Damazio
*/
public class SaveActivity extends Activity {
public static final String EXTRA_SHARE_FILE = "share_file";
public static final String EXTRA_FILE_FORMAT = "file_format";
private static final int RESULT_DIALOG = 1;
/* VisibleForTesting */
static final int PROGRESS_DIALOG = 2;
private MyTracksProviderUtils providerUtils;
private long trackId;
private TrackWriter writer;
private boolean shareFile;
private TrackFileFormat format;
private WriteProgressController controller;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
providerUtils = MyTracksProviderUtils.Factory.get(this);
}
@Override
protected void onStart() {
super.onStart();
Intent intent = getIntent();
String action = intent.getAction();
String type = intent.getType();
Uri data = intent.getData();
if (!getString(R.string.save_intent_action).equals(action) ||
!TracksColumns.CONTENT_ITEMTYPE.equals(type) ||
!UriUtils.matchesContentUri(data, TracksColumns.CONTENT_URI)) {
Log.e(TAG, "Got bad save intent: " + intent);
finish();
return;
}
trackId = ContentUris.parseId(data);
int formatIdx = intent.getIntExtra(EXTRA_FILE_FORMAT, -1);
format = TrackFileFormat.values()[formatIdx];
shareFile = intent.getBooleanExtra(EXTRA_SHARE_FILE, false);
writer = TrackWriterFactory.newWriter(this, providerUtils, trackId, format);
if (writer == null) {
Log.e(TAG, "Unable to build writer");
finish();
return;
}
if (shareFile) {
// If the file is for sending, save it to a temporary location instead.
FileUtils fileUtils = new FileUtils();
String extension = format.getExtension();
String dirName = fileUtils.buildExternalDirectoryPath(extension, "tmp");
File dir = new File(dirName);
writer.setDirectory(dir);
}
controller = new WriteProgressController(this, writer, PROGRESS_DIALOG);
controller.setOnCompletionListener(new WriteProgressController.OnCompletionListener() {
@Override
public void onComplete() {
onWriteComplete();
}
});
controller.startWrite();
}
private void onWriteComplete() {
if (shareFile) {
shareWrittenFile();
} else {
showResultDialog();
}
}
private void shareWrittenFile() {
if (!writer.wasSuccess()) {
showResultDialog();
return;
}
// Share the file.
Intent shareIntent = new Intent(Intent.ACTION_SEND);
shareIntent.putExtra(Intent.EXTRA_SUBJECT,
getResources().getText(R.string.send_track_subject).toString());
shareIntent.putExtra(Intent.EXTRA_TEXT,
getResources().getText(R.string.send_track_body_format)
.toString());
shareIntent.setType(format.getMimeType());
Uri u = Uri.fromFile(new File(writer.getAbsolutePath()));
shareIntent.putExtra(Intent.EXTRA_STREAM, u);
shareIntent.putExtra(getString(R.string.track_id_broadcast_extra), trackId);
startActivity(Intent.createChooser(shareIntent,
getResources().getText(R.string.share_track).toString()));
}
private void showResultDialog() {
removeDialog(RESULT_DIALOG);
showDialog(RESULT_DIALOG);
}
@Override
protected Dialog onCreateDialog(int id) {
switch (id) {
case RESULT_DIALOG:
return createResultDialog();
case PROGRESS_DIALOG:
if (controller != null) {
return controller.createProgressDialog();
}
//$FALL-THROUGH$
default:
return super.onCreateDialog(id);
}
}
private Dialog createResultDialog() {
boolean success = writer.wasSuccess();
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setMessage(writer.getErrorMessage());
builder.setPositiveButton(R.string.ok, new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int arg1) {
dialog.dismiss();
finish();
}
});
builder.setOnCancelListener(new OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
dialog.dismiss();
finish();
}
});
builder.setIcon(success ? android.R.drawable.ic_dialog_info :
android.R.drawable.ic_dialog_alert);
builder.setTitle(success ? R.string.success : R.string.error);
return builder.create();
}
public static void handleExportTrackAction(Context ctx, long trackId, int actionCode) {
if (trackId < 0) {
return;
}
TrackFileFormat exportFormat = null;
switch (actionCode) {
case Constants.SAVE_GPX_FILE:
case Constants.SHARE_GPX_FILE:
exportFormat = TrackFileFormat.GPX;
break;
case Constants.SAVE_KML_FILE:
case Constants.SHARE_KML_FILE:
exportFormat = TrackFileFormat.KML;
break;
case Constants.SAVE_CSV_FILE:
case Constants.SHARE_CSV_FILE:
exportFormat = TrackFileFormat.CSV;
break;
case Constants.SAVE_TCX_FILE:
case Constants.SHARE_TCX_FILE:
exportFormat = TrackFileFormat.TCX;
break;
default:
throw new IllegalArgumentException("Warning unhandled action code: " + actionCode);
}
boolean shareFile = false;
switch (actionCode) {
case Constants.SHARE_GPX_FILE:
case Constants.SHARE_KML_FILE:
case Constants.SHARE_CSV_FILE:
case Constants.SHARE_TCX_FILE:
shareFile = true;
}
Uri uri = ContentUris.withAppendedId(TracksColumns.CONTENT_URI, trackId);
Intent intent = new Intent(ctx, SaveActivity.class);
intent.setAction(ctx.getString(R.string.save_intent_action));
intent.setDataAndType(uri, TracksColumns.CONTENT_ITEMTYPE);
intent.putExtra(EXTRA_FILE_FORMAT, exportFormat.ordinal());
intent.putExtra(EXTRA_SHARE_FILE, shareFile);
ctx.startActivity(intent);
}
}
@@ -0,0 +1,319 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.file;
import com.google.android.apps.mytracks.content.MyTracksLocation;
import com.google.android.apps.mytracks.content.Sensor;
import com.google.android.apps.mytracks.content.Sensor.SensorDataSet;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.io.file.TrackWriterFactory.TrackFileFormat;
import com.google.android.apps.mytracks.util.SystemUtils;
import android.content.Context;
import android.location.Location;
import android.os.Build;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.nio.charset.Charset;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;
/**
* Write out a a track in the Garmin training center database, tcx format.
* As defined by:
* http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2
*
* The TCX file written by this class has been verified as compatible with
* Garmin Training Center 3.5.3.
*
* @author Sandor Dornbush
* @author Dominik Ršttsches
*/
public class TcxTrackWriter implements TrackFormatWriter {
protected static final String TIMESTAMP_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'";
// These are the only sports allowed by the TCX v2 specification for fields
// of type Sport_t.
private static final String TCX_SPORT_BIKING = "Biking";
private static final String TCX_SPORT_RUNNING = "Running";
private static final String TCX_SPORT_OTHER = "Other";
// Values for fields of type Build_t/Type.
private static final String TCX_TYPE_RELEASE = "Release";
private static final String TCX_TYPE_INTERNAL = "Internal";
private final SimpleDateFormat timestampFormatter;
private final Context context;
private PrintWriter pw = null;
private Track track;
// Determines whether to encode cadence value as running or cycling cadence.
private boolean sportIsCycling;
public TcxTrackWriter(Context context) {
this.context = context;
timestampFormatter = new SimpleDateFormat(TIMESTAMP_FORMAT);
timestampFormatter.setTimeZone(TimeZone.getTimeZone("UTC"));
}
@SuppressWarnings("hiding")
@Override
public void prepare(Track track, OutputStream out) {
this.track = track;
this.pw = new PrintWriter(out);
this.sportIsCycling = categoryToTcxSport(track.getCategory()).equals(TCX_SPORT_BIKING);
}
@Override
public void close() {
if (pw != null) {
pw.close();
pw = null;
}
}
@Override
public String getExtension() {
return TrackFileFormat.TCX.getExtension();
}
@Override
public void writeHeader() {
if (pw == null) {
return;
}
pw.format("<?xml version=\"1.0\" encoding=\"%s\" standalone=\"no\" ?>\n",
Charset.defaultCharset().name());
pw.print("<TrainingCenterDatabase ");
pw.print("xmlns=\"http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2\" ");
pw.print("xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" ");
pw.print("xsi:schemaLocation=\"http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2 ");
pw.println("http://www.garmin.com/xmlschemas/TrainingCenterDatabasev2.xsd\">");
pw.println();
}
@Override
public void writeBeginTrack(Location firstPoint) {
if (pw == null) {
return;
}
String startTime = timestampFormatter.format(track.getStatistics().getStartTime());
pw.println(" <Activities>");
pw.format(" <Activity Sport=\"%s\">\n", categoryToTcxSport(track.getCategory()));
pw.format(" <Id>%s</Id>\n", startTime);
pw.format(" <Lap StartTime=\"%s\">\n", startTime);
pw.print(" <TotalTimeSeconds>");
pw.print(track.getStatistics().getTotalTime() / 1000);
pw.println("</TotalTimeSeconds>");
pw.print(" <DistanceMeters>");
pw.print(track.getStatistics().getTotalDistance());
pw.println("</DistanceMeters>");
// TODO max speed etc.
// Calories are a required element just put in 0.
pw.print("<Calories>0</Calories>");
pw.println("<Intensity>Active</Intensity>");
pw.println("<TriggerMethod>Manual</TriggerMethod>");
}
@Override
public void writeOpenSegment() {
if (pw != null) {
pw.println(" <Track>");
}
}
@Override
public void writeLocation(Location location) {
if (pw == null) {
return;
}
pw.println(" <Trackpoint>");
Date d = new Date(location.getTime());
pw.println(" <Time>" + timestampFormatter.format(d) + "</Time>");
pw.println(" <Position>");
pw.print(" <LatitudeDegrees>");
pw.print(location.getLatitude());
pw.println("</LatitudeDegrees>");
pw.print(" <LongitudeDegrees>");
pw.print(location.getLongitude());
pw.println("</LongitudeDegrees>");
pw.println(" </Position>");
pw.print(" <AltitudeMeters>");
pw.print(location.getAltitude());
pw.println("</AltitudeMeters>");
if (location instanceof MyTracksLocation) {
SensorDataSet sensorData = ((MyTracksLocation) location).getSensorDataSet();
if (sensorData != null) {
if (sensorData.hasHeartRate()
&& sensorData.getHeartRate().getState() == Sensor.SensorState.SENDING
&& sensorData.getHeartRate().hasValue()) {
pw.print(" <HeartRateBpm>");
pw.print("<Value>");
pw.print(sensorData.getHeartRate().getValue());
pw.print("</Value>");
pw.println("</HeartRateBpm>");
}
boolean cadenceAvailable = sensorData.hasCadence()
&& sensorData.getCadence().getState() == Sensor.SensorState.SENDING
&& sensorData.getCadence().hasValue();
// TCX Trackpoint_t contains a sequence. Thus, the legacy XML element
// <Cadence> needs to be put before <Extensions>.
// This field should only be used for the case that activity was marked as biking.
// Otherwise cadence is interpreted as running cadence data which
// is written in the <Extensions> as <RunCadence>.
if (sportIsCycling && cadenceAvailable) {
pw.print(" <Cadence>");
pw.print(Math.min(254, sensorData.getCadence().getValue()));
pw.println("</Cadence>");
}
boolean powerAvailable = sensorData.hasPower()
&& sensorData.getPower().getState() == Sensor.SensorState.SENDING
&& sensorData.getPower().hasValue();
if(powerAvailable || (!sportIsCycling && cadenceAvailable)) {
pw.print(" <Extensions>");
pw.print("<TPX xmlns=\"http://www.garmin.com/xmlschemas/ActivityExtension/v2\">");
// RunCadence needs to be put before power in order to be understood
// by Garmin Training Center.
if (!sportIsCycling && cadenceAvailable) {
pw.print("<RunCadence>");
pw.print(Math.min(254, sensorData.getCadence().getValue()));
pw.print("</RunCadence>");
}
if (powerAvailable) {
pw.print("<Watts>");
pw.print(sensorData.getPower().getValue());
pw.print("</Watts>");
}
pw.println("</TPX></Extensions>");
}
}
}
pw.println(" </Trackpoint>");
}
@Override
public void writeCloseSegment() {
if (pw != null) {
pw.println(" </Track>");
}
}
@Override
public void writeEndTrack(Location lastPoint) {
if (pw == null) {
return;
}
pw.println(" </Lap>");
pw.print(" <Creator xsi:type=\"Device_t\">");
pw.format("<Name>My Tracks running on %s</Name>\n", Build.MODEL);
// The following code is correct. ID is inconsistently capitalized in the
// TCX schema.
pw.println("<UnitId>0</UnitId>");
pw.println("<ProductID>0</ProductID>");
writeVersion();
pw.println("</Creator>");
pw.println(" </Activity>");
pw.println(" </Activities>");
}
@Override
public void writeFooter() {
if (pw == null) {
return;
}
pw.println(" <Author xsi:type=\"Application_t\">");
// We put the version in the name because there isn't a better place for
// it. The TCX schema tightly defined the Version tag, so we can't put it
// there. They've similarly constrained the PartNumber tag, so it can't go
// there either.
pw.format("<Name>My Tracks %s by Google</Name>\n", SystemUtils.getMyTracksVersion(context));
pw.println("<Build>");
writeVersion();
pw.format("<Type>%s</Type>\n", SystemUtils.isRelease(context) ? TCX_TYPE_RELEASE
: TCX_TYPE_INTERNAL);
pw.println("</Build>");
pw.format("<LangID>%s</LangID>\n", Locale.getDefault().getLanguage());
pw.println("<PartNumber>000-00000-00</PartNumber>");
pw.println("</Author>");
pw.println("</TrainingCenterDatabase>");
}
@Override
public void writeWaypoint(Waypoint waypoint) {
// TODO Write out the waypoints somewhere.
}
private void writeVersion() {
if (pw == null) {
return;
}
// Splitting the myTracks version code into VersionMajor, VersionMinor and BuildMajor
// to fit the integer type requirement for these fields in the TCX spec.
// Putting a string like "x.x.x" into VersionMajor breaks XML validation.
// We also set the BuildMinor version to 1 if this is a development build to
// signify that this build is newer than the one associated with the
// version code given in BuildMajor.
String[] myTracksVersionComponents = SystemUtils.getMyTracksVersion(context).split("\\.");
pw.println("<Version>");
pw.format("<VersionMajor>%d</VersionMajor>\n", Integer.valueOf(myTracksVersionComponents[0]));
pw.format("<VersionMinor>%d</VersionMinor>\n", Integer.valueOf(myTracksVersionComponents[1]));
// TCX schema says these are optional but http://connect.garmin.com only accepts
// the TCX file when they are present.
pw.format("<BuildMajor>%d</BuildMajor>\n", Integer.valueOf(myTracksVersionComponents[2]));
pw.format("<BuildMinor>%d</BuildMinor>\n", SystemUtils.isRelease(context) ? 0 : 1);
pw.println("</Version>");
}
private String categoryToTcxSport(String category) {
category = category.trim();
if (category.equalsIgnoreCase(TCX_SPORT_RUNNING)) {
return TCX_SPORT_RUNNING;
} else if (category.equalsIgnoreCase(TCX_SPORT_BIKING)) {
return TCX_SPORT_BIKING;
} else {
return TCX_SPORT_OTHER;
}
}
}
@@ -0,0 +1,78 @@
/*
* 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 static com.google.android.apps.mytracks.Constants.TAG;
import android.os.Environment;
import android.util.Log;
import java.io.File;
/**
* A class to clean up old temporary files.
* @author Sandor Dornbush
*/
public class TempFileCleaner {
private long currentTimeMillis;
public static void clean() {
(new TempFileCleaner(System.currentTimeMillis())).cleanImpl();
}
// @VisibleForTesting
TempFileCleaner(long time) {
currentTimeMillis = time;
}
private void cleanImpl() {
if (!Environment.getExternalStorageState().equals(
Environment.MEDIA_MOUNTED)) {
return; // Can't do anything now.
}
cleanTmpDirectory("csv");
cleanTmpDirectory("gpx");
cleanTmpDirectory("kml");
cleanTmpDirectory("tcx");
}
private void cleanTmpDirectory(String name) {
String sep = System.getProperty("file.separator");
cleanTmpDirectory(
new File(
Environment.getExternalStorageDirectory() + sep + name + sep
+ "tmp"));
}
// @VisibleForTesting
int cleanTmpDirectory(File dir) {
if (!dir.exists()) {
return 0;
}
int count = 0;
long oldest = currentTimeMillis - 1000 * 3600;
for (File f : dir.listFiles()) {
if (f.lastModified() < oldest) {
if (!f.delete()) {
Log.w(TAG, "Failed to delete file " + f.getAbsolutePath());
}
count++;
}
}
return count;
}
}
@@ -0,0 +1,113 @@
/*
* Copyright 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.file;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.Waypoint;
import android.location.Location;
import java.io.OutputStream;
/**
* Interface for writing data to a specific track file format.
*
* The expected sequence of calls is:
* <ol>
* <li>{@link #prepare}
* <li>{@link #writeHeader}
* <li>{@link #writeBeginTrack}
* <li>For each segment:
* <ol>
* <li>{@link #writeOpenSegment}
* <li>For each location in the segment: {@link #writeLocation}
* <li>{@link #writeCloseSegment}
* </ol>
* <li>{@link #writeEndTrack}
* <li>For each waypoint: {@link #writeWaypoint}
* <li>{@link #writeFooter}
* </ol>
*
* @author Rodrigo Damazio
*/
public interface TrackFormatWriter {
/**
* Sets up the writer to write the given track to the given output.
*
* @param track the track to write
* @param out the stream to write the track contents to
*/
void prepare(Track track, OutputStream out);
/**
* @return The file extension (i.e. gpx, kml, ...)
*/
String getExtension();
/**
* Writes the header.
* This is chance for classes to write out opening information.
*/
void writeHeader();
/**
* Writes the footer.
* This is chance for classes to write out closing information.
*/
void writeFooter();
/**
* Write the given location object.
*
* TODO Add some flexible handling of other sensor data.
*
* @param location the location to write
*/
void writeLocation(Location location) throws InterruptedException;
/**
* Write a way point.
*
* @param waypoint
*/
void writeWaypoint(Waypoint waypoint);
/**
* Write the beginning of a track.
*/
void writeBeginTrack(Location firstPoint);
/**
* Write the end of a track.
*/
void writeEndTrack(Location lastPoint);
/**
* Write the statements necessary to open a new segment.
*/
void writeOpenSegment();
/**
* Write the statements necessary to close a segment.
*/
void writeCloseSegment();
/**
* Close the underlying file handle.
*/
void close();
}
@@ -0,0 +1,92 @@
/*
* 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 java.io.File;
/**
* Implementations of this class export tracks to the SD card. This class is
* intended to be format-neutral - it handles creating the output file and
* reading the track to be exported, but requires an instance of
* {@link TrackFormatWriter} to actually format the data.
*
* @author Sandor Dornbush
* @author Rodrigo Damazio
*/
public interface TrackWriter {
/** This listener is used to signal completion of track write */
public interface OnCompletionListener {
public void onComplete();
}
/** This listener is used to signal track writes. */
public interface OnWriteListener {
/**
* This method is invoked whenever a location within a track is written.
* @param number the location number
* @param max the maximum number of locations, for calculation of
* completion percentage
*/
public void onWrite(int number, int max);
}
/**
* Sets listener to be invoked when the writer has finished.
*/
void setOnCompletionListener(OnCompletionListener onCompletionListener);
/**
* Sets a listener to be invoked for each location writer.
*/
void setOnWriteListener(OnWriteListener onWriteListener);
/**
* Sets a custom directory where the file will be written.
*/
void setDirectory(File directory);
/**
* Returns the absolute path to the file which was created.
*/
String getAbsolutePath();
/**
* Writes the given track id to the SD card.
* This is non-blocking.
*/
void writeTrackAsync();
/**
* Writes the given track id to the SD card.
* This is blocking.
*/
void writeTrack();
/**
* Stop any in-progress writes
*/
void stopWriteTrack();
/**
* Returns true if the write completed successfully.
*/
boolean wasSuccess();
/**
* Returns the error message (if any) generated by a writer failure.
*/
int getErrorMessage();
}
@@ -0,0 +1,121 @@
/*
* 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 static com.google.android.apps.mytracks.Constants.TAG;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.Track;
import android.content.Context;
import android.util.Log;
/**
* A factory to produce track writers for any format.
*
* @author Rodrigo Damazio
*/
public class TrackWriterFactory {
/**
* Definition of all possible track formats.
*/
public enum TrackFileFormat {
GPX {
@Override
TrackFormatWriter newFormatWriter(Context context) {
return new GpxTrackWriter();
}
},
KML {
@Override
TrackFormatWriter newFormatWriter(Context context) {
return new KmlTrackWriter(context);
}
},
CSV {
@Override
public TrackFormatWriter newFormatWriter(Context context) {
return new CsvTrackWriter();
}
},
TCX {
@Override
public TrackFormatWriter newFormatWriter(Context context) {
return new TcxTrackWriter(context);
}
};
/**
* Creates and returns a new format writer for each format.
*/
abstract TrackFormatWriter newFormatWriter(Context context);
/**
* Returns the mime type for each format.
*/
public String getMimeType() {
return "application/" + getExtension() + "+xml";
}
/**
* Returns the file extension for each format.
*/
public String getExtension() {
return this.name().toLowerCase();
}
}
/**
* Creates a new track writer to write the track with the given ID.
*
* @param context the context in which the track will be read
* @param providerUtils the data provider utils to read the track with
* @param trackId the ID of the track to be written
* @param format the output format to write in
* @return the new track writer
*/
public static TrackWriter newWriter(Context context,
MyTracksProviderUtils providerUtils,
long trackId, TrackFileFormat format) {
Track track = providerUtils.getTrack(trackId);
if (track == null) {
Log.w(TAG, "Trying to create a writer for an invalid track, id=" + trackId);
return null;
}
return newWriter(context, providerUtils, track, format);
}
/**
* Creates a new track writer to write the given track.
*
* @param context the context in which the track will be read
* @param providerUtils the data provider utils to read the track with
* @param track the track to be written
* @param format the output format to write in
* @return the new track writer
*/
private static TrackWriter newWriter(Context context,
MyTracksProviderUtils providerUtils,
Track track, TrackFileFormat format) {
TrackFormatWriter writer = format.newFormatWriter(context);
return new TrackWriterImpl(context, providerUtils, track, writer);
}
private TrackWriterFactory() { }
}
@@ -0,0 +1,391 @@
/*
* Copyright 2008 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.io.file;
import static com.google.android.apps.mytracks.Constants.TAG;
import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.content.MyTracksLocation;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils.LocationIterator;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.util.FileUtils;
import com.google.android.apps.mytracks.util.LocationUtils;
import com.google.android.maps.mytracks.R;
import android.app.Activity;
import android.content.Context;
import android.database.Cursor;
import android.location.Location;
import android.util.Log;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.OutputStream;
/**
* This class exports tracks to the SD card. It is intended to be format-
* neutral -- it handles creating the output file and reading the track to be
* exported, but requires an instance of {@link TrackFormatWriter} to actually
* format the data.
*
* @author Sandor Dornbush
* @author Rodrigo Damazio
*/
class TrackWriterImpl implements TrackWriter {
private final Context context;
private final MyTracksProviderUtils providerUtils;
private final Track track;
private final TrackFormatWriter writer;
private final FileUtils fileUtils;
private boolean success = false;
private int errorMessage = -1;
private File directory = null;
private File file = null;
private OnCompletionListener onCompletionListener;
private OnWriteListener onWriteListener;
private Thread writeThread;
TrackWriterImpl(Context context, MyTracksProviderUtils providerUtils,
Track track, TrackFormatWriter writer) {
this.context = context;
this.providerUtils = providerUtils;
this.track = track;
this.writer = writer;
this.fileUtils = new FileUtils();
}
@Override
public void setOnCompletionListener(OnCompletionListener onCompletionListener) {
this.onCompletionListener = onCompletionListener;
}
@Override
public void setOnWriteListener(OnWriteListener onWriteListener) {
this.onWriteListener = onWriteListener;
}
@Override
public void setDirectory(File directory) {
this.directory = directory;
}
@Override
public String getAbsolutePath() {
return file.getAbsolutePath();
}
@Override
public void writeTrackAsync() {
writeThread = new Thread() {
@Override
public void run() {
doWriteTrack();
}
};
writeThread.start();
}
@Override
public void writeTrack() {
writeTrackAsync();
try {
writeThread.join();
} catch (InterruptedException e) {
Log.e(Constants.TAG, "Interrupted waiting for write to complete", e);
}
}
private void doWriteTrack() {
// Open the input and output
success = false;
errorMessage = R.string.error_export_generic;
if (track != null) {
if (openFile()) {
try {
writeDocument();
} catch (InterruptedException e) {
Log.i(Constants.TAG, "The track write was interrupted");
if (file != null) {
if (!file.delete()) {
Log.w(TAG, "Failed to delete file " + file.getAbsolutePath());
}
}
success = false;
errorMessage = R.string.error_export_operation_canceled;
}
}
}
finished();
}
public void stopWriteTrack() {
if (writeThread != null && writeThread.isAlive()) {
Log.i(Constants.TAG, "Attempting to stop track write");
writeThread.interrupt();
try {
writeThread.join();
Log.i(Constants.TAG, "Track write stopped");
} catch (InterruptedException e) {
Log.e(Constants.TAG, "Failed to wait for writer to stop", e);
}
}
}
@Override
public int getErrorMessage() {
return errorMessage;
}
@Override
public boolean wasSuccess() {
return success;
}
/*
* Helper methods:
* ===============
*/
private void finished() {
if (onCompletionListener != null) {
runOnUiThread(new Runnable() {
@Override
public void run() {
onCompletionListener.onComplete();
}
});
return;
}
}
/**
* Runs the given runnable in the UI thread.
*/
protected void runOnUiThread(Runnable runnable) {
if (context instanceof Activity) {
((Activity) context).runOnUiThread(runnable);
}
}
/**
* Opens the file and prepares the format writer for it.
*
* @return true on success, false otherwise (and errorMessage is set)
*/
protected boolean openFile() {
if (!canWriteFile()) {
return false;
}
// Make sure the file doesn't exist yet (possibly by changing the filename)
String fileName = fileUtils.buildUniqueFileName(
directory, track.getName(), writer.getExtension());
if (fileName == null) {
Log.e(Constants.TAG,
"Unable to get a unique filename for " + track.getName());
return false;
}
Log.i(Constants.TAG, "Writing track to: " + fileName);
try {
writer.prepare(track, newOutputStream(fileName));
} catch (FileNotFoundException e) {
Log.e(Constants.TAG, "Failed to open output file.", e);
errorMessage = R.string.io_write_failed;
return false;
}
return true;
}
/**
* Checks and returns whether we're ready to create the output file.
*/
protected boolean canWriteFile() {
if (directory == null) {
String dirName =
fileUtils.buildExternalDirectoryPath(writer.getExtension());
directory = newFile(dirName);
}
if (!fileUtils.isSdCardAvailable()) {
Log.i(Constants.TAG, "Could not find SD card.");
errorMessage = R.string.io_no_external_storage_found;
return false;
}
if (!fileUtils.ensureDirectoryExists(directory)) {
Log.i(Constants.TAG, "Could not create export directory.");
errorMessage = R.string.io_create_dir_failed;
return false;
}
return true;
}
/**
* Creates a new output stream to write to the given filename.
*
* @throws FileNotFoundException if the file could't be created
*/
protected OutputStream newOutputStream(String fileName)
throws FileNotFoundException {
file = new File(directory, fileName);
return new FileOutputStream(file);
}
/**
* Creates a new file object for the given path.
*/
protected File newFile(String path) {
return new File(path);
}
/**
* Writes the waypoints for the given track.
*
* @param trackId the ID of the track to write waypoints for
*/
private void writeWaypoints(long trackId) {
// TODO: Stream through he 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.
Cursor cursor = null;
cursor = providerUtils.getWaypointsCursor(trackId, 0,
Constants.MAX_LOADED_WAYPOINTS_POINTS);
if (cursor != null) {
try {
if (cursor.moveToFirst()) {
// Yes, this will skip the 1st way point and that is intentional
// as the 1st points holds the stats for the current/last segment.
while (cursor.moveToNext()) {
Waypoint wpt = providerUtils.createWaypoint(cursor);
writer.writeWaypoint(wpt);
}
}
} finally {
cursor.close();
}
}
}
/**
* Does the actual work of writing the track to the now open file.
*/
void writeDocument() throws InterruptedException {
Log.d(Constants.TAG, "Started writing track.");
writer.writeHeader();
writeWaypoints(track.getId());
writeLocations();
writer.writeFooter();
writer.close();
success = true;
Log.d(Constants.TAG, "Done writing track.");
errorMessage = R.string.io_write_finished;
}
private void writeLocations() throws InterruptedException {
boolean wroteFirst = false;
boolean segmentOpen = false;
boolean isLastValid = false;
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 tmpLoc = lastLocation;
lastLocation = currentLocation;
currentLocation = tmpLoc;
if (currentLocation != null) {
currentLocation.reset();
}
}
};
TrackWriterLocationFactory locationFactory = new TrackWriterLocationFactory();
LocationIterator it = providerUtils.getLocationIterator(track.getId(), 0, false,
locationFactory);
try {
if (!it.hasNext()) {
Log.w(Constants.TAG, "Unable to get any points to write");
return;
}
int pointNumber = 0;
while (it.hasNext()) {
Location loc = it.next();
if (Thread.interrupted()) {
throw new InterruptedException();
}
pointNumber++;
boolean isValid = LocationUtils.isValidLocation(loc);
boolean validSegment = isValid && isLastValid;
if (!wroteFirst && validSegment) {
// Found the first two consecutive points which are valid
writer.writeBeginTrack(locationFactory.lastLocation);
wroteFirst = true;
}
if (validSegment) {
if (!segmentOpen) {
// Start a segment for this point
writer.writeOpenSegment();
segmentOpen = true;
// Write the previous point, which we had previously skipped
writer.writeLocation(locationFactory.lastLocation);
}
// Write the current point
writer.writeLocation(loc);
if (onWriteListener != null) {
onWriteListener.onWrite(pointNumber, track.getNumberOfPoints());
}
} else {
if (segmentOpen) {
writer.writeCloseSegment();
segmentOpen = false;
}
}
locationFactory.swapLocations();
isLastValid = isValid;
}
if (segmentOpen) {
writer.writeCloseSegment();
segmentOpen = false;
}
if (wroteFirst) {
writer.writeEndTrack(locationFactory.lastLocation);
}
} finally {
it.close();
}
}
}
@@ -0,0 +1,132 @@
/*
* 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.maps.mytracks.R;
import android.app.Activity;
import android.app.ProgressDialog;
import android.content.DialogInterface;
/**
* Given a {@link TrackWriter}, this class manages the process of writing the
* data in the track represented by the writer. This includes the display of
* a progress dialog, updating the progress bar in said dialog, and notifying
* interested parties when the write completes.
*
* @author Matthew Simmons
*/
class WriteProgressController {
/**
* This listener is used to notify interested parties when the write has
* completed.
*/
public interface OnCompletionListener {
/**
* When this method is invoked, the write has completed, and the progress
* dialog has been dismissed. Whether the write succeeded can be
* determined by examining the {@link TrackWriter}.
*/
public void onComplete();
}
private final Activity activity;
private final TrackWriter writer;
private ProgressDialog dialog;
private OnCompletionListener onCompletionListener;
private final int progressDialogId;
/**
* @param activity the activity associated with this write
* @param writer the writer which writes the track to disk. Note that this
* class will use the writer's completion listener. If callers are
* interested in notification upon completion of the write, they should
* use {@link #setOnCompletionListener}.
*/
public WriteProgressController(Activity activity, TrackWriter writer, int progressDialogId) {
this.activity = activity;
this.writer = writer;
this.progressDialogId = progressDialogId;
writer.setOnCompletionListener(writerCompleteListener);
writer.setOnWriteListener(writerWriteListener);
}
/** Set a listener to be invoked when the write completes. */
public void setOnCompletionListener(OnCompletionListener onCompletionListener) {
this.onCompletionListener = onCompletionListener;
}
// For testing purpose
OnCompletionListener getOnCompletionListener() {
return onCompletionListener;
}
public ProgressDialog createProgressDialog() {
dialog = new ProgressDialog(activity);
dialog.setIcon(android.R.drawable.ic_dialog_info);
dialog.setTitle(activity.getString(R.string.progress_title));
dialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
dialog.setMessage(activity.getString(R.string.progress_message_export));
dialog.setIndeterminate(true);
dialog.setOnCancelListener(dialogCancelListener);
return dialog;
}
/** Initiate an asynchronous write. */
public void startWrite() {
activity.showDialog(progressDialogId);
writer.writeTrackAsync();
}
/** VisibleForTesting */
ProgressDialog getDialog() {
return dialog;
}
private final DialogInterface.OnCancelListener dialogCancelListener =
new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialogInterface) {
writer.stopWriteTrack();
}
};
private final TrackWriter.OnCompletionListener writerCompleteListener =
new TrackWriter.OnCompletionListener() {
@Override
public void onComplete() {
activity.dismissDialog(progressDialogId);
if (onCompletionListener != null) {
onCompletionListener.onComplete();
}
}
};
private final TrackWriter.OnWriteListener writerWriteListener =
new TrackWriter.OnWriteListener() {
@Override
public void onWrite(int number, int max) {
if (number % 500 == 0) {
dialog.setIndeterminate(false);
dialog.setMax(max);
dialog.setProgress(Math.min(number, max));
}
}
};
}
@@ -0,0 +1,461 @@
/*
* 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.Config;
import android.util.Log;
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;
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;
/**
* 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 boolean LOCAL_LOGV = DEBUG ? Config.LOGD : Config.LOGV;
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;
try {
encodedUri = URLEncoder.encode(uri, "UTF-8");
} catch (UnsupportedEncodingException uee) {
// should not happen.
Log.e("JakartaGDataClient", "UTF-8 not supported -- should not happen. "
+ "Using default encoding.", uee);
encodedUri = URLEncoder.encode(uri);
}
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 (LOCAL_LOGV) {
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,50 @@
/*
* 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) {
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,309 @@
/*
* 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.gdata;
import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.io.AuthManager;
import com.google.android.apps.mytracks.io.AuthManager.AuthCallback;
import android.util.Log;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* GDataWrapper provides a wrapper around GData operations that maintains the
* GData client, and provides a method to run GData queries with proper error
* handling. After a query is run, the wrapper can be queried about the error
* that occurred.
*
* @param C the GData service client
* @author Sandor Dornbush
*/
public class GDataWrapper<C> {
public static class AuthenticationException extends Exception {
private Exception exception;
private static final long serialVersionUID = 1L;
public AuthenticationException(Exception caught) {
this.exception = caught;
}
public Exception getException() {
return exception;
}
};
public static class ParseException extends Exception {
private Exception exception;
private static final long serialVersionUID = 1L;
public ParseException(Exception caught) {
this.exception = caught;
}
public Exception getException() {
return exception;
}
};
public static class ConflictDetectedException extends Exception {
private Exception exception;
private static final long serialVersionUID = 1L;
public ConflictDetectedException(Exception caught) {
this.exception = caught;
}
public Exception getException() {
return exception;
}
};
public static class HttpException extends Exception {
private static final long serialVersionUID = 1L;
private int statusCode;
private String statusMessage;
public HttpException(int statusCode, String statusMessage) {
super();
this.statusCode = statusCode;
this.statusMessage = statusMessage;
}
public int getStatusCode() {
return statusCode;
}
public String getStatusMessage() {
return statusMessage;
}
};
/**
* A QueryFunction is passed in when executing a query. The query function of
* the class is called with the GData client as a parameter. The function
* should execute whatever operations it desires on the client without concern
* for whether the client will throw an error.
*/
public interface QueryFunction<C> {
public abstract void query(C client)
throws AuthenticationException, IOException, ParseException,
ConflictDetectedException, HttpException;
}
/**
* A AuthenticatedFunction is passed in when executing the google
* authenticated service. The authenticated function of the class is called
* with the current authentication token for the service. The function should
* execute whatever operations with the google service without concern for
* whether the client will throw an error.
*/
public interface AuthenticatedFunction {
public abstract void run(String authenticationToken)
throws AuthenticationException, IOException;
}
// The types of error that may be encountered
// No error occurred.
public static final int ERROR_NO_ERROR = 0;
// There was an authentication error, the auth token may be invalid.
public static final int ERROR_AUTH = 1;
// There was an internal error on the server side.
public static final int ERROR_INTERNAL = 2;
// There was an error connecting to the server.
public static final int ERROR_CONNECTION = 3;
// The item queried did not exit.
public static final int ERROR_NOT_FOUND = 4;
// There was an error parsing or serializing locally.
public static final int ERROR_LOCAL = 5;
// There was a conflict, update the entry and try again.
public static final int ERROR_CONFLICT = 6;
// A query was run after cleaning up the wrapper, so the client was invalid.
public static final int ERROR_CLEANED_UP = 7;
// An unknown error occurred.
public static final int ERROR_UNKNOWN = 100;
private static final int AUTH_TOKEN_INVALIDATE_REFRESH_NUM_RETRIES = 1;
private static final int AUTH_TOKEN_INVALIDATE_REFRESH_TIMEOUT = 5000;
private String errorMessage;
private int errorType;
private C gdataServiceClient;
private AuthManager auth;
private boolean retryOnAuthFailure;
public GDataWrapper() {
errorType = ERROR_NO_ERROR;
errorMessage = null;
auth = null;
retryOnAuthFailure = false;
}
public void setClient(C gdataServiceClient) {
this.gdataServiceClient = gdataServiceClient;
}
public boolean runAuthenticatedFunction(
final AuthenticatedFunction function) {
return runCommon(function, null);
}
public boolean runQuery(final QueryFunction<C> query) {
return runCommon(null, query);
}
/**
* Runs an arbitrary piece of code.
*/
private boolean runCommon(final AuthenticatedFunction function,
final QueryFunction<C> query) {
for (int i = 0; i <= AUTH_TOKEN_INVALIDATE_REFRESH_NUM_RETRIES; i++) {
runOne(function, query);
if (errorType == ERROR_NO_ERROR) {
return true;
}
Log.d(Constants.TAG, "GData error encountered: " + errorMessage);
if (errorType == ERROR_AUTH && auth != null) {
if (!retryOnAuthFailure || !invalidateAndRefreshAuthToken()) {
return false;
}
}
Log.d(Constants.TAG, "retrying function/query");
}
return false;
}
/**
* Execute a given function or query. If one is executed, errorType and
* errorMessage will contain the result/status of the function/query.
*/
private void runOne(final AuthenticatedFunction function,
final QueryFunction<C> query) {
try {
if (function != null) {
function.run(this.auth.getAuthToken());
} else if (query != null) {
query.query(gdataServiceClient);
} else {
throw new IllegalArgumentException(
"invalid invocation of runOne; one of function/query " +
"must be non-null");
}
errorType = ERROR_NO_ERROR;
errorMessage = null;
} catch (AuthenticationException e) {
Log.e(Constants.TAG, "AuthenticationException", e);
errorType = ERROR_AUTH;
errorMessage = e.getMessage();
} catch (HttpException e) {
Log.e(Constants.TAG,
"HttpException, code " + e.getStatusCode() + " message " + e.getMessage(), e);
errorMessage = e.getMessage();
if (e.getStatusCode() == 401) {
errorType = ERROR_AUTH;
} else {
errorType = ERROR_CONNECTION;
}
} catch (FileNotFoundException e) {
Log.e(Constants.TAG, "Exception", e);
errorType = ERROR_AUTH;
errorMessage = e.getMessage();
} catch (IOException e) {
Log.e(Constants.TAG, "Exception", e);
errorMessage = e.getMessage();
if (errorMessage != null && errorMessage.contains("503")) {
errorType = ERROR_INTERNAL;
} else {
errorType = ERROR_CONNECTION;
}
} catch (ParseException e) {
Log.e(Constants.TAG, "Exception", e);
errorType = ERROR_LOCAL;
errorMessage = e.getMessage();
} catch (ConflictDetectedException e) {
Log.e(Constants.TAG, "Exception", e);
errorType = ERROR_CONFLICT;
errorMessage = e.getMessage();
}
}
/**
* Invalidates and refreshes the auth token. Blocks until the refresh has
* completed or until we deem the refresh as having timed out.
*
* @return true If the invalidate/refresh succeeds, false if it fails or
* times out.
*/
private boolean invalidateAndRefreshAuthToken() {
Log.d(Constants.TAG, "Retrying due to auth failure");
// This FutureTask doesn't do anything -- it exists simply to be
// blocked upon using get().
final FutureTask<?> whenFinishedFuture = new FutureTask<Object>(new Runnable() {
public void run() {}
}, null);
final AtomicBoolean finalSuccess = new AtomicBoolean(false);
auth.invalidateAndRefresh(new AuthCallback() {
@Override
public void onAuthResult(boolean success) {
finalSuccess.set(success);
whenFinishedFuture.run();
}
});
try {
Log.d(Constants.TAG, "waiting for invalidate");
whenFinishedFuture.get(AUTH_TOKEN_INVALIDATE_REFRESH_TIMEOUT,
TimeUnit.MILLISECONDS);
boolean success = finalSuccess.get();
Log.d(Constants.TAG, "invalidate finished, success = " + success);
return success;
} catch (InterruptedException e) {
Log.e(Constants.TAG, "Failed to invalidate", e);
} catch (ExecutionException e) {
Log.e(Constants.TAG, "Failed to invalidate", e);
} catch (TimeoutException e) {
Log.e(Constants.TAG, "Invalidate didn't complete in time", e);
} finally {
whenFinishedFuture.cancel(false);
}
return false;
}
public int getErrorType() {
return errorType;
}
public String getErrorMessage() {
return errorMessage;
}
public void setAuthManager(AuthManager auth) {
this.auth = auth;
}
public AuthManager getAuthManager() {
return auth;
}
public void setRetryOnAuthFailure(boolean retry) {
retryOnAuthFailure = retry;
}
}
@@ -0,0 +1,106 @@
/*
* 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 android.util.Log;
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.
Log.w("QueryParamsImpl", "UTF-8 not supported -- should not happen. "
+ "Using default encoding.", uee);
encodedValue = URLEncoder.encode(value);
}
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,50 @@
// Copyright 2009 Google Inc. All Rights Reserved.
package com.google.android.apps.mytracks.io.mymaps;
import com.google.android.maps.GeoPoint;
/**
* A rectangle in geographical space.
*/
class GeoRect {
public int top;
public int left;
public int bottom;
public int right;
public GeoRect() {
top = 0;
left = 0;
bottom = 0;
right = 0;
}
public GeoRect(GeoPoint center, int latSpan, int longSpan) {
top = center.getLatitudeE6() - latSpan / 2;
left = center.getLongitudeE6() - longSpan / 2;
bottom = center.getLatitudeE6() + latSpan / 2;
right = center.getLongitudeE6() + longSpan / 2;
}
public GeoPoint getCenter() {
return new GeoPoint(top / 2 + bottom / 2, left / 2 + right / 2);
}
public int getLatSpan() {
return bottom - top;
}
public int getLongSpan() {
return right - left;
}
public boolean contains(GeoPoint geoPoint) {
if (geoPoint.getLatitudeE6() >= top
&& geoPoint.getLatitudeE6() <= bottom
&& geoPoint.getLongitudeE6() >= left
&& geoPoint.getLongitudeE6() <= right) {
return true;
}
return false;
}
}
@@ -0,0 +1,36 @@
// Copyright 2010 Google Inc. All Rights Reserved.
package com.google.android.apps.mytracks.io.mymaps;
import com.google.wireless.gdata.data.Entry;
import java.util.HashMap;
import java.util.Map;
/**
* GData entry for a map feature.
*/
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,101 @@
// Copyright 2010 Google Inc. All Rights Reserved.
package com.google.android.apps.mytracks.io.mymaps;
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;
/**
* Client to talk to Google Maps via GData.
*/
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 MyMapsConstants.SERVICE_NAME;
}
public static String buildMapUrl(String mapId) {
return MyMapsConstants.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(MAPS_VERSION_FEED_PATH_FORMAT,
MAPS_BASE_FEED_URL, versionUserId,
versionClient, currentVersion);
}
}
@@ -0,0 +1,372 @@
// Copyright 2011 Google Inc. All Rights Reserved.
package com.google.android.apps.mytracks.io.mymaps;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.io.AuthManager;
import com.google.android.maps.GeoPoint;
import com.google.android.maps.mytracks.R;
import com.google.wireless.gdata.client.HttpException;
import com.google.wireless.gdata.data.Entry;
import com.google.wireless.gdata.parser.GDataParser;
import com.google.wireless.gdata.parser.ParseException;
import android.content.Context;
import android.location.Location;
import android.text.TextUtils;
import android.util.Log;
import java.io.IOException;
import java.util.Collection;
import org.xmlpull.v1.XmlPullParserException;
/**
* Single interface which abstracts all access to the Google Maps service.
*
* @author Rodrigo Damazio
*/
public class MapsFacade {
/**
* Interface for receiving data back from getMapsList.
* All calls to the interface will happen before getMapsList returns.
*/
public interface MapsListCallback {
void onReceivedMapListing(String mapId, String title, String description,
boolean isPublic);
}
private static final String END_ICON_URL =
"http://maps.google.com/mapfiles/ms/micons/red-dot.png";
private static final String START_ICON_URL =
"http://maps.google.com/mapfiles/ms/micons/green-dot.png";
private final Context context;
private final MyMapsGDataWrapper wrapper;
private final MyMapsGDataConverter gdataConverter;
private final String authToken;
public MapsFacade(Context context, AuthManager auth) {
this.context = context;
this.authToken = auth.getAuthToken();
wrapper = new MyMapsGDataWrapper(context, auth);
wrapper.setRetryOnAuthFailure(true);
try {
gdataConverter = new MyMapsGDataConverter();
} catch (XmlPullParserException e) {
throw new IllegalStateException("Unable to create maps data converter", e);
}
}
public static String buildMapUrl(String mapId) {
return MapsClient.buildMapUrl(mapId);
}
/**
* Returns a list of all maps for the current user.
*
* @param callback callback to call for each map returned
* @return true on success, false otherwise
*/
public boolean getMapsList(final MapsListCallback callback) {
return wrapper.runQuery(new MyMapsGDataWrapper.QueryFunction() {
@Override
public void query(MapsClient client) throws IOException, Exception {
GDataParser listParser = client.getParserForFeed(
MapFeatureEntry.class, MapsClient.getMapsFeed(), authToken);
listParser.init();
while (listParser.hasMoreData()) {
MapFeatureEntry entry =
(MapFeatureEntry) listParser.readNextEntry(null);
MyMapsMapMetadata metadata =
MyMapsGDataConverter.getMapMetadataForEntry(entry);
String mapId = MyMapsGDataConverter.getMapidForEntry(entry);
callback.onReceivedMapListing(
mapId, metadata.getTitle(), metadata.getDescription(), metadata.getSearchable());
}
listParser.close();
listParser = null;
}
});
}
/**
* Creates a new map for the current user.
*
* @param title title of the map
* @param category category of the map
* @param description description for the map
* @param isPublic whether the map should be public
* @param mapIdBuilder builder to append the map ID to
* @return true on success, false otherwise
*/
public boolean createNewMap(
final String title, final String category, final String description,
final boolean isPublic, final StringBuilder mapIdBuilder) {
if (mapIdBuilder.length() > 0) {
throw new IllegalArgumentException("mapIdBuilder should be empty");
}
return wrapper.runQuery(new MyMapsGDataWrapper.QueryFunction() {
@Override
public void query(MapsClient client) throws IOException, Exception {
Log.d(MyMapsConstants.TAG, "Creating a new map.");
String mapFeed = MapsClient.getMapsFeed();
Log.d(MyMapsConstants.TAG, "Map feed is " + mapFeed);
MyMapsMapMetadata metaData = new MyMapsMapMetadata();
metaData.setTitle(title);
metaData.setDescription(description + " - "
+ category + " - " + context.getString(R.string.send_to_google_by_mytracks));
metaData.setSearchable(isPublic);
Entry entry = MyMapsGDataConverter.getMapEntryForMetadata(metaData);
Log.d(MyMapsConstants.TAG, "Title: " + entry.getTitle());
Entry map = client.createEntry(mapFeed, authToken, entry);
String mapId = MapsClient.getMapIdFromMapEntryId(map.getId());
mapIdBuilder.append(mapId);
Log.d(MyMapsConstants.TAG, "New map id is: " + mapId);
}
});
}
/**
* Uploads a single start or end marker to the given map.
*
* @param mapId ID of the map to upload to
* @param trackName name of the track being started/ended
* @param trackDescription description of the track being started/ended
* @param location the location of the marker
* @param isStart true to add a start marker, false to add an end marker
* @return true on success, false otherwise
*/
public boolean uploadMarker(final String mapId, final String trackName,
final String trackDescription, final Location loc, final boolean isStart) {
return wrapper.runQuery(new MyMapsGDataWrapper.QueryFunction() {
@Override
public void query(MapsClient client)
throws IOException, Exception {
String featureFeed = MapsClient.getFeaturesFeed(mapId);
GeoPoint geoPoint = getGeoPoint(loc);
insertMarker(client, featureFeed, trackName, trackDescription, geoPoint, isStart);
}
});
}
/**
* Inserts a place mark. Second try if 1st try fails. Will throw exception on
* 2nd failure.
*/
private void insertMarker(MapsClient client,
String featureFeed,
String trackName, String trackDescription,
GeoPoint geoPoint,
boolean isStart) throws IOException, Exception {
MyMapsFeature feature =
buildMyMapsPlacemarkFeature(trackName, trackDescription, geoPoint, isStart);
Entry entry = gdataConverter.getEntryForFeature(feature);
Log.d(MyMapsConstants.TAG, "SendToMyMaps: Creating placemark "
+ entry.getTitle());
try {
client.createEntry(featureFeed, authToken, entry);
Log.d(MyMapsConstants.TAG, "SendToMyMaps: createEntry success!");
} catch (IOException e) {
Log.w(MyMapsConstants.TAG,
"SendToMyMaps: createEntry 1st try failed. Trying again.");
// Retry once (often IOException is thrown on a timeout):
client.createEntry(featureFeed, authToken, entry);
Log.d(MyMapsConstants.TAG,
"SendToMyMaps: createEntry success on 2nd try!");
}
}
/**
* Builds a placemark MyMapsFeature from a track.
*
* @param track the track
* @param isStart true if it's the start of the track, or false for end
* @return a MyMapsFeature
*/
private MyMapsFeature buildMyMapsPlacemarkFeature(
String trackName, String trackDescription,
GeoPoint geoPoint, boolean isStart) {
String iconUrl;
if (isStart) {
iconUrl = START_ICON_URL;
} else {
iconUrl = END_ICON_URL;
}
String title = trackName + " "
+ (isStart ? context.getString(R.string.start)
: context.getString(R.string.end));
String description = isStart ? "" : trackDescription;
return buildMyMapsPlacemarkFeature(title, description, iconUrl, geoPoint);
}
/**
* Builds a MyMapsFeature from a track.
*
* @param wpt the waypoint
* @return a MyMapsFeature
*/
private static MyMapsFeature buildMyMapsPlacemarkFeature(
String title, String description, String iconUrl, GeoPoint geoPoint) {
MyMapsFeature myMapsFeature = new MyMapsFeature();
myMapsFeature.generateAndroidId();
myMapsFeature.setType(MyMapsFeature.MARKER);
myMapsFeature.setIconUrl(iconUrl);
myMapsFeature.setDescription(description);
myMapsFeature.addPoint(geoPoint);
if (TextUtils.isEmpty(title)) {
// Features must have a name (otherwise GData upload may fail):
myMapsFeature.setTitle("-");
} else {
myMapsFeature.setTitle(title);
}
myMapsFeature.setDescription(description.replaceAll("\n", "<br>"));
return myMapsFeature;
}
/**
* Uploads a series of waypoints to the given map.
*
* @param mapId ID of the map to upload to
* @param waypoints the waypoints to upload
* @return true on success, false otherwise
*/
public boolean uploadWaypoints(
final String mapId, final Iterable<Waypoint> waypoints) {
return wrapper.runQuery(new MyMapsGDataWrapper.QueryFunction() {
public void query(MapsClient client) {
// TODO(rdamazio): 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.
String featureFeed = MapsClient.getFeaturesFeed(mapId);
try {
for (Waypoint waypoint : waypoints) {
MyMapsFeature feature = buildMyMapsPlacemarkFeature(
waypoint.getName(), waypoint.getDescription(), waypoint.getIcon(),
getGeoPoint(waypoint.getLocation()));
Entry entry = gdataConverter.getEntryForFeature(feature);
Log.d(MyMapsConstants.TAG,
"SendToMyMaps: Creating waypoint.");
try {
client.createEntry(featureFeed, authToken, entry);
Log.d(MyMapsConstants.TAG,
"SendToMyMaps: createEntry success!");
} catch (IOException e) {
Log.w(MyMapsConstants.TAG,
"SendToMyMaps: createEntry 1st try failed. Retrying.");
// Retry once (often IOException is thrown on a timeout):
client.createEntry(featureFeed, authToken, entry);
Log.d(MyMapsConstants.TAG,
"SendToMyMaps: createEntry success on 2nd try!");
}
}
} catch (ParseException e) {
Log.w(MyMapsConstants.TAG, "ParseException caught.", e);
} catch (HttpException e) {
Log.w(MyMapsConstants.TAG, "HttpException caught.", e);
} catch (IOException e) {
Log.w(MyMapsConstants.TAG, "IOException caught.", e);
}
}
});
}
/**
* Uploads a series of points to the given map.
*
* @param mapId ID of the map to upload to
* @param trackName the name of the track
* @param locations the locations to upload
* @return true on success, false otherwise
*/
public boolean uploadTrackPoints(
final String mapId, final String trackName, final Collection<Location> locations) {
return wrapper.runQuery(new MyMapsGDataWrapper.QueryFunction() {
@Override
public void query(MapsClient client)
throws IOException, Exception {
String featureFeed = MapsClient.getFeaturesFeed(mapId);
Log.d(MyMapsConstants.TAG, "Feature feed url: " + featureFeed);
uploadTrackPoints(client, featureFeed, trackName, locations);
}
});
}
private boolean uploadTrackPoints(
MapsClient client,
String featureFeed,
String trackName, Collection<Location> locations)
throws IOException, Exception {
Entry entry = null;
int numLocations = locations.size();
if (numLocations < 2) {
// Need at least two points for a polyline:
Log.w(MyMapsConstants.TAG, "Not uploading too few points");
return true;
}
// Put the line:
entry = gdataConverter.getEntryForFeature(
buildMyMapsLineFeature(trackName, locations));
Log.d(MyMapsConstants.TAG,
"SendToMyMaps: Creating line " + entry.getTitle());
try {
client.createEntry(featureFeed, authToken, entry);
Log.d(MyMapsConstants.TAG, "SendToMyMaps: createEntry success!");
} catch (IOException e) {
Log.w(MyMapsConstants.TAG,
"SendToMyMaps: createEntry 1st try failed. Trying again.");
// Retry once (often IOException is thrown on a timeout):
client.createEntry(featureFeed, authToken, entry);
Log.d(MyMapsConstants.TAG,
"SendToMyMaps: createEntry success on 2nd try!");
}
return true;
}
/**
* Builds a MyMapsFeature from a track.
*
* @param track the track
* @return a MyMapsFeature
*/
private static MyMapsFeature buildMyMapsLineFeature(String trackName,
Iterable<Location> locations) {
MyMapsFeature myMapsFeature = new MyMapsFeature();
myMapsFeature.generateAndroidId();
myMapsFeature.setType(MyMapsFeature.LINE);
if (TextUtils.isEmpty(trackName)) {
// Features must have a name (otherwise GData upload may fail):
myMapsFeature.setTitle("-");
} else {
myMapsFeature.setTitle(trackName);
}
myMapsFeature.setColor(0x80FF0000);
for (Location loc : locations) {
myMapsFeature.addPoint(getGeoPoint(loc));
}
return myMapsFeature;
}
/**
* Cleans up after a series of uploads.
* This closes the connection to Maps and resets retry counters.
*/
public void cleanUp() {
wrapper.cleanUp();
}
private static GeoPoint getGeoPoint(Location location) {
return new GeoPoint((int) (location.getLatitude() * 1E6),
(int) (location.getLongitude() * 1E6));
}
}
@@ -0,0 +1,17 @@
// Copyright 2009 Google Inc. All Rights Reserved.
package com.google.android.apps.mytracks.io.mymaps;
/**
* Constants for My Maps.
*/
public class MyMapsConstants {
static final String TAG = "MapsApi";
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 MyMapsConstants() { }
}
@@ -0,0 +1,235 @@
// Copyright 2009 Google Inc. All Rights Reserved.
package com.google.android.apps.mytracks.io.mymaps;
import com.google.android.maps.GeoPoint;
import java.util.Random;
import java.util.Vector;
/**
* MyMapsFeature contains all of the data associated with a feature in My Maps,
* where a feature is a marker, line, or shape. Some of the data is stored in a
* {@link MyMapsFeatureMetadata} object so that it can be more efficiently
* transmitted to other activities.
*/
class MyMapsFeature {
private static final long serialVersionUID = 8439035544430497236L;
/** A marker feature displays an icon at a single point on the map. */
public static final int MARKER = 0;
/**
* A line feature displays a line connecting a set of points on the map.
*/
public static final int LINE = 1;
/**
* A shape feature displays a border defined by connecting a set of points,
* including connecting the last to the first, and displays the area
* confined by this border.
*/
public static final int SHAPE = 2;
/** The local feature id for this feature, if needed. */
private String androidId;
/**
* The latitudes of the points of this feature in order, specified in
* millionths of a degree north.
*/
private final Vector<Integer> latitudeE6 = new Vector<Integer>();
/**
* The longitudes of the points of this feature in order, specified in
* millionths of a degree east.
*/
private final Vector<Integer> longitudeE6 = new Vector<Integer>();
/** The metadata of this feature in a format efficient for transmission. */
private MyMapsFeatureMetadata featureInfo = new MyMapsFeatureMetadata();
private final Random random = new Random();
/**
* Initializes a valid but empty feature. It will default to a
* {@link #MARKER} with a blue placemark with a dot as an icon at the
* location (0, 0).
*/
public MyMapsFeature() {
}
/**
* Adds a new point to the end of this feature.
*
* @param point The new point to add
*/
public void addPoint(GeoPoint point) {
latitudeE6.add(point.getLatitudeE6());
longitudeE6.add(point.getLongitudeE6());
}
/**
* Generates a new local id for this feature based on the current time and
* a random number.
*/
void generateAndroidId() {
long time = System.currentTimeMillis();
int rand = random.nextInt(10000);
androidId = time + "." + rand;
}
/**
* Retrieves the current local id for this feature if one is available.
*
* @return The local id for this feature
*/
String getAndroidId() {
return androidId;
}
/**
* Retrieves the current (html) description of this feature. The description
* is stored in the feature metadata.
*
* @return The description of this feature
*/
public String getDescription() {
return featureInfo.getDescription();
}
/**
* Sets the description of this feature. That description is stored in the
* feature metadata.
*
* @param description The new description of this feature
*/
public void setDescription(String description) {
featureInfo.setDescription(description);
}
/**
* Retrieves the point at the given index for this feature.
*
* @param index The index of the point desired
* @return A {@link GeoPoint} representing the point or null if that point
* doesn't exist
*/
public GeoPoint getPoint(int index) {
if (latitudeE6.size() <= index) {
return null;
}
return new GeoPoint(latitudeE6.get(index), longitudeE6.get(index));
}
/**
* Counts the number of points in this feature and return that count.
*
* @return The number of points in this feature
*/
public int getPointCount() {
return latitudeE6.size();
}
/**
* Retrieves the title of this feature. That title is stored in the feature
* metadata.
*
* @return the current title of this feature
*/
public String getTitle() {
return featureInfo.getTitle();
}
/**
* Retrieves the type of this feature. That type is stored in the feature
* metadata.
*
* @return One of {@link #MARKER}, {@link #LINE}, or {@link #SHAPE}
* identifying the type of this feature
*/
public int getType() {
return featureInfo.getType();
}
/**
* Retrieves the current color of this feature as an ARGB color integer.
* That color is stored in the feature metadata.
*
* @return The ARGB color of this feature
*/
public int getColor() {
return featureInfo.getColor();
}
/**
* Retrieves the current line width of this feature. That line width is
* stored in the feature metadata.
*
* @return The line width of this feature
*/
public int getLineWidth() {
return featureInfo.getLineWidth();
}
/**
* Retrieves the current fill color of this feature as an ARGB color
* integer. That color is stored in the feature metadata.
*
* @return The ARGB fill color of this feature
*/
public int getFillColor() {
return featureInfo.getFillColor();
}
/**
* Retrieves the current icon url of this feature. That icon url is stored
* in the feature metadata.
*
* @return The icon url for this feature
*/
public String getIconUrl() {
return featureInfo.getIconUrl();
}
/**
* Sets the title of this feature. That title is stored in the feature
* metadata.
*
* @param title The new title of this feature
*/
public void setTitle(String title) {
featureInfo.setTitle(title);
}
/**
* Sets the type of this feature. That type is stored in the feature
* metadata.
*
* @param type The new type of the feature. That type must be one of
* {@link #MARKER}, {@link #LINE}, or {@link #SHAPE}
*/
public void setType(int type) {
featureInfo.setType(type);
}
/**
* Sets the ARGB color of this feature. That color is stored in the feature
* metadata.
*
* @param color The new ARGB color of this feature
*/
public void setColor(int color) {
featureInfo.setColor(color);
}
/**
* Sets the icon url of this feature. That icon url is stored in the feature
* metadata.
*
* @param url The new icon url of the feature
*/
public void setIconUrl(String url) {
featureInfo.setIconUrl(url);
}
}
@@ -0,0 +1,87 @@
// Copyright 2009 Google Inc. All Rights Reserved.
package com.google.android.apps.mytracks.io.mymaps;
/**
* Metadata about a maps feature.
*/
class MyMapsFeatureMetadata {
private static final String BLUE_DOT_URL =
"http://maps.google.com/mapfiles/ms/micons/blue-dot.png";
private static final int DEFAULT_COLOR = 0x800000FF;
private static final int DEFAULT_FILL_COLOR = 0xC00000FF;
private String title;
private String description;
private int type;
private int color;
private int lineWidth;
private int fillColor;
private String iconUrl;
public MyMapsFeatureMetadata() {
title = "";
description = "";
type = MyMapsFeature.MARKER;
color = DEFAULT_COLOR;
lineWidth = 5;
fillColor = DEFAULT_FILL_COLOR;
iconUrl = BLUE_DOT_URL;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public int getType() {
return type;
}
public void setType(int type) {
this.type = type;
}
public int getColor() {
return color;
}
public void setColor(int color) {
this.color = color;
}
public int getLineWidth() {
return lineWidth;
}
public void setLineWidth(int width) {
lineWidth = width;
}
public int getFillColor() {
return fillColor;
}
public void setFillColor(int color) {
fillColor = color;
}
public String getIconUrl() {
return iconUrl;
}
public void setIconUrl(String url) {
iconUrl = url;
}
}
@@ -0,0 +1,183 @@
// Copyright 2009 Google Inc. All Rights Reserved.
package com.google.android.apps.mytracks.io.mymaps;
import com.google.wireless.gdata.data.Entry;
import com.google.wireless.gdata.data.StringUtils;
import android.graphics.Color;
import android.util.Log;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;
import org.xmlpull.v1.XmlSerializer;
import java.io.IOException;
import java.io.StringWriter;
/**
* Converter from GData objects to MyMaps objects.
*/
class MyMapsGDataConverter {
private final XmlSerializer xmlSerializer;
public MyMapsGDataConverter() throws XmlPullParserException {
xmlSerializer = XmlPullParserFactory.newInstance().newSerializer();
}
public static MyMapsMapMetadata getMapMetadataForEntry(
MapFeatureEntry entry) {
MyMapsMapMetadata metadata = new MyMapsMapMetadata();
if ("public".equals(entry.getPrivacy())) {
metadata.setSearchable(true);
} else {
metadata.setSearchable(false);
}
metadata.setTitle(entry.getTitle());
String desc = entry.getContent();
if (desc.length() > 12) {
metadata.setDescription(desc.substring(9, desc.length() - 3));
}
String editUri = entry.getEditUri();
if (editUri != null) {
metadata.setGDataEditUri(editUri);
}
return metadata;
}
public static String getMapidForEntry(Entry entry) {
return MapsClient.getMapIdFromMapEntryId(entry.getId());
}
public static Entry getMapEntryForMetadata(MyMapsMapMetadata metadata) {
MapFeatureEntry entry = new MapFeatureEntry();
entry.setEditUri(metadata.getGDataEditUri());
entry.setTitle(metadata.getTitle());
entry.setContent(metadata.getDescription());
entry.setPrivacy(metadata.getSearchable() ? "public" : "unlisted");
entry.setAuthor("android");
entry.setEmail("nobody@google.com");
return entry;
}
public MapFeatureEntry getEntryForFeature(MyMapsFeature feature) {
MapFeatureEntry entry = new MapFeatureEntry();
entry.setTitle(feature.getTitle());
entry.setAuthor("android");
entry.setEmail("nobody@google.com");
entry.setCategoryScheme("http://schemas.google.com/g/2005#kind");
entry.setCategory("http://schemas.google.com/g/2008#mapfeature");
entry.setEditUri("");
if (!StringUtils.isEmpty(feature.getAndroidId())) {
entry.setAttribute("_androidId", feature.getAndroidId());
}
try {
StringWriter writer = new StringWriter();
xmlSerializer.setOutput(writer);
xmlSerializer.startTag(null, "Placemark");
xmlSerializer.attribute(null, "xmlns", "http://earth.google.com/kml/2.2");
xmlSerializer.startTag(null, "Style");
if (feature.getType() == MyMapsFeature.MARKER) {
xmlSerializer.startTag(null, "IconStyle");
xmlSerializer.startTag(null, "Icon");
xmlSerializer.startTag(null, "href");
xmlSerializer.text(feature.getIconUrl());
xmlSerializer.endTag(null, "href");
xmlSerializer.endTag(null, "Icon");
xmlSerializer.endTag(null, "IconStyle");
} else {
xmlSerializer.startTag(null, "LineStyle");
xmlSerializer.startTag(null, "color");
int color = feature.getColor();
// Reverse the color because KML is ABGR and Android is ARGB
xmlSerializer.text(Integer.toHexString(
Color.argb(Color.alpha(color), Color.blue(color),
Color.green(color), Color.red(color))));
xmlSerializer.endTag(null, "color");
xmlSerializer.startTag(null, "width");
xmlSerializer.text(Integer.toString(feature.getLineWidth()));
xmlSerializer.endTag(null, "width");
xmlSerializer.endTag(null, "LineStyle");
if (feature.getType() == MyMapsFeature.SHAPE) {
xmlSerializer.startTag(null, "PolyStyle");
xmlSerializer.startTag(null, "color");
int fcolor = feature.getFillColor();
// Reverse the color because KML is ABGR and Android is ARGB
xmlSerializer.text(Integer.toHexString(Color.argb(Color.alpha(fcolor),
Color.blue(fcolor), Color.green(fcolor), Color.red(fcolor))));
xmlSerializer.endTag(null, "color");
xmlSerializer.startTag(null, "fill");
xmlSerializer.text("1");
xmlSerializer.endTag(null, "fill");
xmlSerializer.startTag(null, "outline");
xmlSerializer.text("1");
xmlSerializer.endTag(null, "outline");
xmlSerializer.endTag(null, "PolyStyle");
}
}
xmlSerializer.endTag(null, "Style");
xmlSerializer.startTag(null, "name");
xmlSerializer.text(feature.getTitle());
xmlSerializer.endTag(null, "name");
xmlSerializer.startTag(null, "description");
xmlSerializer.cdsect(feature.getDescription());
xmlSerializer.endTag(null, "description");
StringBuilder pointBuilder = new StringBuilder();
for (int i = 0; i < feature.getPointCount(); ++i) {
if (i > 0) {
pointBuilder.append('\n');
}
pointBuilder.append(feature.getPoint(i).getLongitudeE6() / 1e6);
pointBuilder.append(',');
pointBuilder.append(feature.getPoint(i).getLatitudeE6() / 1e6);
pointBuilder.append(",0.000000");
}
String pointString = pointBuilder.toString();
if (feature.getType() == MyMapsFeature.MARKER) {
xmlSerializer.startTag(null, "Point");
xmlSerializer.startTag(null, "coordinates");
xmlSerializer.text(pointString);
xmlSerializer.endTag(null, "coordinates");
xmlSerializer.endTag(null, "Point");
} else if (feature.getType() == MyMapsFeature.LINE) {
xmlSerializer.startTag(null, "LineString");
xmlSerializer.startTag(null, "tessellate");
xmlSerializer.text("1");
xmlSerializer.endTag(null, "tessellate");
xmlSerializer.startTag(null, "coordinates");
xmlSerializer.text(pointString);
xmlSerializer.endTag(null, "coordinates");
xmlSerializer.endTag(null, "LineString");
} else {
xmlSerializer.startTag(null, "Polygon");
xmlSerializer.startTag(null, "outerBoundaryIs");
xmlSerializer.startTag(null, "LinearRing");
xmlSerializer.startTag(null, "tessellate");
xmlSerializer.text("1");
xmlSerializer.endTag(null, "tessellate");
xmlSerializer.startTag(null, "coordinates");
xmlSerializer.text(pointString + "\n"
+ Double.toString(feature.getPoint(0).getLongitudeE6() / 1e6)
+ ","
+ Double.toString(feature.getPoint(0).getLatitudeE6() / 1e6)
+ ",0.000000");
xmlSerializer.endTag(null, "coordinates");
xmlSerializer.endTag(null, "LinearRing");
xmlSerializer.endTag(null, "outerBoundaryIs");
xmlSerializer.endTag(null, "Polygon");
}
xmlSerializer.endTag(null, "Placemark");
xmlSerializer.flush();
entry.setContent(writer.toString());
Log.d("My Google Maps", "Generated kml:\n" + entry.getContent());
Log.d("My Google Maps", "Edit URI: " + entry.getEditUri());
} catch (IOException e) {
e.printStackTrace();
}
return entry;
}
}
@@ -0,0 +1,171 @@
// Copyright 2010 Google Inc. All Rights Reserved.
package com.google.android.apps.mytracks.io.mymaps;
import com.google.android.apps.mytracks.io.AuthManager;
import com.google.android.apps.mytracks.io.AuthManager.AuthCallback;
import com.google.android.apps.mytracks.io.gdata.GDataClientFactory;
import com.google.android.common.gdata.AndroidXmlParserFactory;
import com.google.wireless.gdata.client.GDataClient;
import com.google.wireless.gdata.client.HttpException;
import com.google.wireless.gdata.parser.xml.SimplePullParser.ParseException;
import com.google.wireless.gdata2.ConflictDetectedException;
import com.google.wireless.gdata2.client.AuthenticationException;
import android.content.Context;
import android.util.Log;
import java.io.IOException;
/**
* MyMapsGDataWrapper provides a wrapper around GData operations that maintains
* the GData client, and provides a method to run gdata queries with proper
* error handling. After a query is run, the wrapper can be queried about the
* error that occurred.
*/
class MyMapsGDataWrapper {
/**
* A QueryFunction is passed in when executing a query. The query function
* of the class is called with the GData client as a parameter. The
* function should execute whatever operations it desires on the client
* without concern for whether the client will throw an error.
*/
interface QueryFunction {
public abstract void query(MapsClient client)
throws AuthenticationException, IOException, ParseException,
ConflictDetectedException, Exception;
}
// The types of error that may be encountered
/** No error occurred. */
public static final int ERROR_NO_ERROR = 0;
/** There was an authentication error, the auth token may be invalid. */
public static final int ERROR_AUTH = 1;
/** There was an internal error on the server side. */
public static final int ERROR_INTERNAL = 2;
/** There was an error connecting to the server. */
public static final int ERROR_CONNECTION = 3;
/** The item queried did not exit. */
public static final int ERROR_NOT_FOUND = 4;
/** There was an error parsing or serializing locally. */
public static final int ERROR_LOCAL = 5;
/** There was a conflict, update the entry and try again. */
public static final int ERROR_CONFLICT = 6;
/**
* A query was run after cleaning up the wrapper, so the client was invalid.
*/
public static final int ERROR_CLEANED_UP = 7;
/** An unknown error occurred. */
public static final int ERROR_UNKNOWN = 100;
private final GDataClient androidGdataClient;
private final AuthManager auth;
private final MapsClient client;
private String errorMessage;
private int errorType;
private boolean retryOnAuthFailure;
private int retriesPending;
private boolean cleanupCalled;
public MyMapsGDataWrapper(Context context, AuthManager auth) {
androidGdataClient = GDataClientFactory.getGDataClient(context);
this.auth = auth;
client =
new MapsClient(androidGdataClient, new XmlMapsGDataParserFactory(
new AndroidXmlParserFactory()));
errorType = ERROR_NO_ERROR;
errorMessage = null;
retryOnAuthFailure = false;
retriesPending = 0;
cleanupCalled = false;
}
public boolean runQuery(final QueryFunction query) {
if (client == null) {
errorType = ERROR_CLEANED_UP;
errorMessage = "GData Wrapper has already been cleaned up!";
return false;
}
try {
query.query(client);
errorType = ERROR_NO_ERROR;
errorMessage = null;
return true;
} catch (AuthenticationException e) {
Log.e(MyMapsConstants.TAG, "Exception", e);
errorType = ERROR_AUTH;
errorMessage = e.getMessage();
} catch (HttpException e) {
Log.e(MyMapsConstants.TAG, "HttpException", e);
errorMessage = e.getMessage();
if (errorMessage != null && errorMessage.contains("401")) {
errorType = ERROR_AUTH;
} else {
errorType = ERROR_CONNECTION;
}
} catch (IOException e) {
Log.e(MyMapsConstants.TAG, "Exception", e);
errorMessage = e.getMessage();
if (errorMessage != null && errorMessage.contains("503")) {
errorType = ERROR_INTERNAL;
} else {
errorType = ERROR_CONNECTION;
}
} catch (ParseException e) {
Log.e(MyMapsConstants.TAG, "Exception", e);
errorType = ERROR_LOCAL;
errorMessage = e.getMessage();
} catch (ConflictDetectedException e) {
Log.e(MyMapsConstants.TAG, "Exception", e);
errorType = ERROR_CONFLICT;
errorMessage = e.getMessage();
} catch (Exception e) {
Log.e(MyMapsConstants.TAG, "Exception", e);
errorType = ERROR_UNKNOWN;
errorMessage = e.getMessage();
e.printStackTrace();
}
Log.d(MyMapsConstants.TAG, "GData error encountered: " + errorMessage);
if (errorType == ERROR_AUTH && auth != null) {
AuthCallback whenFinished = null;
if (retryOnAuthFailure) {
retriesPending++;
whenFinished = new AuthCallback() {
@Override
public void onAuthResult(boolean success) {
retriesPending--;
retryOnAuthFailure = false;
runQuery(query);
if (cleanupCalled && retriesPending == 0) {
cleanUp();
}
}
};
}
auth.invalidateAndRefresh(whenFinished);
}
return false;
}
public int getErrorType() {
return errorType;
}
public String getErrorMessage() {
return errorMessage;
}
// cleanUp must be called when done using this wrapper to close the client.
// Note that the cleanup will be delayed if auth failure retries were
// requested and there is a pending retry.
public void cleanUp() {
if (retriesPending == 0 && !cleanupCalled) {
androidGdataClient.close();
}
cleanupCalled = true;
}
public void setRetryOnAuthFailure(boolean retry) {
retryOnAuthFailure = retry;
}
}
@@ -0,0 +1,52 @@
// Copyright 2009 Google Inc. All Rights Reserved.
package com.google.android.apps.mytracks.io.mymaps;
/**
* Metadata about a "my maps" map.
*/
class MyMapsMapMetadata {
private String title;
private String description;
private String gdataEditUri;
private boolean searchable;
public MyMapsMapMetadata() {
title = "";
description = "";
gdataEditUri = "";
searchable = false;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public boolean getSearchable() {
return searchable;
}
public void setSearchable(boolean searchable) {
this.searchable = searchable;
}
public String getGDataEditUri() {
return gdataEditUri;
}
public void setGDataEditUri(String editUri) {
this.gdataEditUri = editUri;
}
}

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