forked from upstream-mirrors/OpenTracks
Add test case for ChooseMapAsyncTask and adjust some code in ChooseMapAsyncTask.java to make it testable.
This commit is contained in:
@@ -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";
|
||||
|
||||
}
|
||||
@@ -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()
|
||||
{ }
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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,116 @@
|
||||
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.Context;
|
||||
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, Context.MODE_PRIVATE);
|
||||
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.stat_speed, R.string.stat_pace);
|
||||
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,65 @@
|
||||
/*
|
||||
* 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 {
|
||||
|
||||
private final static int DEFAULT_PERSISTEDINT = 0;
|
||||
|
||||
public AntPreference(Context context) {
|
||||
super(context);
|
||||
init();
|
||||
}
|
||||
|
||||
public AntPreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init();
|
||||
}
|
||||
|
||||
private void init() {
|
||||
int sensorId = getPersistedInt(AntPreference.DEFAULT_PERSISTEDINT);
|
||||
if (sensorId == 0) {
|
||||
setSummary(R.string.settings_sensor_ant_not_paired);
|
||||
} else {
|
||||
setSummary(
|
||||
String.format(getContext().getString(R.string.settings_sensor_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_sensor_ant_not_paired);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.google.android.apps.mytracks;
|
||||
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.content.Context;
|
||||
import android.preference.EditTextPreference;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewParent;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.AutoCompleteTextView;
|
||||
|
||||
/**
|
||||
* The {@link AutoCompleteTextPreference} class is a preference that allows for
|
||||
* string input using auto complete . It is a subclass of
|
||||
* {@link EditTextPreference} and shows the {@link AutoCompleteTextView} in a
|
||||
* dialog.
|
||||
* <p>
|
||||
* This preference will store a string into the SharedPreferences.
|
||||
*
|
||||
* @author Rimas Trumpa (with Matt Levan)
|
||||
*/
|
||||
public class AutoCompleteTextPreference extends EditTextPreference {
|
||||
|
||||
private AutoCompleteTextView mEditText = null;
|
||||
|
||||
public AutoCompleteTextPreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
mEditText = new AutoCompleteTextView(context, attrs);
|
||||
mEditText.setThreshold(0);
|
||||
|
||||
// Gets autocomplete values for 'Default Activity' preference
|
||||
if (getKey().equals(context.getString(R.string.default_activity_key))) {
|
||||
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(context,
|
||||
R.array.activity_types, android.R.layout.simple_dropdown_item_1line);
|
||||
mEditText.setAdapter(adapter);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onBindDialogView(View view) {
|
||||
AutoCompleteTextView editText = mEditText;
|
||||
editText.setText(getText());
|
||||
|
||||
ViewParent oldParent = editText.getParent();
|
||||
if (oldParent != view) {
|
||||
if (oldParent != null) {
|
||||
((ViewGroup) oldParent).removeView(editText);
|
||||
}
|
||||
onAddEditTextToDialogView(view, editText);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDialogClosed(boolean positiveResult) {
|
||||
if (positiveResult) {
|
||||
String value = mEditText.getText().toString();
|
||||
if (callChangeListener(value)) {
|
||||
setText(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* 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.RemoveTempFilesService;
|
||||
import com.google.android.apps.mytracks.services.TrackRecordingService;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* This class handles MyTracks related broadcast messages.
|
||||
*
|
||||
* One example of a broadcast message that this class is interested in,
|
||||
* is notification about the phone boot. We may want to resume a previously
|
||||
* started tracking session if the phone crashed (hopefully not), or the user
|
||||
* decided to swap the battery or some external event occurred which forced
|
||||
* a phone reboot.
|
||||
*
|
||||
* This class simply delegates to {@link TrackRecordingService} to make a
|
||||
* decision whether to continue with the previous track (if any), or just
|
||||
* abandon it.
|
||||
*
|
||||
* @author Bartlomiej Niechwiej
|
||||
*/
|
||||
public class BootReceiver extends BroadcastReceiver {
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
Log.d(TAG, "BootReceiver.onReceive: " + intent.getAction());
|
||||
if (ACTION_BOOT_COMPLETED.equals(intent.getAction())) {
|
||||
Intent startIntent = new Intent(context, TrackRecordingService.class);
|
||||
startIntent.putExtra(RESUME_TRACK_EXTRA_NAME, true);
|
||||
context.startService(startIntent);
|
||||
|
||||
Intent removeTempFilesIntent = new Intent(context, RemoveTempFilesService.class);
|
||||
context.startService(removeTempFilesIntent);
|
||||
} else {
|
||||
Log.w(TAG, "BootReceiver: unsupported action");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,498 @@
|
||||
/*
|
||||
* 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.stats.DoubleBuffer;
|
||||
import com.google.android.apps.mytracks.stats.TripStatisticsBuilder;
|
||||
import com.google.android.apps.mytracks.util.LocationUtils;
|
||||
import com.google.android.apps.mytracks.util.UnitConversions;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
|
||||
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.speech.tts.TextToSpeech;
|
||||
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 = true;
|
||||
private boolean reportSpeed = true;
|
||||
|
||||
/*
|
||||
* 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
|
||||
setVolumeControlStream(TextToSpeech.Engine.DEFAULT_STREAM);
|
||||
|
||||
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(Menu.NONE, Constants.MENU_CHART_SETTINGS, Menu.NONE,
|
||||
R.string.menu_chart_view_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());
|
||||
settingsDialog.setDisplaySpeed(reportSpeed);
|
||||
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
|
||||
*/
|
||||
@VisibleForTesting
|
||||
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:
|
||||
if (lastLocation != null) {
|
||||
double d = lastLocation.distanceTo(location);
|
||||
if (metricUnits) {
|
||||
profileLength += d;
|
||||
} else {
|
||||
profileLength += d * UnitConversions.KM_TO_MI;
|
||||
}
|
||||
}
|
||||
timeOrDistance = profileLength * UnitConversions.M_TO_KM;
|
||||
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() * UnitConversions.MS_TO_KMH;
|
||||
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) {
|
||||
if (metricUnits == metric) {
|
||||
return false;
|
||||
}
|
||||
metricUnits = metric;
|
||||
chartView.setMetricUnits(metricUnits);
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
chartView.requestLayout();
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onReportSpeedChanged(boolean speed) {
|
||||
if (reportSpeed == speed) {
|
||||
return false;
|
||||
}
|
||||
reportSpeed = speed;
|
||||
chartView.setReportSpeed(speed, this);
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
chartView.requestLayout();
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
ChartView getChartView() {
|
||||
return chartView;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
MenuItem getChartSettingsMenuItem() {
|
||||
return chartSettingsMenuItem;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void setTrackMaxSpeed(double maxSpeed) {
|
||||
trackMaxSpeed = maxSpeed;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
* 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 com.google.common.annotations.VisibleForTesting;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether to display speed or pace.
|
||||
*
|
||||
* @param displaySpeed true to display speed
|
||||
*/
|
||||
public void setDisplaySpeed(boolean displaySpeed) {
|
||||
series[ChartView.SPEED_SERIES].setText(displaySpeed ? R.string.stat_speed : R.string.stat_pace);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
CheckBox[] getSeries() {
|
||||
return series;
|
||||
}
|
||||
}
|
||||
@@ -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.Paint.Style;
|
||||
import android.graphics.Path;
|
||||
|
||||
import java.text.NumberFormat;
|
||||
|
||||
/**
|
||||
* 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 NumberFormat 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 fillColor The paint for filling the chart
|
||||
* @param strokeColor The paint for stroking the outside the chart, optional
|
||||
* @param zoomSettings The settings related to zooming
|
||||
* @param titleId The title ID
|
||||
*
|
||||
* TODO: Get rid of Context and inject appropriate values instead.
|
||||
*/
|
||||
public ChartValueSeries(
|
||||
Context context, int fillColor, int strokeColor, ZoomSettings zoomSettings, int titleId) {
|
||||
this.format = NumberFormat.getIntegerInstance();
|
||||
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 number format for this series
|
||||
*/
|
||||
NumberFormat 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,899 @@
|
||||
/*
|
||||
* 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 com.google.common.annotations.VisibleForTesting;
|
||||
|
||||
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.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 = 40;
|
||||
|
||||
/** 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 = NumberFormat.getIntegerInstance();
|
||||
private static final NumberFormat X_SHORT_FORMAT = NumberFormat.getNumberInstance();
|
||||
|
||||
static {
|
||||
X_SHORT_FORMAT.setMaximumFractionDigits(1);
|
||||
X_SHORT_FORMAT.setMinimumFractionDigits(1);
|
||||
}
|
||||
|
||||
/*
|
||||
* 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.stat_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.stat_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.sensor_state_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.sensor_state_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.sensor_state_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.stat_speed
|
||||
: R.string.stat_pace));
|
||||
}
|
||||
|
||||
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) {
|
||||
if (mode == Mode.BY_DISTANCE) {
|
||||
double lenghtInKm = waypoint.getLength() * UnitConversions.M_TO_KM;
|
||||
return getX(metricUnits ? lenghtInKm : lenghtInKm * UnitConversions.KM_TO_MI);
|
||||
} else {
|
||||
return 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.formatElapsedTime((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.unit_kilometer) : c.getString(R.string.unit_mile))
|
||||
: c.getString(R.string.unit_minute);
|
||||
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.unit_meter) : c.getString(R.string.unit_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.
|
||||
*/
|
||||
public void setChartValueSeriesEnabled(int index, boolean enabled) {
|
||||
series[index].setEnabled(enabled);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
int getZoomLevel() {
|
||||
return zoomLevel;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
int getMaxZoomLevel() {
|
||||
return maxZoomLevel;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* 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.ApiAdapterFactory;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
/**
|
||||
* Checks with the user if he prefers the metric units or the imperial units.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
class CheckUnits {
|
||||
private static final String CHECK_UNITS_PREFERENCE_FILE = "checkunits";
|
||||
private static final String CHECK_UNITS_PREFERENCE_KEY = "checkunits.checked";
|
||||
private static boolean metric = true;
|
||||
|
||||
public static void check(final Context context) {
|
||||
final SharedPreferences checkUnitsSharedPreferences = context.getSharedPreferences(
|
||||
CHECK_UNITS_PREFERENCE_FILE, Context.MODE_PRIVATE);
|
||||
if (checkUnitsSharedPreferences.getBoolean(CHECK_UNITS_PREFERENCE_KEY, false)) {
|
||||
return;
|
||||
}
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
||||
builder.setTitle(context.getString(R.string.preferred_units_title));
|
||||
CharSequence[] items = { context.getString(R.string.preferred_units_metric),
|
||||
context.getString(R.string.preferred_units_imperial) };
|
||||
builder.setSingleChoiceItems(items, 0, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
if (which == 0) {
|
||||
metric = true;
|
||||
} else {
|
||||
metric = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
builder.setCancelable(true);
|
||||
builder.setPositiveButton(R.string.generic_ok, new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
recordCheckPerformed(checkUnitsSharedPreferences);
|
||||
SharedPreferences useMetricPreferences = context.getSharedPreferences(
|
||||
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
|
||||
SharedPreferences.Editor editor = useMetricPreferences.edit();
|
||||
String key = context.getString(R.string.metric_units_key);
|
||||
ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor.putBoolean(key, metric));
|
||||
}
|
||||
});
|
||||
builder.setOnCancelListener(new DialogInterface.OnCancelListener() {
|
||||
public void onCancel(DialogInterface dialog) {
|
||||
recordCheckPerformed(checkUnitsSharedPreferences);
|
||||
}
|
||||
});
|
||||
builder.show();
|
||||
}
|
||||
|
||||
private static void recordCheckPerformed(SharedPreferences preferences) {
|
||||
ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(
|
||||
preferences.edit().putBoolean(CHECK_UNITS_PREFERENCE_KEY, 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,216 @@
|
||||
/*
|
||||
* 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 SHOW_TRACK = 0;
|
||||
public static final int SHARE_GPX_FILE = 1;
|
||||
public static final int SHARE_KML_FILE = 2;
|
||||
public static final int SHARE_CSV_FILE = 3;
|
||||
public static final int SHARE_TCX_FILE = 4;
|
||||
public static final int SAVE_GPX_FILE = 5;
|
||||
public static final int SAVE_KML_FILE = 6;
|
||||
public static final int SAVE_CSV_FILE = 7;
|
||||
public static final int SAVE_TCX_FILE = 8;
|
||||
public static final int SHOW_WAYPOINT = 9;
|
||||
public static final int WELCOME = 10;
|
||||
|
||||
/*
|
||||
* 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. Sorted alphabetically.
|
||||
*/
|
||||
public static final int MENU_CLEAR_MAP = 100;
|
||||
public static final int MENU_DELETE = 101;
|
||||
public static final int MENU_EDIT = 102;
|
||||
public static final int MENU_PLAY = 103;
|
||||
public static final int MENU_SAVE_CSV_FILE = 104;
|
||||
public static final int MENU_SAVE_GPX_FILE = 105;
|
||||
public static final int MENU_SAVE_KML_FILE = 106;
|
||||
public static final int MENU_SAVE_TCX_FILE = 107;
|
||||
public static final int MENU_SEND_TO_GOOGLE = 108;
|
||||
public static final int MENU_SHARE = 109;
|
||||
public static final int MENU_SHARE_CSV_FILE = 110;
|
||||
public static final int MENU_SHARE_FUSION_TABLE = 111;
|
||||
public static final int MENU_SHARE_GPX_FILE = 112;
|
||||
public static final int MENU_SHARE_KML_FILE = 113;
|
||||
public static final int MENU_SHARE_MAP = 114;
|
||||
public static final int MENU_SHARE_TCX_FILE = 115;
|
||||
public static final int MENU_SHOW = 116;
|
||||
public static final int MENU_WRITE_TO_SD_CARD = 117;
|
||||
|
||||
/**
|
||||
* 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,85 @@
|
||||
/*
|
||||
* 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.ApiAdapterFactory;
|
||||
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.content.SharedPreferences.Editor;
|
||||
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.track_list_delete_all_confirm_message));
|
||||
builder.setTitle(context.getString(R.string.generic_confirm_title));
|
||||
builder.setIcon(android.R.drawable.ic_dialog_alert);
|
||||
builder.setPositiveButton(context.getString(R.string.generic_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, Context.MODE_PRIVATE);
|
||||
Editor editor = prefs.edit();
|
||||
// TODO: Go through data manager
|
||||
editor.putLong(context.getString(R.string.selected_track_key), -1);
|
||||
ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor);
|
||||
if (done != null) {
|
||||
Handler h = new Handler();
|
||||
h.post(done);
|
||||
}
|
||||
}
|
||||
});
|
||||
builder.setNegativeButton(context.getString(R.string.generic_no),
|
||||
new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface dialogInterface, int i) {
|
||||
dialogInterface.dismiss();
|
||||
}
|
||||
});
|
||||
dialog = builder.create();
|
||||
dialog.show();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* 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.ApiAdapterFactory;
|
||||
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.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_list_delete_track_confirm_message));
|
||||
builder.setTitle(getString(R.string.generic_confirm_title));
|
||||
builder.setIcon(android.R.drawable.ic_dialog_alert);
|
||||
builder.setPositiveButton(getString(R.string.generic_yes), this);
|
||||
builder.setNegativeButton(getString(R.string.generic_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, Context.MODE_PRIVATE);
|
||||
if (preferences.getLong(selectedKey, -1) == deleteTrackId) {
|
||||
Editor editor = preferences.edit().putLong(selectedKey, -1);
|
||||
ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
/*
|
||||
* 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.Context;
|
||||
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");
|
||||
|
||||
String exportFileFormat = activity.getString(R.string.track_list_export_file);
|
||||
String fileTypes[] = activity.getResources().getStringArray(R.array.file_types);
|
||||
|
||||
String[] choices = new String[fileTypes.length];
|
||||
for (int i = 0; i < fileTypes.length; i++) {
|
||||
choices[i] = String.format(exportFileFormat, fileTypes[i]);
|
||||
}
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
|
||||
builder.setTitle(R.string.track_list_export_all);
|
||||
builder.setSingleChoiceItems(choices, 0, itemClick);
|
||||
builder.setPositiveButton(R.string.generic_ok, positiveClick);
|
||||
builder.setNegativeButton(R.string.generic_cancel, null);
|
||||
builder.show();
|
||||
}
|
||||
|
||||
private final DialogInterface.OnClickListener positiveClick =
|
||||
new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
new Thread(runner, "ExportAllTracks").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, Context.MODE_PRIVATE);
|
||||
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_success, Toast.LENGTH_SHORT);
|
||||
}
|
||||
|
||||
private void makeProgressDialog(final int trackCount) {
|
||||
String exportMsg = activity.getString(R.string.track_list_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(null, null, TracksColumns._ID);
|
||||
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.export_error, 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,269 @@
|
||||
/*
|
||||
* Copyright 2011 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package com.google.android.apps.mytracks;
|
||||
|
||||
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
|
||||
import com.google.android.apps.mytracks.content.TracksColumns;
|
||||
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.ContentUris;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.net.Uri;
|
||||
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 FileUtils fileUtils;
|
||||
private boolean singleTrackSelected;
|
||||
private String gpxPath;
|
||||
private WakeLock wakeLock;
|
||||
private ProgressDialog progress;
|
||||
private int gpxFileCount;
|
||||
private int importSuccessCount;
|
||||
private long importedTrackIds[];
|
||||
|
||||
public ImportAllTracks(Activity activity) {
|
||||
this(activity, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor to import tracks.
|
||||
*
|
||||
* @param activity the activity
|
||||
* @param path path of the gpx file to import and display. If null, then just
|
||||
* import all the gpx files under MyTracks/gpx and do not display any
|
||||
* track.
|
||||
*/
|
||||
public ImportAllTracks(Activity activity, String path) {
|
||||
Log.i(Constants.TAG, "ImportAllTracks: Starting");
|
||||
this.activity = activity;
|
||||
fileUtils = new FileUtils();
|
||||
singleTrackSelected = path != null;
|
||||
gpxPath = path == null ? fileUtils.buildExternalDirectoryPath("gpx") : path;
|
||||
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, Context.MODE_PRIVATE);
|
||||
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 Runnable() {
|
||||
@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_no_file, gpxPath));
|
||||
} else {
|
||||
String totalFiles = activity.getResources().getQuantityString(
|
||||
R.plurals.importGpxFiles, gpxFileCount, gpxFileCount);
|
||||
builder.setMessage(
|
||||
activity.getString(R.string.import_success, importSuccessCount, totalFiles, gpxPath));
|
||||
}
|
||||
builder.setPositiveButton(R.string.generic_ok, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
if (singleTrackSelected) {
|
||||
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);
|
||||
activity.startActivity(intent);
|
||||
activity.finish();
|
||||
}
|
||||
}
|
||||
});
|
||||
builder.show();
|
||||
}
|
||||
|
||||
private void makeProgressDialog(final int trackCount) {
|
||||
String importMsg = activity.getString(R.string.track_list_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(final File gpxFile, MyTracksProviderUtils providerUtils) {
|
||||
Log.i(Constants.TAG, "ImportAllTracks: importing: " + gpxFile.getName());
|
||||
try {
|
||||
importedTrackIds = 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);
|
||||
}
|
||||
activity.runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Toast.makeText(activity, activity.getString(R.string.import_error, gpxFile.getName()),
|
||||
Toast.LENGTH_LONG).show();
|
||||
}
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a list of the GPX files. If singleTrackSelected is true, returns a
|
||||
* list containing just the gpxPath file. If singleTrackSelected is false,
|
||||
* returns a list of GPX files under the gpxPath directory.
|
||||
*/
|
||||
private List<File> getGpxFiles() {
|
||||
List<File> gpxFiles = new LinkedList<File>();
|
||||
File file = new File(gpxPath);
|
||||
if (singleTrackSelected) {
|
||||
gpxFiles.add(file);
|
||||
} else {
|
||||
File[] gpxFileCandidates = file.listFiles();
|
||||
if (gpxFileCandidates != null) {
|
||||
for (File candidate : gpxFileCandidates) {
|
||||
if (!candidate.isDirectory() && candidate.getName().endsWith(".gpx")) {
|
||||
gpxFiles.add(candidate);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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,716 @@
|
||||
/*
|
||||
* 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.SendRequest;
|
||||
import com.google.android.apps.mytracks.io.sendtogoogle.UploadServiceChooserActivity;
|
||||
import com.google.android.apps.mytracks.stats.TripStatistics;
|
||||
import com.google.android.apps.mytracks.util.GeoRect;
|
||||
import com.google.android.apps.mytracks.util.LocationUtils;
|
||||
import com.google.android.apps.mytracks.util.PlayTrackUtils;
|
||||
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.app.Dialog;
|
||||
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.speech.tts.TextToSpeech;
|
||||
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 {
|
||||
|
||||
private static final int DIALOG_INSTALL_EARTH = 0;
|
||||
|
||||
// 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 ID of a track on which we want to show a waypoint.
|
||||
* The waypoint will be shown as soon as the track is loaded.
|
||||
*/
|
||||
private long showWaypointTrackId;
|
||||
|
||||
/**
|
||||
* The ID of a waypoint which we want to show.
|
||||
* The waypoint will be shown as soon as its track is loaded.
|
||||
*/
|
||||
private long showWaypointId;
|
||||
|
||||
/**
|
||||
* The track that's currently selected.
|
||||
* This differs from {@link TrackDataHub#getSelectedTrackId} in that this one is only set after
|
||||
* actual track data has been received.
|
||||
*/
|
||||
private long selectedTrackId;
|
||||
|
||||
/**
|
||||
* 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
|
||||
setVolumeControlStream(TextToSpeech.Engine.DEFAULT_STREAM);
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Dialog onCreateDialog(int id) {
|
||||
switch (id) {
|
||||
case DIALOG_INSTALL_EARTH:
|
||||
return PlayTrackUtils.createInstallEarthDialog(this);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Zooms and pans the map so that the given waypoint is visible, when the given track is loaded.
|
||||
* If the track is already loaded, it does that immediately.
|
||||
*
|
||||
* @param trackId the ID of the track on which to show the waypoint
|
||||
* @param waypointId the ID of the waypoint to show
|
||||
*/
|
||||
public void showWaypoint(long trackId, long waypointId) {
|
||||
synchronized (this) {
|
||||
if (trackId == selectedTrackId) {
|
||||
showWaypoint(waypointId);
|
||||
return;
|
||||
}
|
||||
|
||||
showWaypointTrackId = trackId;
|
||||
showWaypointId = waypointId;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Does the proper zooming/panning for a just-loaded track.
|
||||
* This may be either zooming to a waypoint that has been previously selected, or
|
||||
* zooming to the whole track.
|
||||
*
|
||||
* @param track the loaded track
|
||||
*/
|
||||
private void zoomLoadedTrack(Track track) {
|
||||
synchronized (this) {
|
||||
if (track.getId() == showWaypointTrackId) {
|
||||
// There's a waypoint to show in this track.
|
||||
showWaypoint(showWaypointId);
|
||||
|
||||
showWaypointId = 0L;
|
||||
showWaypointTrackId = 0L;
|
||||
} else {
|
||||
// Zoom out to show the whole track.
|
||||
zoomMapToBoundaries(track);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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);
|
||||
|
||||
synchronized (this) {
|
||||
// Need to get the track ID only at this point, to prevent a race condition
|
||||
// among showWaypoint, zoomLoadedTrack and dataHub.loadTrack.
|
||||
selectedTrackId = track.getId();
|
||||
zoomLoadedTrack(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.track_list_context_menu_title);
|
||||
menu.add(Menu.NONE, Constants.MENU_EDIT, Menu.NONE, R.string.track_list_edit_track);
|
||||
if (!dataHub.isRecordingSelected()) {
|
||||
String saveFileFormat = getString(R.string.track_list_save_file);
|
||||
String shareFileFormat = getString(R.string.track_list_share_file);
|
||||
String fileTypes[] = getResources().getStringArray(R.array.file_types);
|
||||
|
||||
menu.add(Menu.NONE, Constants.MENU_PLAY, Menu.NONE, R.string.track_list_play);
|
||||
menu.add(Menu.NONE, Constants.MENU_SEND_TO_GOOGLE, Menu.NONE,
|
||||
R.string.track_list_send_google);
|
||||
SubMenu share = menu.addSubMenu(
|
||||
Menu.NONE, Constants.MENU_SHARE, Menu.NONE, R.string.track_list_share_track);
|
||||
share.add(
|
||||
Menu.NONE, Constants.MENU_SHARE_MAP, Menu.NONE, R.string.track_list_share_map);
|
||||
share.add(Menu.NONE, Constants.MENU_SHARE_FUSION_TABLE, Menu.NONE,
|
||||
R.string.track_list_share_fusion_table);
|
||||
share.add(Menu.NONE, Constants.MENU_SHARE_GPX_FILE, Menu.NONE,
|
||||
String.format(shareFileFormat, fileTypes[0]));
|
||||
share.add(Menu.NONE, Constants.MENU_SHARE_KML_FILE, Menu.NONE,
|
||||
String.format(shareFileFormat, fileTypes[1]));
|
||||
share.add(Menu.NONE, Constants.MENU_SHARE_CSV_FILE, Menu.NONE,
|
||||
String.format(shareFileFormat, fileTypes[2]));
|
||||
share.add(Menu.NONE, Constants.MENU_SHARE_TCX_FILE, Menu.NONE,
|
||||
String.format(shareFileFormat, fileTypes[3]));
|
||||
SubMenu save = menu.addSubMenu(
|
||||
Menu.NONE, Constants.MENU_WRITE_TO_SD_CARD, Menu.NONE, R.string.track_list_save_sd);
|
||||
save.add(Menu.NONE, Constants.MENU_SAVE_GPX_FILE, Menu.NONE,
|
||||
String.format(saveFileFormat, fileTypes[0]));
|
||||
save.add(Menu.NONE, Constants.MENU_SAVE_KML_FILE, Menu.NONE,
|
||||
String.format(saveFileFormat, fileTypes[1]));
|
||||
save.add(Menu.NONE, Constants.MENU_SAVE_CSV_FILE, Menu.NONE,
|
||||
String.format(saveFileFormat, fileTypes[2]));
|
||||
save.add(Menu.NONE, Constants.MENU_SAVE_TCX_FILE, Menu.NONE,
|
||||
String.format(saveFileFormat, fileTypes[3]));
|
||||
menu.add(Menu.NONE, Constants.MENU_CLEAR_MAP, Menu.NONE, R.string.track_list_clear_map);
|
||||
menu.add(Menu.NONE, Constants.MENU_DELETE, Menu.NONE, R.string.track_list_delete_track);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public boolean onMenuItemSelected(int featureId, MenuItem item) {
|
||||
Intent intent;
|
||||
long trackId = dataHub.getSelectedTrackId();
|
||||
switch (item.getItemId()) {
|
||||
case Constants.MENU_EDIT:
|
||||
intent = new Intent(this, TrackDetail.class).putExtra(TrackDetail.TRACK_ID, trackId);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
case Constants.MENU_PLAY:
|
||||
if (PlayTrackUtils.isEarthInstalled(this)) {
|
||||
PlayTrackUtils.playTrack(this, trackId);
|
||||
return true;
|
||||
} else {
|
||||
showDialog(DIALOG_INSTALL_EARTH);
|
||||
return true;
|
||||
}
|
||||
case Constants.MENU_SEND_TO_GOOGLE:
|
||||
intent = new Intent(this, UploadServiceChooserActivity.class)
|
||||
.putExtra(SendRequest.SEND_REQUEST_KEY, new SendRequest(trackId, true, true, true));
|
||||
startActivity(intent);
|
||||
return true;
|
||||
case Constants.MENU_SHARE_MAP:
|
||||
intent = new Intent(this, UploadServiceChooserActivity.class)
|
||||
.putExtra(SendRequest.SEND_REQUEST_KEY, new SendRequest(trackId, true, false, false));
|
||||
startActivity(intent);
|
||||
return true;
|
||||
case Constants.MENU_SHARE_FUSION_TABLE:
|
||||
intent = new Intent(this, UploadServiceChooserActivity.class)
|
||||
.putExtra(SendRequest.SEND_REQUEST_KEY, new SendRequest(trackId, false, true, false));
|
||||
startActivity(intent);
|
||||
return true;
|
||||
case Constants.MENU_SHARE_GPX_FILE:
|
||||
case Constants.MENU_SHARE_KML_FILE:
|
||||
case Constants.MENU_SHARE_CSV_FILE:
|
||||
case Constants.MENU_SHARE_TCX_FILE:
|
||||
case Constants.MENU_SAVE_GPX_FILE:
|
||||
case Constants.MENU_SAVE_KML_FILE:
|
||||
case Constants.MENU_SAVE_CSV_FILE:
|
||||
case Constants.MENU_SAVE_TCX_FILE:
|
||||
SaveActivity.handleExportTrackAction(
|
||||
this, trackId, Constants.getActionFromMenuId(item.getItemId()));
|
||||
return true;
|
||||
case Constants.MENU_CLEAR_MAP:
|
||||
dataHub.unloadCurrentTrack();
|
||||
return true;
|
||||
case Constants.MENU_DELETE:
|
||||
Uri uri = ContentUris.withAppendedId(TracksColumns.CONTENT_URI, trackId);
|
||||
intent = new Intent(Intent.ACTION_DELETE, uri);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
default:
|
||||
return super.onMenuItemSelected(featureId, item);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
super.onCreateOptionsMenu(menu);
|
||||
myLocation = menu.add(
|
||||
Menu.NONE, Constants.MENU_MY_LOCATION, Menu.NONE, R.string.menu_map_view_my_location);
|
||||
myLocation.setIcon(android.R.drawable.ic_menu_mylocation);
|
||||
toggleLayers = menu.add(
|
||||
Menu.NONE, Constants.MENU_TOGGLE_LAYERS, Menu.NONE, R.string.menu_map_view_satellite_mode);
|
||||
toggleLayers.setIcon(android.R.drawable.ic_menu_mapmode);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareOptionsMenu(Menu menu) {
|
||||
toggleLayers.setTitle(mapView.isSatellite() ?
|
||||
R.string.menu_map_view_map_mode : R.string.menu_map_view_satellite_mode);
|
||||
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);
|
||||
}
|
||||
if (currentLocation != null) {
|
||||
showCurrentLocation();
|
||||
}
|
||||
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.gps_need_to_enable;
|
||||
isGpsDisabled = true;
|
||||
break;
|
||||
case NO_FIX:
|
||||
case BAD_FIX:
|
||||
messageId = R.string.gps_wait_for_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.gps_not_found,
|
||||
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.apps.mytracks.util.UnitConversions;
|
||||
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 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() * UnitConversions.MS_TO_KMH);
|
||||
}
|
||||
};
|
||||
|
||||
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, Context.MODE_PRIVATE)
|
||||
.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,102 @@
|
||||
/*
|
||||
* Copyright 2010 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package com.google.android.apps.mytracks;
|
||||
|
||||
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);
|
||||
|
||||
// TODO: Replace search button with search widget if API level >= 11
|
||||
return true;
|
||||
}
|
||||
|
||||
public void onPrepareOptionsMenu(Menu menu, boolean hasRecorded,
|
||||
boolean isRecording, boolean hasSelectedTrack) {
|
||||
menu.findItem(R.id.menu_markers)
|
||||
.setEnabled(hasRecorded && hasSelectedTrack);
|
||||
menu.findItem(R.id.menu_record_track)
|
||||
.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_record_track: {
|
||||
activity.startRecording();
|
||||
return true;
|
||||
}
|
||||
case R.id.menu_stop_recording: {
|
||||
activity.stopRecording();
|
||||
return true;
|
||||
}
|
||||
case R.id.menu_tracks: {
|
||||
activity.startActivityForResult(new Intent(activity, TrackList.class),
|
||||
Constants.SHOW_TRACK);
|
||||
return true;
|
||||
}
|
||||
case R.id.menu_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_statistics: {
|
||||
return startActivity(AggregatedStatsActivity.class);
|
||||
}
|
||||
case R.id.menu_help: {
|
||||
return startActivity(WelcomeActivity.class);
|
||||
}
|
||||
case R.id.menu_search: {
|
||||
// TODO: Pass the current track ID and current location to do some fancier ranking.
|
||||
activity.onSearchRequested();
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean startActivity(Class<? extends Activity> activityClass) {
|
||||
activity.startActivity(new Intent(activity, activityClass));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,501 @@
|
||||
/*
|
||||
* 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.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.ServiceUtils;
|
||||
import com.google.android.apps.mytracks.services.TrackRecordingServiceConnection;
|
||||
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
|
||||
import com.google.android.apps.mytracks.util.EulaUtils;
|
||||
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.AlertDialog;
|
||||
import android.app.Dialog;
|
||||
import android.app.TabActivity;
|
||||
import android.content.ContentUris;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
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.speech.tts.TextToSpeech;
|
||||
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 static final int DIALOG_EULA_ID = 0;
|
||||
|
||||
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);
|
||||
if (!SystemUtils.isRelease(this)) {
|
||||
ApiAdapterFactory.getApiAdapter().enableStrictMode();
|
||||
}
|
||||
|
||||
tracker = GoogleAnalyticsTracker.getInstance();
|
||||
// Start the tracker in manual dispatch mode...
|
||||
tracker.start(getString(R.string.my_tracks_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, Context.MODE_PRIVATE);
|
||||
dataHub = ((MyTracksApplication) getApplication()).getTrackDataHub();
|
||||
menuManager = new MenuManager(this);
|
||||
serviceConnection = new TrackRecordingServiceConnection(this, serviceBindCallback);
|
||||
|
||||
setVolumeControlStream(TextToSpeech.Engine.DEFAULT_STREAM);
|
||||
|
||||
// We don't need a window title bar:
|
||||
requestWindowFeature(Window.FEATURE_NO_TITLE);
|
||||
|
||||
// If the user just starts typing (on a device with a keyboard), we start a search.
|
||||
setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
|
||||
|
||||
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);
|
||||
|
||||
if (!EulaUtils.getEulaValue(this)) {
|
||||
showDialog(DIALOG_EULA_ID);
|
||||
}
|
||||
}
|
||||
|
||||
@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);
|
||||
} else if (Intent.ACTION_VIEW.equals(action)
|
||||
&& WaypointsColumns.CONTENT_ITEMTYPE.equals(intent.getType())
|
||||
&& UriUtils.matchesContentUri(data, WaypointsColumns.CONTENT_URI)) {
|
||||
// TODO(rdamazio): Waypoint URIs should be base/trackid/waypointid
|
||||
long waypointId = ContentUris.parseId(data);
|
||||
Waypoint waypoint = providerUtils.getWaypoint(waypointId);
|
||||
long trackId = waypoint.getTrackId();
|
||||
|
||||
// Request that the waypoint is shown (now or when the right track is loaded).
|
||||
showWaypoint(trackId, waypointId);
|
||||
|
||||
// Load the right track, if not loaded already.
|
||||
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();
|
||||
super.onStop();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
Log.d(TAG, "MyTracks.onDestroy");
|
||||
serviceConnection.unbind();
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Dialog onCreateDialog(int id) {
|
||||
switch (id) {
|
||||
case DIALOG_EULA_ID:
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||
builder.setTitle(R.string.eula_title);
|
||||
builder.setMessage(EulaUtils.getEulaMessage(this));
|
||||
builder.setPositiveButton(R.string.eula_accept, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
EulaUtils.setEulaValue(MyTracks.this);
|
||||
Intent startIntent = new Intent(MyTracks.this, WelcomeActivity.class);
|
||||
startActivityForResult(startIntent, Constants.WELCOME);
|
||||
}
|
||||
});
|
||||
builder.setNegativeButton(R.string.eula_decline, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
finish();
|
||||
}
|
||||
});
|
||||
builder.setCancelable(true);
|
||||
builder.setOnCancelListener(new DialogInterface.OnCancelListener() {
|
||||
@Override
|
||||
public void onCancel(DialogInterface dialog) {
|
||||
finish();
|
||||
}
|
||||
});
|
||||
return builder.create();
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@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(WaypointDetails.WAYPOINT_ID_EXTRA, -1);
|
||||
if (waypointId >= 0) {
|
||||
showWaypoint(trackId, waypointId);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Constants.WELCOME: {
|
||||
CheckUnits.check(this);
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
Log.w(TAG, "Warning unhandled request code: " + requestCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void showWaypoint(long trackId, long waypointId) {
|
||||
MapActivity map =
|
||||
(MapActivity) getLocalActivityManager().getActivity("tab1");
|
||||
if (map != null) {
|
||||
getTabHost().setCurrentTab(0);
|
||||
map.showWaypoint(trackId, waypointId);
|
||||
} else {
|
||||
Log.e(TAG, "Couldnt' get map tab");
|
||||
}
|
||||
}
|
||||
|
||||
@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.marker_insert_success, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
return waypointId;
|
||||
} catch (RemoteException e) {
|
||||
Toast.makeText(this, R.string.marker_insert_error, 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.track_record_success), 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.track_record_error), 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, Context.MODE_PRIVATE);
|
||||
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, TrackDetail.class);
|
||||
intent.putExtra(TrackDetail.TRACK_ID, currentTrackId);
|
||||
intent.putExtra(TrackDetail.SHOW_CANCEL, false);
|
||||
startActivity(intent);
|
||||
}
|
||||
}
|
||||
|
||||
long getSelectedTrackId() {
|
||||
return dataHub.getSelectedTrackId();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* 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 com.google.android.apps.mytracks.services.RemoveTempFilesService;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.Intent;
|
||||
|
||||
/**
|
||||
* MyTracksApplication for keeping global state.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class MyTracksApplication extends Application {
|
||||
|
||||
private TrackDataHub trackDataHub;
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
startService(new Intent(this, RemoveTempFilesService.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,267 @@
|
||||
/*
|
||||
* 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.SearchEngine;
|
||||
import com.google.android.apps.mytracks.content.SearchEngine.ScoredResult;
|
||||
import com.google.android.apps.mytracks.content.SearchEngine.SearchQuery;
|
||||
import com.google.android.apps.mytracks.content.SearchEngineProvider;
|
||||
import com.google.android.apps.mytracks.content.Track;
|
||||
import com.google.android.apps.mytracks.content.TracksColumns;
|
||||
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.apps.mytracks.util.StringUtils;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.app.ListActivity;
|
||||
import android.app.SearchManager;
|
||||
import android.content.ContentUris;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.location.Location;
|
||||
import android.location.LocationManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.provider.SearchRecentSuggestions;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.ListView;
|
||||
import android.widget.SimpleAdapter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.SortedSet;
|
||||
|
||||
/**
|
||||
* Activity to search for tracks or waypoints.
|
||||
*
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
public class SearchActivity extends ListActivity {
|
||||
|
||||
private static final String ICON_FIELD = "icon";
|
||||
private static final String NAME_FIELD = "name";
|
||||
private static final String DESCRIPTION_FIELD = "description";
|
||||
private static final String CATEGORY_FIELD = "category";
|
||||
private static final String TIME_FIELD = "time";
|
||||
private static final String STATS_FIELD = "stats";
|
||||
private static final String TRACK_ID_FIELD = "trackId";
|
||||
private static final String WAYPOINT_ID_FIELD = "waypointId";
|
||||
|
||||
private static final String EXTRA_CURRENT_TRACK_ID = "trackId";
|
||||
|
||||
private static final boolean LOG_SCORES = true;
|
||||
|
||||
private MyTracksProviderUtils providerUtils;
|
||||
private SearchEngine engine;
|
||||
private LocationManager locationManager;
|
||||
private SharedPreferences preferences;
|
||||
|
||||
private SearchRecentSuggestions suggestions;
|
||||
|
||||
private boolean metricUnits;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
providerUtils = MyTracksProviderUtils.Factory.get(this);
|
||||
engine = new SearchEngine(providerUtils);
|
||||
suggestions = SearchEngineProvider.newHelper(this);
|
||||
locationManager = (LocationManager) getSystemService(LOCATION_SERVICE);
|
||||
preferences = getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
|
||||
|
||||
setContentView(R.layout.search_list);
|
||||
|
||||
handleIntent(getIntent());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
|
||||
metricUnits =
|
||||
preferences.getBoolean(getString(R.string.metric_units_key), true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onNewIntent(Intent intent) {
|
||||
setIntent(intent);
|
||||
handleIntent(intent);
|
||||
}
|
||||
|
||||
private void handleIntent(Intent intent) {
|
||||
if (!Intent.ACTION_SEARCH.equals(intent.getAction())) {
|
||||
Log.e(TAG, "Got bad search intent: " + intent);
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
String textQuery = intent.getStringExtra(SearchManager.QUERY);
|
||||
Location currentLocation = locationManager.getLastKnownLocation("gps");
|
||||
long currentTrackId = intent.getLongExtra(EXTRA_CURRENT_TRACK_ID, -1);
|
||||
long currentTimestamp = System.currentTimeMillis();
|
||||
|
||||
final SearchQuery query =
|
||||
new SearchQuery(textQuery, currentLocation, currentTrackId, currentTimestamp);
|
||||
|
||||
// Do the actual search in a separate thread.
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
doSearch(query);
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
|
||||
private void doSearch(SearchQuery query) {
|
||||
SortedSet<ScoredResult> scoredResults = engine.search(query);
|
||||
|
||||
final List<? extends Map<String, ?>> displayResults = prepareResultsforDisplay(scoredResults);
|
||||
|
||||
if (LOG_SCORES) {
|
||||
Log.i(TAG, "Search scores: " + displayResults);
|
||||
}
|
||||
|
||||
// Then go back to the UI thread to display them.
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
showSearchResults(displayResults);
|
||||
}
|
||||
});
|
||||
|
||||
// Save the query as a suggestion for the future.
|
||||
suggestions.saveRecentQuery(query.textQuery, null);
|
||||
}
|
||||
|
||||
private List<Map<String, Object>> prepareResultsforDisplay(
|
||||
Collection<ScoredResult> scoredResults) {
|
||||
ArrayList<Map<String, Object>> output = new ArrayList<Map<String, Object>>(scoredResults.size());
|
||||
for (ScoredResult result : scoredResults) {
|
||||
Map<String, Object> resultMap = new HashMap<String, Object>();
|
||||
if (result.track != null) {
|
||||
prepareTrackForDisplay(result.track, resultMap);
|
||||
} else {
|
||||
prepareWaypointForDisplay(result.waypoint, resultMap);
|
||||
}
|
||||
|
||||
resultMap.put("score", result.score);
|
||||
|
||||
output.add(resultMap);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
private void prepareWaypointForDisplay(Waypoint waypoint, Map<String, Object> resultMap) {
|
||||
// Look up the owner track.
|
||||
// TODO: It may be more appropriate to do this as a join in the retrieval phase of the search.
|
||||
String trackName = null;
|
||||
long trackId = waypoint.getTrackId();
|
||||
if (trackId > 0) {
|
||||
Track track = providerUtils.getTrack(trackId);
|
||||
if (track != null) {
|
||||
trackName = track.getName();
|
||||
}
|
||||
}
|
||||
|
||||
resultMap.put(ICON_FIELD, waypoint.getType() == Waypoint.TYPE_STATISTICS
|
||||
? R.drawable.ylw_pushpin : R.drawable.blue_pushpin);
|
||||
resultMap.put(NAME_FIELD, waypoint.getName());
|
||||
resultMap.put(DESCRIPTION_FIELD, waypoint.getDescription());
|
||||
resultMap.put(CATEGORY_FIELD, waypoint.getCategory());
|
||||
// In the same place as we show time for tracks, show the track name for waypoints.
|
||||
resultMap.put(TIME_FIELD, getString(R.string.track_list_track_name, trackName));
|
||||
resultMap.put(STATS_FIELD, StringUtils.formatDateTime(this, waypoint.getLocation().getTime()));
|
||||
resultMap.put(TRACK_ID_FIELD, waypoint.getTrackId());
|
||||
resultMap.put(WAYPOINT_ID_FIELD, waypoint.getId());
|
||||
}
|
||||
|
||||
private void prepareTrackForDisplay(Track track, Map<String, Object> resultMap) {
|
||||
TripStatistics stats = track.getStatistics();
|
||||
|
||||
resultMap.put(ICON_FIELD, R.drawable.track);
|
||||
resultMap.put(NAME_FIELD, track.getName());
|
||||
resultMap.put(DESCRIPTION_FIELD, track.getDescription());
|
||||
resultMap.put(CATEGORY_FIELD, track.getCategory());
|
||||
resultMap.put(TIME_FIELD, StringUtils.formatDateTime(this, stats.getStartTime()));
|
||||
resultMap.put(STATS_FIELD,
|
||||
StringUtils.formatTimeDistance(this, stats.getTotalDistance(), stats.getTotalTime(),
|
||||
metricUnits));
|
||||
resultMap.put(TRACK_ID_FIELD, track.getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the given search results.
|
||||
* Must be run from the UI thread.
|
||||
*
|
||||
* @param data the results to show, properly ordered
|
||||
*/
|
||||
private void showSearchResults(List<? extends Map<String, ?>> data) {
|
||||
SimpleAdapter adapter = new SimpleAdapter(this, data,
|
||||
// TODO: Custom view for search results.
|
||||
R.layout.mytracks_list_item,
|
||||
new String[] {
|
||||
ICON_FIELD,
|
||||
NAME_FIELD,
|
||||
DESCRIPTION_FIELD,
|
||||
CATEGORY_FIELD,
|
||||
TIME_FIELD,
|
||||
STATS_FIELD,
|
||||
},
|
||||
new int[] {
|
||||
R.id.track_list_item_icon,
|
||||
R.id.track_list_item_name,
|
||||
R.id.track_list_item_description,
|
||||
R.id.track_list_item_category,
|
||||
R.id.track_list_item_time,
|
||||
R.id.track_list_item_stats,
|
||||
});
|
||||
|
||||
setListAdapter(adapter);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onListItemClick(ListView l, View v, int position, long id) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> clickedData = (Map<String, Object>) getListAdapter().getItem(position);
|
||||
|
||||
startActivity(createViewDataIntent(clickedData));
|
||||
}
|
||||
|
||||
private Intent createViewDataIntent(Map<String, Object> clickedData) {
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW);
|
||||
if (clickedData.containsKey(WAYPOINT_ID_FIELD)) {
|
||||
long waypointId = (Long) clickedData.get(WAYPOINT_ID_FIELD);
|
||||
Uri uri = ContentUris.withAppendedId(WaypointsColumns.CONTENT_URI, waypointId);
|
||||
intent.setDataAndType(uri, WaypointsColumns.CONTENT_ITEMTYPE);
|
||||
} else {
|
||||
long trackId = (Long) clickedData.get(TRACK_ID_FIELD);
|
||||
Uri uri = ContentUris.withAppendedId(TracksColumns.CONTENT_URI, trackId);
|
||||
intent.setDataAndType(uri, TracksColumns.CONTENT_ITEMTYPE);
|
||||
}
|
||||
return intent;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,336 @@
|
||||
/*
|
||||
* 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.apps.mytracks.util.StringUtils;
|
||||
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.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
|
||||
/**
|
||||
* An activity that displays information about sensors.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class SensorStateActivity extends Activity {
|
||||
|
||||
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.
|
||||
*/
|
||||
private 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.
|
||||
*/
|
||||
private 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();
|
||||
}
|
||||
|
||||
private 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.getInstance().getSensorManager(this);
|
||||
}
|
||||
|
||||
// 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) {
|
||||
SensorManagerFactory.getInstance().releaseSensorManager(tempSensorManager);
|
||||
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) findViewById(R.id.sensor_state)).setText(SensorUtils.getStateAsString(state, this));
|
||||
}
|
||||
|
||||
private void updateSensorData(Sensor.SensorDataSet sds) {
|
||||
if (sds == null) {
|
||||
utils.setUnknown(R.id.sensor_state_last_sensor_time);
|
||||
utils.setUnknown(R.id.sensor_state_power);
|
||||
utils.setUnknown(R.id.sensor_state_cadence);
|
||||
utils.setUnknown(R.id.sensor_state_battery);
|
||||
} else {
|
||||
((TextView) findViewById(R.id.sensor_state_last_sensor_time)).setText(getLastSensorTime(sds));
|
||||
((TextView) findViewById(R.id.sensor_state_power)).setText(getPower(sds));
|
||||
((TextView) findViewById(R.id.sensor_state_cadence)).setText(getCadence(sds));
|
||||
((TextView) findViewById(R.id.sensor_state_heart_rate)).setText(getHeartRate(sds));
|
||||
((TextView) findViewById(R.id.sensor_state_battery)).setText(getBattery(sds));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the last sensor time.
|
||||
*
|
||||
* @param sds sensor data set
|
||||
*/
|
||||
private String getLastSensorTime(Sensor.SensorDataSet sds) {
|
||||
return StringUtils.formatTime(this, sds.getCreationTime());
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the power.
|
||||
*
|
||||
* @param sds sensor data set
|
||||
*/
|
||||
private String getPower(Sensor.SensorDataSet sds) {
|
||||
String value;
|
||||
if (sds.hasPower() && sds.getPower().hasValue()
|
||||
&& sds.getPower().getState() == Sensor.SensorState.SENDING) {
|
||||
String format = getString(R.string.sensor_state_power_value);
|
||||
value = String.format(format, sds.getPower().getValue());
|
||||
} else {
|
||||
value = SensorUtils.getStateAsString(
|
||||
sds.hasPower() ? sds.getPower().getState() : Sensor.SensorState.NONE, this);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the cadence.
|
||||
*
|
||||
* @param sds sensor data set
|
||||
*/
|
||||
private String getCadence(Sensor.SensorDataSet sds) {
|
||||
String value;
|
||||
if (sds.hasCadence() && sds.getCadence().hasValue()
|
||||
&& sds.getCadence().getState() == Sensor.SensorState.SENDING) {
|
||||
String format = getString(R.string.sensor_state_cadence_value);
|
||||
value = String.format(format, sds.getCadence().getValue());
|
||||
} else {
|
||||
value = SensorUtils.getStateAsString(
|
||||
sds.hasCadence() ? sds.getCadence().getState() : Sensor.SensorState.NONE, this);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the heart rate.
|
||||
*
|
||||
* @param sds sensor data set
|
||||
*/
|
||||
private String getHeartRate(Sensor.SensorDataSet sds) {
|
||||
String value;
|
||||
if (sds.hasHeartRate() && sds.getHeartRate().hasValue()
|
||||
&& sds.getHeartRate().getState() == Sensor.SensorState.SENDING) {
|
||||
String format = getString(R.string.sensor_state_heart_rate_value);
|
||||
value = String.format(format, sds.getHeartRate().getValue());
|
||||
} else {
|
||||
value = SensorUtils.getStateAsString(
|
||||
sds.hasHeartRate() ? sds.getHeartRate().getState() : Sensor.SensorState.NONE, this);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the battery.
|
||||
*
|
||||
* @param sds sensor data set
|
||||
*/
|
||||
private String getBattery(Sensor.SensorDataSet sds) {
|
||||
String value;
|
||||
if (sds.hasBatteryLevel() && sds.getBatteryLevel().hasValue()
|
||||
&& sds.getBatteryLevel().getState() == Sensor.SensorState.SENDING) {
|
||||
String format = getString(R.string.sensor_state_battery_value);
|
||||
value = String.format(format, sds.getBatteryLevel().getValue());
|
||||
} else {
|
||||
value = SensorUtils.getStateAsString(
|
||||
sds.hasBatteryLevel() ? sds.getBatteryLevel().getState() : Sensor.SensorState.NONE, this);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,713 @@
|
||||
/*
|
||||
* 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.util.ApiAdapterFactory;
|
||||
import com.google.android.apps.mytracks.util.BluetoothDeviceUtils;
|
||||
import com.google.android.apps.mytracks.util.DialogUtils;
|
||||
import com.google.android.apps.mytracks.util.UnitConversions;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.DialogInterface.OnClickListener;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.SharedPreferences.Editor;
|
||||
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.speech.tts.TextToSpeech;
|
||||
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.
|
||||
*
|
||||
* @author Leif Hendrik Wilden
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
public class SettingsActivity extends PreferenceActivity {
|
||||
|
||||
private static final int DIALOG_CONFIRM_RESET = 0;
|
||||
private static final int DIALOG_CONFIRM_ACCESS = 1;
|
||||
|
||||
// Value when the task frequency is off.
|
||||
private static final String TASK_FREQUENCY_OFF = "0";
|
||||
|
||||
// Value when the recording interval is 'Adapt battery life'.
|
||||
private static final String RECORDING_INTERVAL_ADAPT_BATTERY_LIFE = "-2";
|
||||
|
||||
// Value when the recording interval is 'Adapt accuracy'.
|
||||
private static final String RECORDING_INTERVAL_ADAPT_ACCURACY = "-1";
|
||||
|
||||
// Value for the recommended recording interval.
|
||||
private static final String RECORDING_INTERVAL_RECOMMENDED = "0";
|
||||
|
||||
// Value when the auto resume timeout is never.
|
||||
private static final String AUTO_RESUME_TIMEOUT_NEVER = "0";
|
||||
|
||||
// Value when the auto resume timeout is always.
|
||||
private static final String AUTO_RESUME_TIMEOUT_ALWAYS = "-1";
|
||||
|
||||
// Value for the recommended recording distance.
|
||||
private static final String RECORDING_DISTANCE_RECOMMENDED = "5";
|
||||
|
||||
// Value for the recommended track distance.
|
||||
private static final String TRACK_DISTANCE_RECOMMENDED = "200";
|
||||
|
||||
// Value for the recommended GPS accuracy.
|
||||
private static final String GPS_ACCURACY_RECOMMENDED = "200";
|
||||
|
||||
// Value when the GPS accuracy is for excellent GPS signal.
|
||||
private static final String GPS_ACCURACY_EXCELLENT = "10";
|
||||
|
||||
// Value when the GPS accuracy is for poor GPS signal.
|
||||
private static final String GPS_ACCURACY_POOR = "5000";
|
||||
|
||||
private BackupPreferencesListener backupListener;
|
||||
private SharedPreferences preferences;
|
||||
|
||||
/** Called when the activity is first created. */
|
||||
@Override
|
||||
protected void onCreate(Bundle icicle) {
|
||||
super.onCreate(icicle);
|
||||
|
||||
// The volume we want to control is the Text-To-Speech volume
|
||||
setVolumeControlStream(TextToSpeech.Engine.DEFAULT_STREAM);
|
||||
|
||||
// Tell it where to read/write preferences
|
||||
PreferenceManager preferenceManager = getPreferenceManager();
|
||||
preferenceManager.setSharedPreferencesName(Constants.SETTINGS_NAME);
|
||||
preferenceManager.setSharedPreferencesMode(0);
|
||||
|
||||
// Set up automatic preferences backup
|
||||
backupListener = ApiAdapterFactory.getApiAdapter().getBackupPreferencesListener(this);
|
||||
preferences = preferenceManager.getSharedPreferences();
|
||||
preferences.registerOnSharedPreferenceChangeListener(backupListener);
|
||||
|
||||
// Load the preferences to be displayed
|
||||
addPreferencesFromResource(R.xml.preferences);
|
||||
|
||||
setRecordingIntervalOptions();
|
||||
setAutoResumeTimeoutOptions();
|
||||
|
||||
// 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;
|
||||
updateDisplayOptions(isMetric);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
updateDisplayOptions(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) {
|
||||
showDialog(DIALOG_CONFIRM_RESET);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
// Add a confirmation dialog for the 'Allow access' preference.
|
||||
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) {
|
||||
showDialog(DIALOG_CONFIRM_ACCESS);
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Dialog onCreateDialog(int id) {
|
||||
switch (id) {
|
||||
case DIALOG_CONFIRM_RESET:
|
||||
return DialogUtils.createConfirmationDialog(
|
||||
this, R.string.settings_reset_confirm_message, new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int button) {
|
||||
onResetPreferencesConfirmed();
|
||||
}
|
||||
});
|
||||
case DIALOG_CONFIRM_ACCESS:
|
||||
return DialogUtils.createConfirmationDialog(
|
||||
this, R.string.settings_sharing_allow_access_confirm_message, new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int button) {
|
||||
CheckBoxPreference pref = (CheckBoxPreference) findPreference(
|
||||
getString(R.string.allow_access_key));
|
||||
pref.setChecked(true);
|
||||
}
|
||||
});
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the display options for the 'Time between points' option.
|
||||
*/
|
||||
private void setRecordingIntervalOptions() {
|
||||
String[] values = getResources().getStringArray(R.array.recording_interval_values);
|
||||
String[] options = new String[values.length];
|
||||
for (int i = 0; i < values.length; i++) {
|
||||
if (values[i].equals(RECORDING_INTERVAL_ADAPT_BATTERY_LIFE)) {
|
||||
options[i] = getString(R.string.value_adapt_battery_life);
|
||||
} else if (values[i].equals(RECORDING_INTERVAL_ADAPT_ACCURACY)) {
|
||||
options[i] = getString(R.string.value_adapt_accuracy);
|
||||
} else if (values[i].equals(RECORDING_INTERVAL_RECOMMENDED)) {
|
||||
options[i] = getString(R.string.value_smallest_recommended);
|
||||
} else {
|
||||
int value = Integer.parseInt(values[i]);
|
||||
String format;
|
||||
if (value < 60) {
|
||||
format = getString(R.string.value_integer_second);
|
||||
} else {
|
||||
value = value / 60;
|
||||
format = getString(R.string.value_integer_minute);
|
||||
}
|
||||
options[i] = String.format(format, value);
|
||||
}
|
||||
}
|
||||
ListPreference list = (ListPreference) findPreference(
|
||||
getString(R.string.min_recording_interval_key));
|
||||
list.setEntries(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the display options for the 'Auto-resume timeout' option.
|
||||
*/
|
||||
private void setAutoResumeTimeoutOptions() {
|
||||
String[] values = getResources().getStringArray(R.array.recording_auto_resume_timeout_values);
|
||||
String[] options = new String[values.length];
|
||||
for (int i = 0; i < values.length; i++) {
|
||||
if (values[i].equals(AUTO_RESUME_TIMEOUT_NEVER)) {
|
||||
options[i] = getString(R.string.value_never);
|
||||
} else if (values[i].equals(AUTO_RESUME_TIMEOUT_ALWAYS)) {
|
||||
options[i] = getString(R.string.value_always);
|
||||
} else {
|
||||
int value = Integer.parseInt(values[i]);
|
||||
String format = getString(R.string.value_integer_minute);
|
||||
options[i] = String.format(format, value);
|
||||
}
|
||||
}
|
||||
ListPreference list = (ListPreference) findPreference(
|
||||
getString(R.string.auto_resume_track_timeout_key));
|
||||
list.setEntries(options);
|
||||
}
|
||||
|
||||
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.sensor_type_ant_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();
|
||||
|
||||
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_now_summary);
|
||||
restoreNowPreference.setSummary(
|
||||
recording ? R.string.settings_not_while_recording
|
||||
: R.string.settings_backup_restore_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.display_track_color_value_fixed));
|
||||
boolean usesDynamicSpeed =
|
||||
trackColorMode.equals(getString(R.string.display_track_color_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 display options that depends on the preferred distance units, metric or imperial.
|
||||
*
|
||||
* @param isMetric true to use metric units, false to use imperial
|
||||
*/
|
||||
private void updateDisplayOptions(boolean isMetric) {
|
||||
setTaskOptions(isMetric, R.string.announcement_frequency_key);
|
||||
setTaskOptions(isMetric, R.string.split_frequency_key);
|
||||
setRecordingDistanceOptions(isMetric, R.string.min_recording_distance_key);
|
||||
setTrackDistanceOptions(isMetric, R.string.max_recording_distance_key);
|
||||
setGpsAccuracyOptions(isMetric, R.string.min_required_accuracy_key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the display options for a periodic task.
|
||||
*/
|
||||
private void setTaskOptions(boolean isMetric, int listId) {
|
||||
String[] values = getResources().getStringArray(R.array.recording_task_frequency_values);
|
||||
String[] options = new String[values.length];
|
||||
for (int i = 0; i < values.length; i++) {
|
||||
if (values[i].equals(TASK_FREQUENCY_OFF)) {
|
||||
options[i] = getString(R.string.value_off);
|
||||
} else if (values[i].startsWith("-")) {
|
||||
int value = Integer.parseInt(values[i].substring(1));
|
||||
int stringId = isMetric ? R.string.value_integer_kilometer : R.string.value_integer_mile;
|
||||
String format = getString(stringId);
|
||||
options[i] = String.format(format, value);
|
||||
} else {
|
||||
int value = Integer.parseInt(values[i]);
|
||||
String format = getString(R.string.value_integer_minute);
|
||||
options[i] = String.format(format, value);
|
||||
}
|
||||
}
|
||||
|
||||
ListPreference list = (ListPreference) findPreference(getString(listId));
|
||||
list.setEntries(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the display options for 'Distance between points' option.
|
||||
*/
|
||||
private void setRecordingDistanceOptions(boolean isMetric, int listId) {
|
||||
String[] values = getResources().getStringArray(R.array.recording_distance_values);
|
||||
String[] options = new String[values.length];
|
||||
for (int i = 0; i < values.length; i++) {
|
||||
int value = Integer.parseInt(values[i]);
|
||||
if (!isMetric) {
|
||||
value = (int) (value * UnitConversions.M_TO_FT);
|
||||
}
|
||||
String format;
|
||||
if (values[i].equals(RECORDING_DISTANCE_RECOMMENDED)) {
|
||||
int stringId = isMetric ? R.string.value_integer_meter_recommended
|
||||
: R.string.value_integer_feet_recommended;
|
||||
format = getString(stringId);
|
||||
} else {
|
||||
int stringId = isMetric ? R.string.value_integer_meter : R.string.value_integer_feet;
|
||||
format = getString(stringId);
|
||||
}
|
||||
options[i] = String.format(format, value);
|
||||
}
|
||||
|
||||
ListPreference list = (ListPreference) findPreference(getString(listId));
|
||||
list.setEntries(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the display options for 'Distance between Tracks'.
|
||||
*/
|
||||
private void setTrackDistanceOptions(boolean isMetric, int listId) {
|
||||
String[] values = getResources().getStringArray(R.array.recording_track_distance_values);
|
||||
String[] options = new String[values.length];
|
||||
for (int i = 0; i < values.length; i++) {
|
||||
int value = Integer.parseInt(values[i]);
|
||||
String format;
|
||||
if (isMetric) {
|
||||
int stringId = values[i].equals(TRACK_DISTANCE_RECOMMENDED)
|
||||
? R.string.value_integer_meter_recommended : R.string.value_integer_meter;
|
||||
format = getString(stringId);
|
||||
options[i] = String.format(format, value);
|
||||
} else {
|
||||
value = (int) (value * UnitConversions.M_TO_FT);
|
||||
if (value < 2000) {
|
||||
int stringId = values[i].equals(TRACK_DISTANCE_RECOMMENDED)
|
||||
? R.string.value_integer_feet_recommended : R.string.value_integer_feet;
|
||||
format = getString(stringId);
|
||||
options[i] = String.format(format, value);
|
||||
} else {
|
||||
double mile = value * UnitConversions.FT_TO_MI;
|
||||
format = getString(R.string.value_float_mile);
|
||||
options[i] = String.format(format, mile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ListPreference list = (ListPreference) findPreference(getString(listId));
|
||||
list.setEntries(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the display options for 'GPS accuracy'.
|
||||
*/
|
||||
private void setGpsAccuracyOptions(boolean isMetric, int listId) {
|
||||
String[] values = getResources().getStringArray(R.array.recording_gps_accuracy_values);
|
||||
String[] options = new String[values.length];
|
||||
for (int i = 0; i < values.length; i++) {
|
||||
int value = Integer.parseInt(values[i]);
|
||||
String format;
|
||||
if (isMetric) {
|
||||
if (values[i].equals(GPS_ACCURACY_RECOMMENDED)) {
|
||||
format = getString(R.string.value_integer_meter_recommended);
|
||||
} else if (values[i].equals(GPS_ACCURACY_EXCELLENT)) {
|
||||
format = getString(R.string.value_integer_meter_excellent_gps);
|
||||
} else if (values[i].equals(GPS_ACCURACY_POOR)) {
|
||||
format = getString(R.string.value_integer_meter_poor_gps);
|
||||
} else {
|
||||
format = getString(R.string.value_integer_meter);
|
||||
}
|
||||
options[i] = String.format(format, value);
|
||||
} else {
|
||||
value = (int) (value * UnitConversions.M_TO_FT);
|
||||
if (value < 2000) {
|
||||
if (values[i].equals(GPS_ACCURACY_RECOMMENDED)) {
|
||||
format = getString(R.string.value_integer_feet_recommended);
|
||||
} else if (values[i].equals(GPS_ACCURACY_EXCELLENT)) {
|
||||
format = getString(R.string.value_integer_feet_excellent_gps);
|
||||
} else {
|
||||
format = getString(R.string.value_integer_feet);
|
||||
}
|
||||
options[i] = String.format(format, value);
|
||||
} else {
|
||||
double mile = value * UnitConversions.FT_TO_MI;
|
||||
if (values[i].equals(GPS_ACCURACY_POOR)) {
|
||||
format = getString(R.string.value_float_mile_poor_gps);
|
||||
} else {
|
||||
format = getString(R.string.value_float_mile);
|
||||
}
|
||||
options[i] = String.format(format, mile);
|
||||
}
|
||||
}
|
||||
}
|
||||
ListPreference list = (ListPreference) findPreference(getString(listId));
|
||||
list.setEntries(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures preference actions related to bluetooth.
|
||||
*/
|
||||
private void configureBluetoothPreferences() {
|
||||
// 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
|
||||
BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
|
||||
if (bluetoothAdapter != null) {
|
||||
BluetoothDeviceUtils.populateDeviceLists(bluetoothAdapter, 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 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.KM_TO_MI);
|
||||
} 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(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.MI_TO_KM));
|
||||
} catch (NumberFormatException e) {
|
||||
metricspeed = "0";
|
||||
}
|
||||
} else {
|
||||
metricspeed = newValue;
|
||||
}
|
||||
SharedPreferences prefs = getPreferenceManager().getSharedPreferences();
|
||||
Editor editor = prefs.edit();
|
||||
editor.putString(getString(id), metricspeed);
|
||||
ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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((String) newValue, metricKey);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
trackColorModePreference.setOnPreferenceClickListener(
|
||||
new OnPreferenceClickListener() {
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
viewTrackColorModeSettings((EditTextPreference) preference, metricKey);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,324 @@
|
||||
/*
|
||||
* 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.util.UnitConversions;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.location.Location;
|
||||
import android.os.Bundle;
|
||||
import android.speech.tts.TextToSpeech;
|
||||
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, Context.MODE_PRIVATE);
|
||||
utils = new StatsUtilities(this);
|
||||
|
||||
// The volume we want to control is the Text-To-Speech volume
|
||||
setVolumeControlStream(TextToSpeech.Engine.DEFAULT_STREAM);
|
||||
|
||||
// We don't need a window title bar:
|
||||
requestWindowFeature(Window.FEATURE_NO_TITLE);
|
||||
|
||||
setContentView(R.layout.stats);
|
||||
|
||||
ScrollView sv = ((ScrollView) findViewById(R.id.scrolly));
|
||||
sv.setScrollBarStyle(ScrollView.SCROLLBARS_OUTSIDE_INSET);
|
||||
|
||||
showUnknownLocation();
|
||||
updateLabels();
|
||||
|
||||
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) {
|
||||
if (utils.isMetricUnits() == metric) {
|
||||
return false;
|
||||
}
|
||||
utils.setMetricUnits(metric);
|
||||
updateLabels();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onReportSpeedChanged(boolean speed) {
|
||||
if (utils.isReportSpeed() == speed) {
|
||||
return false;
|
||||
}
|
||||
utils.setReportSpeed(speed);
|
||||
updateLabels();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void updateLabels() {
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
utils.updateUnits();
|
||||
utils.setSpeedLabel(R.id.speed_label, R.string.stat_speed, R.string.stat_pace);
|
||||
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() * UnitConversions.MS_TO_KMH);
|
||||
}
|
||||
|
||||
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,267 @@
|
||||
/*
|
||||
* 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 android.app.Activity;
|
||||
import android.util.Log;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.text.NumberFormat;
|
||||
|
||||
/**
|
||||
* 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 = NumberFormat.getNumberInstance();
|
||||
private static final NumberFormat ALTITUDE_FORMAT = NumberFormat.getIntegerInstance();
|
||||
private static final NumberFormat SPEED_FORMAT = NumberFormat.getNumberInstance();
|
||||
private static final NumberFormat GRADE_FORMAT = NumberFormat.getPercentInstance();
|
||||
|
||||
static {
|
||||
LAT_LONG_FORMAT.setMaximumFractionDigits(5);
|
||||
LAT_LONG_FORMAT.setMinimumFractionDigits(5);
|
||||
SPEED_FORMAT.setMaximumFractionDigits(2);
|
||||
SPEED_FORMAT.setMinimumFractionDigits(2);
|
||||
GRADE_FORMAT.setMaximumFractionDigits(1);
|
||||
GRADE_FORMAT.setMinimumFractionDigits(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.value_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.unit_meter : R.string.unit_feet);
|
||||
}
|
||||
|
||||
public void setDistanceUnits(int unitLabelId) {
|
||||
TextView unitTextView = (TextView) activity.findViewById(unitLabelId);
|
||||
unitTextView.setText(metricUnits ? R.string.unit_kilometer : R.string.unit_mile);
|
||||
}
|
||||
|
||||
public void setSpeedUnits(int unitLabelId, int unitLabelBottomId) {
|
||||
TextView unitTextView = (TextView) activity.findViewById(unitLabelId);
|
||||
unitTextView.setText(reportSpeed
|
||||
? (metricUnits ? R.string.unit_kilometer : R.string.unit_mile)
|
||||
: R.string.unit_minute);
|
||||
|
||||
unitTextView = (TextView) activity.findViewById(unitLabelBottomId);
|
||||
unitTextView.setText(reportSpeed
|
||||
? R.string.unit_hour
|
||||
: (metricUnits ? R.string.unit_kilometer : R.string.unit_mile));
|
||||
}
|
||||
|
||||
public void setTime(int id, long l) {
|
||||
setText(id, StringUtils.formatElapsedTime(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 * UnitConversions.M_TO_KM);
|
||||
setSpeed(R.id.average_speed_register, averageSpeed * UnitConversions.MS_TO_KMH);
|
||||
setSpeed(R.id.average_moving_speed_register, averageMovingSpeed * UnitConversions.MS_TO_KMH);
|
||||
setSpeed(R.id.max_speed_register, maxSpeed * UnitConversions.MS_TO_KMH);
|
||||
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() * UnitConversions.M_TO_KM);
|
||||
setSpeed(R.id.average_speed_register, stats.getAverageSpeed() * UnitConversions.MS_TO_KMH);
|
||||
setSpeed(R.id.average_moving_speed_register,
|
||||
stats.getAverageMovingSpeed() * UnitConversions.MS_TO_KMH);
|
||||
setSpeed(R.id.max_speed_register, stats.getMaxSpeed() * UnitConversions.MS_TO_KMH);
|
||||
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.stat_average_speed,
|
||||
R.string.stat_average_pace);
|
||||
setSpeedLabel(R.id.average_moving_speed_label,
|
||||
R.string.stat_average_moving_speed,
|
||||
R.string.stat_average_moving_pace);
|
||||
setSpeedLabel(R.id.max_speed_label,
|
||||
R.string.stat_max_speed,
|
||||
R.string.stat_min_pace);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* 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.maps.mytracks.R;
|
||||
|
||||
import android.app.Activity;
|
||||
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 track name, activity type, and track description.
|
||||
*
|
||||
* @author Leif Hendrik Wilden
|
||||
*/
|
||||
public class TrackDetail extends Activity implements OnClickListener {
|
||||
|
||||
public static final String TRACK_ID = "trackId";
|
||||
public static final String SHOW_CANCEL = "showCancel";
|
||||
|
||||
private static final String TAG = TrackDetail.class.getSimpleName();
|
||||
|
||||
private Long trackId;
|
||||
private MyTracksProviderUtils myTracksProviderUtils;
|
||||
private Track track;
|
||||
|
||||
private EditText trackName;
|
||||
private AutoCompleteTextView activityType;
|
||||
private EditText trackDescription;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle bundle) {
|
||||
super.onCreate(bundle);
|
||||
setContentView(R.layout.track_detail);
|
||||
|
||||
trackId = getIntent().getLongExtra(TRACK_ID, -1);
|
||||
if (trackId < 0) {
|
||||
Log.e(TAG, "invalid trackId.");
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
myTracksProviderUtils = MyTracksProviderUtils.Factory.get(this);
|
||||
|
||||
track = myTracksProviderUtils.getTrack(trackId);
|
||||
if (track == null) {
|
||||
Log.e(TAG, "no track.");
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
trackName = (EditText) findViewById(R.id.track_detail_track_name);
|
||||
trackName.setText(track.getName());
|
||||
|
||||
activityType = (AutoCompleteTextView) findViewById(R.id.track_detail_activity_type);
|
||||
activityType.setText(track.getCategory());
|
||||
|
||||
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(
|
||||
this, R.array.activity_types, android.R.layout.simple_dropdown_item_1line);
|
||||
activityType.setAdapter(adapter);
|
||||
trackDescription = (EditText) findViewById(R.id.track_detail_track_description);
|
||||
trackDescription.setText(track.getDescription());
|
||||
|
||||
Button save = (Button) findViewById(R.id.track_detail_save);
|
||||
save.setOnClickListener(this);
|
||||
|
||||
Button cancel = (Button) findViewById(R.id.track_detail_cancel);
|
||||
if (getIntent().getBooleanExtra(SHOW_CANCEL, true)) {
|
||||
cancel.setOnClickListener(this);
|
||||
cancel.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
cancel.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
switch (view.getId()) {
|
||||
case R.id.track_detail_save:
|
||||
save();
|
||||
finish();
|
||||
break;
|
||||
case R.id.track_detail_cancel:
|
||||
finish();
|
||||
break;
|
||||
default:
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
private void save() {
|
||||
track.setName(trackName.getText().toString());
|
||||
track.setCategory(activityType.getText().toString());
|
||||
track.setDescription(trackDescription.getText().toString());
|
||||
myTracksProviderUtils.updateTrack(track);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,372 @@
|
||||
/*
|
||||
* 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.SendRequest;
|
||||
import com.google.android.apps.mytracks.io.sendtogoogle.UploadServiceChooserActivity;
|
||||
import com.google.android.apps.mytracks.services.ServiceUtils;
|
||||
import com.google.android.apps.mytracks.services.TrackRecordingServiceConnection;
|
||||
import com.google.android.apps.mytracks.util.PlayTrackUtils;
|
||||
import com.google.android.apps.mytracks.util.StringUtils;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.app.ListActivity;
|
||||
import android.content.ContentUris;
|
||||
import android.content.Context;
|
||||
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.Menu;
|
||||
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 Google, or writing to SD card.
|
||||
*
|
||||
* @author Leif Hendrik Wilden
|
||||
*/
|
||||
public class TrackList extends ListActivity
|
||||
implements SharedPreferences.OnSharedPreferenceChangeListener,
|
||||
View.OnClickListener {
|
||||
|
||||
private static final int DIALOG_INSTALL_EARTH = 0;
|
||||
|
||||
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.track_list_context_menu_title);
|
||||
AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo;
|
||||
contextPosition = info.position;
|
||||
trackId = TrackList.this.listView.getAdapter().getItemId(contextPosition);
|
||||
menu.add(Menu.NONE, Constants.MENU_SHOW, Menu.NONE, R.string.track_list_show_on_map);
|
||||
menu.add(Menu.NONE, Constants.MENU_EDIT, Menu.NONE, R.string.track_list_edit_track);
|
||||
if (!isRecording() || trackId != recordingTrackId) {
|
||||
String saveFileFormat = getString(R.string.track_list_save_file);
|
||||
String shareFileFormat = getString(R.string.track_list_share_file);
|
||||
String fileTypes[] = getResources().getStringArray(R.array.file_types);
|
||||
|
||||
menu.add(Menu.NONE, Constants.MENU_PLAY, Menu.NONE, R.string.track_list_play);
|
||||
menu.add(Menu.NONE, Constants.MENU_SEND_TO_GOOGLE, Menu.NONE,
|
||||
R.string.track_list_send_google);
|
||||
SubMenu share = menu.addSubMenu(
|
||||
Menu.NONE, Constants.MENU_SHARE, Menu.NONE, R.string.track_list_share_track);
|
||||
share.add(
|
||||
Menu.NONE, Constants.MENU_SHARE_MAP, Menu.NONE, R.string.track_list_share_map);
|
||||
share.add(Menu.NONE, Constants.MENU_SHARE_FUSION_TABLE, Menu.NONE,
|
||||
R.string.track_list_share_fusion_table);
|
||||
share.add(Menu.NONE, Constants.MENU_SHARE_GPX_FILE, Menu.NONE,
|
||||
String.format(shareFileFormat, fileTypes[0]));
|
||||
share.add(Menu.NONE, Constants.MENU_SHARE_KML_FILE, Menu.NONE,
|
||||
String.format(shareFileFormat, fileTypes[1]));
|
||||
share.add(Menu.NONE, Constants.MENU_SHARE_CSV_FILE, Menu.NONE,
|
||||
String.format(shareFileFormat, fileTypes[2]));
|
||||
share.add(Menu.NONE, Constants.MENU_SHARE_TCX_FILE, Menu.NONE,
|
||||
String.format(shareFileFormat, fileTypes[3]));
|
||||
SubMenu save = menu.addSubMenu(
|
||||
Menu.NONE, Constants.MENU_WRITE_TO_SD_CARD, Menu.NONE, R.string.track_list_save_sd);
|
||||
save.add(Menu.NONE, Constants.MENU_SAVE_GPX_FILE, Menu.NONE,
|
||||
String.format(saveFileFormat, fileTypes[0]));
|
||||
save.add(Menu.NONE, Constants.MENU_SAVE_KML_FILE, Menu.NONE,
|
||||
String.format(saveFileFormat, fileTypes[1]));
|
||||
save.add(Menu.NONE, Constants.MENU_SAVE_CSV_FILE, Menu.NONE,
|
||||
String.format(saveFileFormat, fileTypes[2]));
|
||||
save.add(Menu.NONE, Constants.MENU_SAVE_TCX_FILE, Menu.NONE,
|
||||
String.format(saveFileFormat, fileTypes[3]));
|
||||
menu.add(Menu.NONE, Constants.MENU_DELETE, Menu.NONE, R.string.track_list_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) {
|
||||
Intent intent;
|
||||
switch (item.getItemId()) {
|
||||
case Constants.MENU_SHOW: {
|
||||
onListItemClick(null, null, 0, trackId);
|
||||
return true;
|
||||
}
|
||||
case Constants.MENU_EDIT: {
|
||||
intent = new Intent(this, TrackDetail.class).putExtra(TrackDetail.TRACK_ID, trackId);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
}
|
||||
case Constants.MENU_PLAY:
|
||||
if (PlayTrackUtils.isEarthInstalled(this)) {
|
||||
PlayTrackUtils.playTrack(this, trackId);
|
||||
return true;
|
||||
} else {
|
||||
showDialog(DIALOG_INSTALL_EARTH);
|
||||
return true;
|
||||
}
|
||||
case Constants.MENU_SEND_TO_GOOGLE:
|
||||
intent = new Intent(this, UploadServiceChooserActivity.class)
|
||||
.putExtra(SendRequest.SEND_REQUEST_KEY, new SendRequest(trackId, true, true, true));
|
||||
startActivity(intent);
|
||||
return true;
|
||||
case Constants.MENU_SHARE_MAP:
|
||||
intent = new Intent(this, UploadServiceChooserActivity.class)
|
||||
.putExtra(SendRequest.SEND_REQUEST_KEY, new SendRequest(trackId, true, false, false));
|
||||
startActivity(intent);
|
||||
return true;
|
||||
case Constants.MENU_SHARE_FUSION_TABLE:
|
||||
intent = new Intent(this, UploadServiceChooserActivity.class)
|
||||
.putExtra(SendRequest.SEND_REQUEST_KEY, new SendRequest(trackId, false, true, false));
|
||||
startActivity(intent);
|
||||
return true;
|
||||
case Constants.MENU_SHARE_GPX_FILE:
|
||||
case Constants.MENU_SHARE_KML_FILE:
|
||||
case Constants.MENU_SHARE_CSV_FILE:
|
||||
case Constants.MENU_SHARE_TCX_FILE:
|
||||
case Constants.MENU_SAVE_GPX_FILE:
|
||||
case Constants.MENU_SAVE_KML_FILE:
|
||||
case Constants.MENU_SAVE_CSV_FILE:
|
||||
case Constants.MENU_SAVE_TCX_FILE:
|
||||
SaveActivity.handleExportTrackAction(
|
||||
this, trackId, Constants.getActionFromMenuId(item.getItemId()));
|
||||
return true;
|
||||
case Constants.MENU_DELETE:
|
||||
Uri uri = ContentUris.withAppendedId(TracksColumns.CONTENT_URI, trackId);
|
||||
intent = new Intent(Intent.ACTION_DELETE)
|
||||
.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, Context.MODE_PRIVATE);
|
||||
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();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
getMenuInflater().inflate(R.menu.search_only, menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Dialog onCreateDialog(int id) {
|
||||
switch (id) {
|
||||
case DIALOG_INSTALL_EARTH:
|
||||
return PlayTrackUtils.createInstallEarthDialog(this);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/* Callback from menu/search_only.xml */
|
||||
public void onSearch(@SuppressWarnings("unused") MenuItem i) {
|
||||
onSearchRequested();
|
||||
}
|
||||
|
||||
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.track_list_item_name, R.id.track_list_item_time,
|
||||
R.id.track_list_item_stats, R.id.track_list_item_description,
|
||||
R.id.track_list_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(StringUtils.formatDateTime(TrackList.this, time));
|
||||
} else if (columnIndex == totalDistanceIdx) {
|
||||
double totalDistance = cursor.getDouble(totalDistanceIdx);
|
||||
long totalTime = cursor.getLong(totalTimeIdx);
|
||||
String totalDistanceStr = StringUtils.formatTimeDistance(TrackList.this, totalDistance, totalTime, metricUnits);
|
||||
textView.setText(totalDistanceStr);
|
||||
} 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,178 @@
|
||||
/*
|
||||
* 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.Context;
|
||||
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 with the above name).
|
||||
*/
|
||||
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, Context.MODE_PRIVATE);
|
||||
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,336 @@
|
||||
/*
|
||||
* 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.DescriptionGeneratorImpl;
|
||||
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.Context;
|
||||
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.Menu;
|
||||
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.marker_list_context_menu_title);
|
||||
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(Menu.NONE, Constants.MENU_SHOW, Menu.NONE, R.string.marker_list_show_on_map);
|
||||
menu.add(Menu.NONE, Constants.MENU_EDIT, Menu.NONE, R.string.marker_list_edit_marker);
|
||||
MenuItem deleteMenu = menu.add(
|
||||
Menu.NONE, Constants.MENU_DELETE, Menu.NONE, R.string.marker_list_delete_marker);
|
||||
deleteMenu.setEnabled(recordingTrackId < 0
|
||||
|| type == Waypoint.TYPE_WAYPOINT
|
||||
|| type == Waypoint.TYPE_STATISTICS
|
||||
|| 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, Context.MODE_PRIVATE);
|
||||
|
||||
// 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 boolean onCreateOptionsMenu(Menu menu) {
|
||||
getMenuInflater().inflate(R.menu.search_only, menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Callback from menu/search_only.xml */
|
||||
public void onSearch(@SuppressWarnings("unused") MenuItem i) {
|
||||
onSearchRequested();
|
||||
}
|
||||
|
||||
@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.marker_insert_error, 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.marker_insert_success, 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;
|
||||
|
||||
if (time == 0) {
|
||||
textView.setVisibility(View.GONE);
|
||||
} else {
|
||||
textView.setText(StringUtils.formatDateTime(WaypointsList.this, time));
|
||||
textView.setVisibility(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_list_delete_marker_confirm_message));
|
||||
builder.setTitle(getString(R.string.generic_confirm_title));
|
||||
builder.setIcon(android.R.drawable.ic_dialog_alert);
|
||||
builder.setPositiveButton(getString(R.string.generic_yes),
|
||||
new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialogInterface, int i) {
|
||||
dialogInterface.dismiss();
|
||||
providerUtils.deleteWaypoint(waypointId,
|
||||
new DescriptionGeneratorImpl(WaypointsList.this));
|
||||
}
|
||||
});
|
||||
builder.setNegativeButton(getString(R.string.generic_no),
|
||||
new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialogInterface, int i) {
|
||||
dialogInterface.dismiss();
|
||||
}
|
||||
});
|
||||
dialog = builder.create();
|
||||
dialog.show();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* 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.EulaUtils;
|
||||
import com.google.android.apps.mytracks.util.SystemUtils;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.app.Dialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.widget.TextView;
|
||||
|
||||
/**
|
||||
* An activity that displays a welcome screen.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class WelcomeActivity extends Activity {
|
||||
|
||||
private static final int DIALOG_ABOUT_ID = 0;
|
||||
private static final int DIALOG_EULA_ID = 1;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.welcome);
|
||||
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) {
|
||||
showDialog(DIALOG_ABOUT_ID);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Dialog onCreateDialog(int id) {
|
||||
AlertDialog.Builder builder;
|
||||
switch (id) {
|
||||
case DIALOG_ABOUT_ID:
|
||||
LayoutInflater layoutInflator = LayoutInflater.from(this);
|
||||
View view = layoutInflator.inflate(R.layout.about, null);
|
||||
TextView aboutVersionTextView = (TextView) view.findViewById(R.id.about_version);
|
||||
aboutVersionTextView.setText(SystemUtils.getMyTracksVersion(this));
|
||||
|
||||
builder = new AlertDialog.Builder(this);
|
||||
builder.setView(view);
|
||||
builder.setPositiveButton(R.string.generic_ok, null);
|
||||
builder.setNegativeButton(R.string.about_license, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
showDialog(DIALOG_EULA_ID);
|
||||
}
|
||||
});
|
||||
return builder.create();
|
||||
case DIALOG_EULA_ID:
|
||||
builder = new AlertDialog.Builder(this);
|
||||
builder.setTitle(R.string.eula_title);
|
||||
builder.setMessage(EulaUtils.getEulaMessage(this));
|
||||
builder.setPositiveButton(R.string.generic_ok, null);
|
||||
builder.setCancelable(true);
|
||||
return builder.create();
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.my_location_approximate_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.my_location_no_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,257 @@
|
||||
/*
|
||||
* Copyright 2012 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
|
||||
package com.google.android.apps.mytracks.content;
|
||||
|
||||
import com.google.android.apps.mytracks.stats.TripStatistics;
|
||||
import com.google.android.apps.mytracks.util.ChartURLGenerator;
|
||||
import com.google.android.apps.mytracks.util.StringUtils;
|
||||
import com.google.android.apps.mytracks.util.UnitConversions;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Pair;
|
||||
|
||||
import java.util.Vector;
|
||||
|
||||
/**
|
||||
* An implementation of {@link DescriptionGenerator} for My Tracks.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class DescriptionGeneratorImpl implements DescriptionGenerator {
|
||||
|
||||
private static final String HTML_LINE_BREAK = "<br>";
|
||||
private static final String TEXT_LINE_BREAK = "\n";
|
||||
|
||||
private Context context;
|
||||
|
||||
public DescriptionGeneratorImpl(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String generateTrackDescription(
|
||||
Track track, Vector<Double> distances, Vector<Double> elevations) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
|
||||
// Created by
|
||||
String url = context.getString(R.string.my_tracks_web_url);
|
||||
builder.append(context.getString(
|
||||
R.string.send_google_by_my_tracks, "<a href='http://" + url + "'>", "</a>"));
|
||||
builder.append("<p>");
|
||||
|
||||
builder.append(generateTripStatisticsDescription(track.getStatistics(), true));
|
||||
|
||||
// Activity type
|
||||
String trackCategory = track.getCategory();
|
||||
String category = trackCategory != null && trackCategory.length() > 0 ? trackCategory
|
||||
: context.getString(R.string.value_unknown);
|
||||
builder.append(context.getString(R.string.description_activity_type, category));
|
||||
builder.append(HTML_LINE_BREAK);
|
||||
|
||||
// Elevation chart
|
||||
if (distances != null && elevations != null) {
|
||||
builder.append("<img border=\"0\" src=\""
|
||||
+ ChartURLGenerator.getChartUrl(distances, elevations, track, context) + "\"/>");
|
||||
builder.append(HTML_LINE_BREAK);
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String generateWaypointDescription(Waypoint waypoint) {
|
||||
return generateTripStatisticsDescription(waypoint.getStatistics(), false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a description for a {@link TripStatistics}.
|
||||
*
|
||||
* @param stats the trip statistics
|
||||
* @param html true to use "<br>" for line break instead of "\n"
|
||||
*/
|
||||
private String generateTripStatisticsDescription(TripStatistics stats, boolean html) {
|
||||
String lineBreak = html ? HTML_LINE_BREAK : TEXT_LINE_BREAK;
|
||||
StringBuilder builder = new StringBuilder();
|
||||
|
||||
// Total distance
|
||||
writeDistance(
|
||||
stats.getTotalDistance(), builder, R.string.description_total_distance, lineBreak);
|
||||
|
||||
// Total time
|
||||
writeTime(stats.getTotalTime(), builder, R.string.description_total_time, lineBreak);
|
||||
|
||||
// Moving time
|
||||
writeTime(stats.getMovingTime(), builder, R.string.description_moving_time, lineBreak);
|
||||
|
||||
// Average speed
|
||||
Pair<Double, Double> averageSpeed = writeSpeed(
|
||||
stats.getAverageSpeed(), builder, R.string.description_average_speed, lineBreak);
|
||||
|
||||
// Average moving speed
|
||||
Pair<Double, Double> averageMovingSpeed = writeSpeed(stats.getAverageMovingSpeed(), builder,
|
||||
R.string.description_average_moving_speed, lineBreak);
|
||||
|
||||
// Max speed
|
||||
Pair<Double, Double> maxSpeed = writeSpeed(
|
||||
stats.getMaxSpeed(), builder, R.string.description_max_speed, lineBreak);
|
||||
|
||||
// Average pace
|
||||
writePace(averageSpeed, builder, R.string.description_average_pace, lineBreak);
|
||||
|
||||
// Average moving pace
|
||||
writePace(averageMovingSpeed, builder, R.string.description_average_moving_pace, lineBreak);
|
||||
|
||||
// Min pace
|
||||
writePace(maxSpeed, builder, R.string.description_min_pace, lineBreak);
|
||||
|
||||
// Max elevation
|
||||
writeElevation(stats.getMaxElevation(), builder, R.string.description_max_elevation, lineBreak);
|
||||
|
||||
// Min elevation
|
||||
writeElevation(stats.getMinElevation(), builder, R.string.description_min_elevation, lineBreak);
|
||||
|
||||
// Elevation gain
|
||||
writeElevation(
|
||||
stats.getTotalElevationGain(), builder, R.string.description_elevation_gain, lineBreak);
|
||||
|
||||
// Max grade
|
||||
writeGrade(stats.getMaxGrade(), builder, R.string.description_max_grade, lineBreak);
|
||||
|
||||
// Min grade
|
||||
writeGrade(stats.getMinGrade(), builder, R.string.description_min_grade, lineBreak);
|
||||
|
||||
// Recorded time
|
||||
builder.append(
|
||||
context.getString(R.string.description_recorded_time,
|
||||
StringUtils.formatDateTime(context, stats.getStartTime())));
|
||||
builder.append(lineBreak);
|
||||
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes distance.
|
||||
*
|
||||
* @param distance distance in meters
|
||||
* @param builder StringBuilder to append distance
|
||||
* @param resId resource id of distance string
|
||||
* @param lineBreak line break string
|
||||
*/
|
||||
@VisibleForTesting
|
||||
void writeDistance(double distance, StringBuilder builder, int resId, String lineBreak) {
|
||||
double distanceInKm = distance * UnitConversions.M_TO_KM;
|
||||
double distanceInMi = distanceInKm * UnitConversions.KM_TO_MI;
|
||||
builder.append(context.getString(resId, distanceInKm, distanceInMi));
|
||||
builder.append(lineBreak);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes time.
|
||||
*
|
||||
* @param time time in milliseconds.
|
||||
* @param builder StringBuilder to append time
|
||||
* @param resId resource id of time string
|
||||
* @param lineBreak line break string
|
||||
*/
|
||||
@VisibleForTesting
|
||||
void writeTime(long time, StringBuilder builder, int resId, String lineBreak) {
|
||||
builder.append(context.getString(resId, StringUtils.formatElapsedTime(time)));
|
||||
builder.append(lineBreak);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes speed.
|
||||
*
|
||||
* @param speed speed in meters per second
|
||||
* @param builder StringBuilder to append speed
|
||||
* @param resId resource id of speed string
|
||||
* @param lineBreak line break string
|
||||
* @return a pair of speed, first in kilometers per hour, second in miles per
|
||||
* hour.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
Pair<Double, Double> writeSpeed(
|
||||
double speed, StringBuilder builder, int resId, String lineBreak) {
|
||||
double speedInKmHr = speed * UnitConversions.MS_TO_KMH;
|
||||
double speedInMiHr = speedInKmHr * UnitConversions.KM_TO_MI;
|
||||
builder.append(context.getString(resId, speedInKmHr, speedInMiHr));
|
||||
builder.append(lineBreak);
|
||||
return Pair.create(speedInKmHr, speedInMiHr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes pace.
|
||||
*
|
||||
* @param speed a pair of speed, first in kilometers per hour, second in miles
|
||||
* per hour
|
||||
* @param builder StringBuilder to append pace
|
||||
* @param resId resource id of pace string
|
||||
* @param lineBreak line break string
|
||||
*/
|
||||
@VisibleForTesting
|
||||
void writePace(
|
||||
Pair<Double, Double> speed, StringBuilder builder, int resId, String lineBreak) {
|
||||
double paceInMinKm = getPace(speed.first);
|
||||
double paceInMinMi = getPace(speed.second);
|
||||
builder.append(context.getString(resId, paceInMinKm, paceInMinMi));
|
||||
builder.append(lineBreak);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes elevation.
|
||||
*
|
||||
* @param elevation elevation in meters
|
||||
* @param builder StringBuilder to append elevation
|
||||
* @param resId resource id of elevation string
|
||||
* @param lineBreak line break string
|
||||
*/
|
||||
@VisibleForTesting
|
||||
void writeElevation(
|
||||
double elevation, StringBuilder builder, int resId, String lineBreak) {
|
||||
long elevationInM = Math.round(elevation);
|
||||
long elevationInFt = Math.round(elevation * UnitConversions.M_TO_FT);
|
||||
builder.append(context.getString(resId, elevationInM, elevationInFt));
|
||||
builder.append(lineBreak);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes grade.
|
||||
*
|
||||
* @param grade grade in fraction
|
||||
* @param builder StringBuilder to append grade
|
||||
* @param resId resource id grade string
|
||||
* @param lineBreak line break string
|
||||
*/
|
||||
@VisibleForTesting
|
||||
void writeGrade(double grade, StringBuilder builder, int resId, String lineBreak) {
|
||||
long gradeInPercent = Double.isNaN(grade) || Double.isInfinite(grade) ? 0L
|
||||
: Math.round(grade * 100);
|
||||
builder.append(context.getString(resId, gradeInPercent));
|
||||
builder.append(lineBreak);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets pace (in minutes) from speed.
|
||||
*
|
||||
* @param speed speed in hours
|
||||
*/
|
||||
@VisibleForTesting
|
||||
double getPace(double speed) {
|
||||
return speed == 0 ? 0.0 : 60.0 / speed; // convert from hours to minutes
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,470 @@
|
||||
/*
|
||||
* 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.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, Context.MODE_PRIVATE);
|
||||
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);
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,405 @@
|
||||
/*
|
||||
* Copyright 2012 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package com.google.android.apps.mytracks.content;
|
||||
|
||||
import com.google.android.apps.mytracks.stats.TripStatistics;
|
||||
import com.google.android.apps.mytracks.util.LocationUtils;
|
||||
import com.google.android.apps.mytracks.util.UnitConversions;
|
||||
|
||||
import android.database.Cursor;
|
||||
import android.location.Location;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.SortedSet;
|
||||
import java.util.TreeSet;
|
||||
|
||||
/**
|
||||
* Engine for searching for tracks and waypoints by text.
|
||||
*
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
public class SearchEngine {
|
||||
|
||||
/** WHERE query to get tracks by name. */
|
||||
private static final String TRACK_SELECTION_QUERY =
|
||||
TracksColumns.NAME + " LIKE ? OR " +
|
||||
TracksColumns.DESCRIPTION + " LIKE ? OR " +
|
||||
TracksColumns.CATEGORY + " LIKE ?";
|
||||
|
||||
/** WHERE query to get waypoints by name. */
|
||||
private static final String WAYPOINT_SELECTION_QUERY =
|
||||
WaypointsColumns.NAME + " LIKE ? OR " +
|
||||
WaypointsColumns.DESCRIPTION + " LIKE ? OR " +
|
||||
WaypointsColumns.CATEGORY + " LIKE ?";
|
||||
|
||||
/** Order of track results. */
|
||||
private static final String TRACK_SELECTION_ORDER = TracksColumns._ID + " DESC LIMIT 1000";
|
||||
|
||||
/** Order of waypoint results. */
|
||||
private static final String WAYPOINT_SELECTION_ORDER = WaypointsColumns._ID + " DESC";
|
||||
|
||||
/** How much we promote a match in the track category. */
|
||||
private static final double TRACK_CATEGORY_PROMOTION = 2.0;
|
||||
|
||||
/** How much we promote a match in the track description. */
|
||||
private static final double TRACK_DESCRIPTION_PROMOTION = 8.0;
|
||||
|
||||
/** How much we promote a match in the track name. */
|
||||
private static final double TRACK_NAME_PROMOTION = 16.0;
|
||||
|
||||
/** How much we promote a waypoint result if it's in the currently-selected track. */
|
||||
private static final double CURRENT_TRACK_WAYPOINT_PROMOTION = 2.0;
|
||||
|
||||
/** How much we promote a track result if it's the currently-selected track. */
|
||||
private static final double CURRENT_TRACK_DEMOTION = 0.5;
|
||||
|
||||
/** Maximum number of waypoints which will be retrieved and scored. */
|
||||
private static final int MAX_SCORED_WAYPOINTS = 100;
|
||||
|
||||
/** Oldest timestamp for which we rank based on time (2000-01-01 00:00:00.000) */
|
||||
private static final long OLDEST_ALLOWED_TIMESTAMP = 946692000000L;
|
||||
|
||||
/**
|
||||
* Description of a search query, along with all contextual data needed to execute it.
|
||||
*/
|
||||
public static class SearchQuery {
|
||||
public SearchQuery(String textQuery, Location currentLocation, long currentTrackId,
|
||||
long currentTimestamp) {
|
||||
this.textQuery = textQuery.toLowerCase();
|
||||
this.currentLocation = currentLocation;
|
||||
this.currentTrackId = currentTrackId;
|
||||
this.currentTimestamp = currentTimestamp;
|
||||
}
|
||||
|
||||
public final String textQuery;
|
||||
public final Location currentLocation;
|
||||
public final long currentTrackId;
|
||||
public final long currentTimestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Description of a search result which has been retrieved and scored.
|
||||
*/
|
||||
public static class ScoredResult {
|
||||
ScoredResult(Track track, double score) {
|
||||
this.track = track;
|
||||
this.waypoint = null;
|
||||
this.score = score;
|
||||
}
|
||||
|
||||
ScoredResult(Waypoint waypoint, double score) {
|
||||
this.track = null;
|
||||
this.waypoint = waypoint;
|
||||
this.score = score;
|
||||
}
|
||||
|
||||
public final Track track;
|
||||
public final Waypoint waypoint;
|
||||
public final double score;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ScoredResult ["
|
||||
+ (track != null ? ("trackId=" + track.getId() + ", ") : "")
|
||||
+ (waypoint != null ? ("wptId=" + waypoint.getId() + ", ") : "")
|
||||
+ "score=" + score + "]";
|
||||
}
|
||||
}
|
||||
|
||||
/** Comparador for scored results. */
|
||||
private static final Comparator<ScoredResult> SCORED_RESULT_COMPARATOR =
|
||||
new Comparator<ScoredResult>() {
|
||||
@Override
|
||||
public int compare(ScoredResult r1, ScoredResult r2) {
|
||||
// Score ordering.
|
||||
int scoreDiff = Double.compare(r2.score, r1.score);
|
||||
if (scoreDiff != 0) {
|
||||
return scoreDiff;
|
||||
}
|
||||
|
||||
// Make tracks come before waypoints.
|
||||
if (r1.waypoint != null && r2.track != null) {
|
||||
return 1;
|
||||
} else if (r1.track != null && r2.waypoint != null) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Finally, use arbitrary ordering, by ID.
|
||||
long id1 = r1.track != null ? r1.track.getId() : r1.waypoint.getId();
|
||||
long id2 = r2.track != null ? r2.track.getId() : r2.waypoint.getId();
|
||||
long idDiff = id2 - id1;
|
||||
return Long.signum(idDiff);
|
||||
}
|
||||
};
|
||||
|
||||
private final MyTracksProviderUtils providerUtils;
|
||||
|
||||
public SearchEngine(MyTracksProviderUtils providerUtils) {
|
||||
this.providerUtils = providerUtils;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a search query and returns a set of sorted results.
|
||||
*
|
||||
* @param query the query to execute
|
||||
* @return a set of results, sorted according to their score
|
||||
*/
|
||||
public SortedSet<ScoredResult> search(SearchQuery query) {
|
||||
ArrayList<Track> tracks = new ArrayList<Track>();
|
||||
ArrayList<Waypoint> waypoints = new ArrayList<Waypoint>();
|
||||
TreeSet<ScoredResult> scoredResults = new TreeSet<ScoredResult>(SCORED_RESULT_COMPARATOR);
|
||||
|
||||
retrieveTracks(query, tracks);
|
||||
retrieveWaypoints(query, waypoints);
|
||||
|
||||
scoreTrackResults(tracks, query, scoredResults);
|
||||
scoreWaypointResults(waypoints, query, scoredResults);
|
||||
|
||||
return scoredResults;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves tracks matching the given query from the database.
|
||||
*
|
||||
* @param query the query to retrieve for
|
||||
* @param tracks list to fill with the resulting tracks
|
||||
*/
|
||||
private void retrieveTracks(SearchQuery query, ArrayList<Track> tracks) {
|
||||
String queryLikeSelection = "%" + query.textQuery + "%";
|
||||
String[] trackSelectionArgs = new String[] {
|
||||
queryLikeSelection,
|
||||
queryLikeSelection,
|
||||
queryLikeSelection };
|
||||
|
||||
Cursor tracksCursor = providerUtils.getTracksCursor(
|
||||
TRACK_SELECTION_QUERY, trackSelectionArgs, TRACK_SELECTION_ORDER);
|
||||
if (tracksCursor != null) {
|
||||
try {
|
||||
tracks.ensureCapacity(tracksCursor.getCount());
|
||||
|
||||
while (tracksCursor.moveToNext()) {
|
||||
tracks.add(providerUtils.createTrack(tracksCursor));
|
||||
}
|
||||
} finally {
|
||||
tracksCursor.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves waypoints matching the given query from the database.
|
||||
*
|
||||
* @param query the query to retrieve for
|
||||
* @param waypoints list to fill with the resulting waypoints
|
||||
*/
|
||||
private void retrieveWaypoints(SearchQuery query, ArrayList<Waypoint> waypoints) {
|
||||
String queryLikeSelection2 = "%" + query.textQuery + "%";
|
||||
String[] waypointSelectionArgs = new String[] {
|
||||
queryLikeSelection2,
|
||||
queryLikeSelection2,
|
||||
queryLikeSelection2 };
|
||||
Cursor waypointsCursor = providerUtils.getWaypointsCursor(
|
||||
WAYPOINT_SELECTION_QUERY, waypointSelectionArgs, WAYPOINT_SELECTION_ORDER,
|
||||
MAX_SCORED_WAYPOINTS);
|
||||
if (waypointsCursor != null) {
|
||||
try {
|
||||
waypoints.ensureCapacity(waypointsCursor.getCount());
|
||||
|
||||
while (waypointsCursor.moveToNext()) {
|
||||
Waypoint waypoint = providerUtils.createWaypoint(waypointsCursor);
|
||||
if (LocationUtils.isValidLocation(waypoint.getLocation())) {
|
||||
waypoints.add(waypoint);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
waypointsCursor.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scores a collection of track results.
|
||||
*
|
||||
* @param tracks the results to score
|
||||
* @param query the query to score for
|
||||
* @param output the collection to fill with scored results
|
||||
*/
|
||||
private void scoreTrackResults(Collection<Track> tracks, SearchQuery query, Collection<ScoredResult> output) {
|
||||
for (Track track : tracks) {
|
||||
// Calculate the score.
|
||||
double score = scoreTrackResult(query, track);
|
||||
|
||||
// Add to the output.
|
||||
output.add(new ScoredResult(track, score));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scores a single track result.
|
||||
*
|
||||
* @param query the query to score for
|
||||
* @param track the results to score
|
||||
* @return the score for the track
|
||||
*/
|
||||
private double scoreTrackResult(SearchQuery query, Track track) {
|
||||
double score = 1.0;
|
||||
|
||||
score *= getTitleBoost(query, track.getName(), track.getDescription(), track.getCategory());
|
||||
|
||||
TripStatistics statistics = track.getStatistics();
|
||||
// TODO: Also boost for proximity to the currently-centered position on the map.
|
||||
score *= getDistanceBoost(query, statistics.getMeanLatitude(), statistics.getMeanLongitude());
|
||||
|
||||
long meanTimestamp = (statistics.getStartTime() + statistics.getStopTime()) / 2L;
|
||||
score *= getTimeBoost(query, meanTimestamp);
|
||||
|
||||
// Score the currently-selected track lower (user is already there, wouldn't be searching for it).
|
||||
if (track.getId() == query.currentTrackId) {
|
||||
score *= CURRENT_TRACK_DEMOTION;
|
||||
}
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scores a collection of waypoint results.
|
||||
*
|
||||
* @param waypoints the results to score
|
||||
* @param query the query to score for
|
||||
* @param output the collection to fill with scored results
|
||||
*/
|
||||
private void scoreWaypointResults(Collection<Waypoint> waypoints, SearchQuery query, Collection<ScoredResult> output) {
|
||||
for (Waypoint waypoint : waypoints) {
|
||||
// Calculate the score.
|
||||
double score = scoreWaypointResult(query, waypoint);
|
||||
|
||||
// Add to the output.
|
||||
output.add(new ScoredResult(waypoint, score));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scores a single waypoint result.
|
||||
*
|
||||
* @param query the query to score for
|
||||
* @param waypoint the results to score
|
||||
* @return the score for the waypoint
|
||||
*/
|
||||
private double scoreWaypointResult(SearchQuery query, Waypoint waypoint) {
|
||||
double score = 1.0;
|
||||
|
||||
Location location = waypoint.getLocation();
|
||||
score *= getTitleBoost(query, waypoint.getName(), waypoint.getDescription(), waypoint.getCategory());
|
||||
// TODO: Also boost for proximity to the currently-centered position on the map.
|
||||
score *= getDistanceBoost(query, location.getLatitude(), location.getLongitude());
|
||||
score *= getTimeBoost(query, location.getTime());
|
||||
|
||||
// Score waypoints in the currently-selected track higher (searching inside the current track).
|
||||
if (query.currentTrackId != -1 && waypoint.getTrackId() == query.currentTrackId) {
|
||||
score *= CURRENT_TRACK_WAYPOINT_PROMOTION;
|
||||
}
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the boosting of the score due to the field(s) in which the match occured.
|
||||
*
|
||||
* @param query the query to boost for
|
||||
* @param name the name of the track or waypoint
|
||||
* @param description the description of the track or waypoint
|
||||
* @param category the category of the track or waypoint
|
||||
* @return the total boost to be applied to the result
|
||||
*/
|
||||
private double getTitleBoost(SearchQuery query,
|
||||
String name, String description, String category) {
|
||||
// Title boost: track name > description > category.
|
||||
double boost = 1.0;
|
||||
if (name.toLowerCase().contains(query.textQuery)) {
|
||||
boost *= TRACK_NAME_PROMOTION;
|
||||
}
|
||||
if (description.toLowerCase().contains(query.textQuery)) {
|
||||
boost *= TRACK_DESCRIPTION_PROMOTION;
|
||||
}
|
||||
if (category.toLowerCase().contains(query.textQuery)) {
|
||||
boost *= TRACK_CATEGORY_PROMOTION;
|
||||
}
|
||||
return boost;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the boosting of the score due to the recency of the matched entity.
|
||||
*
|
||||
* @param query the query to boost for
|
||||
* @param timestamp the timestamp to calculate the boost for
|
||||
* @return the total boost to be applied to the result
|
||||
*/
|
||||
private double getTimeBoost(SearchQuery query, long timestamp) {
|
||||
if (timestamp < OLDEST_ALLOWED_TIMESTAMP) {
|
||||
// Safety: if timestamp is too old or invalid, don't rank based on time.
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
// Score recent tracks higher.
|
||||
long timeAgoHours = (query.currentTimestamp - timestamp) / (60L * 60L * 1000L);
|
||||
if (timeAgoHours > 0L) {
|
||||
return squash(timeAgoHours);
|
||||
} else {
|
||||
// Should rarely happen (track recorded in the last hour).
|
||||
return Double.POSITIVE_INFINITY;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the boosting of the score due to proximity to a location.
|
||||
*
|
||||
* @param query the query to boost for
|
||||
* @param latitude the latitude to calculate the boost for
|
||||
* @param longitude the longitude to calculate the boost for
|
||||
* @return the total boost to be applied to the result
|
||||
*/
|
||||
private double getDistanceBoost(SearchQuery query, double latitude, double longitude) {
|
||||
if (query.currentLocation == null) {
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
float[] distanceResults = new float[1];
|
||||
|
||||
Location.distanceBetween(
|
||||
latitude, longitude,
|
||||
query.currentLocation.getLatitude(), query.currentLocation.getLongitude(),
|
||||
distanceResults);
|
||||
|
||||
// Score tracks close to the current location higher.
|
||||
double distanceKm = distanceResults[0] * UnitConversions.M_TO_KM;
|
||||
if (distanceKm > 0.0) {
|
||||
// Use the inverse of the amortized distance.
|
||||
return squash(distanceKm);
|
||||
} else {
|
||||
// Should rarely happen (distance is exactly 0).
|
||||
return Double.POSITIVE_INFINITY;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Squashes a number by calculating 1 / log (1 + x).
|
||||
*/
|
||||
private static double squash(double x) {
|
||||
return 1.0 / Math.log1p(x);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright 2012 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package com.google.android.apps.mytracks.content;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SearchRecentSuggestionsProvider;
|
||||
import android.provider.SearchRecentSuggestions;
|
||||
|
||||
/**
|
||||
* Content provider for search suggestions.
|
||||
*
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
public class SearchEngineProvider extends SearchRecentSuggestionsProvider {
|
||||
|
||||
private static final String AUTHORITY = "com.google.android.maps.mytracks.search";
|
||||
private static final int MODE = DATABASE_MODE_QUERIES;
|
||||
|
||||
public SearchEngineProvider() {
|
||||
setupSuggestions(AUTHORITY, MODE);
|
||||
}
|
||||
|
||||
// TODO: Also add suggestions from the database.
|
||||
|
||||
/**
|
||||
* Creates and returns a helper for adding recent queries or clearing the recent query history.
|
||||
*/
|
||||
public static SearchRecentSuggestions newHelper(Context context) {
|
||||
return new SearchRecentSuggestions(context, AUTHORITY, MODE);
|
||||
}
|
||||
}
|
||||
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 TrackDataHub#start}, and in that case
|
||||
* the data will only be passed to listeners when {@link TrackDataHub#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,240 @@
|
||||
/*
|
||||
* 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 com.google.android.apps.mytracks.MyTracks;
|
||||
import com.google.android.apps.mytracks.util.FileUtils;
|
||||
import com.google.android.apps.mytracks.util.StringUtils;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.app.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.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 {
|
||||
|
||||
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.sd_card_error_no_storage);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!backup.isBackupsDirectoryAvailable(true)) {
|
||||
showToast(R.string.sd_card_error_create_dir);
|
||||
return;
|
||||
}
|
||||
|
||||
final ProgressDialog progressDialog = ProgressDialog.show(
|
||||
activity,
|
||||
activity.getString(R.string.generic_progress_title),
|
||||
activity.getString(R.string.settings_backup_now_progress_message),
|
||||
true);
|
||||
|
||||
// Do the writing in another thread
|
||||
new Thread() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
backup.writeToDefaultFile();
|
||||
showToast(R.string.sd_card_success_write_file);
|
||||
} catch (IOException e) {
|
||||
Log.e(Constants.TAG, "Failed to write backup", e);
|
||||
showToast(R.string.sd_card_error_write_file);
|
||||
} 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.sd_card_error_no_storage);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!backup.isBackupsDirectoryAvailable(false)) {
|
||||
showToast(R.string.settings_backup_restore_no_backup);
|
||||
return;
|
||||
}
|
||||
|
||||
final Date[] backupDates = backup.getAvailableBackups();
|
||||
if (backupDates == null || backupDates.length == 0) {
|
||||
showToast(R.string.settings_backup_restore_no_backup);
|
||||
return;
|
||||
}
|
||||
Arrays.sort(backupDates, REVERSE_DATE_ORDER);
|
||||
|
||||
// Show a confirmation dialog
|
||||
Builder confirmationDialogBuilder = new AlertDialog.Builder(activity);
|
||||
confirmationDialogBuilder.setMessage(R.string.settings_backup_restore_confirm_message);
|
||||
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 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] = StringUtils.formatDateTime(activity, backupDates[i].getTime());
|
||||
}
|
||||
|
||||
// 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.settings_backup_restore_select_title);
|
||||
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 asynchronously.
|
||||
*
|
||||
* @param date the date
|
||||
*/
|
||||
private void restoreFromDateAsync(final Date date) {
|
||||
// Show a progress dialog
|
||||
ProgressDialog.show(
|
||||
activity,
|
||||
activity.getString(R.string.generic_progress_title),
|
||||
activity.getString(R.string.settings_backup_restore_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.sd_card_success_read_file);
|
||||
} catch (IOException e) {
|
||||
Log.e(Constants.TAG, "Failed to restore backup", e);
|
||||
showToast(R.string.sd_card_error_read_file);
|
||||
} 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 };
|
||||
}
|
||||
+27
@@ -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 {
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright 2011 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package com.google.android.apps.mytracks.io.backup;
|
||||
|
||||
import android.app.backup.BackupManager;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
/**
|
||||
* 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, Context.MODE_PRIVATE);
|
||||
preferencesHelper.exportPreferences(preferences, outWriter);
|
||||
} catch (IOException e) {
|
||||
// We tried to delete the partially created file, but do nothing
|
||||
// if that also fails.
|
||||
if (!outputFile.delete()) {
|
||||
Log.w(TAG, "Failed to delete file " + outputFile.getAbsolutePath());
|
||||
}
|
||||
|
||||
throw e;
|
||||
} finally {
|
||||
compressedStream.closeEntry();
|
||||
compressedStream.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronously restores the backup from the given file.
|
||||
*/
|
||||
private void restoreFromFile(File inputFile) throws IOException {
|
||||
Log.d(Constants.TAG,
|
||||
"Restoring from file " + inputFile.getAbsolutePath());
|
||||
|
||||
PreferenceBackupHelper preferencesHelper = new PreferenceBackupHelper();
|
||||
ContentResolver resolver = context.getContentResolver();
|
||||
DatabaseImporter trackImporter =
|
||||
new DatabaseImporter(TracksColumns.CONTENT_URI, resolver, false);
|
||||
DatabaseImporter waypointImporter =
|
||||
new DatabaseImporter(WaypointsColumns.CONTENT_URI, resolver, false);
|
||||
DatabaseImporter pointImporter =
|
||||
new DatabaseImporter(TrackPointsColumns.CONTENT_URI, resolver, false);
|
||||
|
||||
ZipFile zipFile = new ZipFile(inputFile, ZipFile.OPEN_READ);
|
||||
ZipEntry zipEntry = zipFile.getEntry(ZIP_ENTRY_NAME);
|
||||
if (zipEntry == null) {
|
||||
throw new IOException("Invalid backup ZIP file");
|
||||
}
|
||||
|
||||
InputStream compressedStream = zipFile.getInputStream(zipEntry);
|
||||
DataInputStream reader = new DataInputStream(compressedStream);
|
||||
|
||||
try {
|
||||
// Delete all previous contents of the tables and preferences.
|
||||
resolver.delete(TracksColumns.CONTENT_URI, null, null);
|
||||
resolver.delete(TrackPointsColumns.CONTENT_URI, null, null);
|
||||
resolver.delete(WaypointsColumns.CONTENT_URI, null, null);
|
||||
|
||||
// Import the new contents of each table
|
||||
trackImporter.importAllRows(reader);
|
||||
waypointImporter.importAllRows(reader);
|
||||
pointImporter.importAllRows(reader);
|
||||
|
||||
// Restore preferences
|
||||
SharedPreferences preferences = context.getSharedPreferences(
|
||||
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
|
||||
preferencesHelper.importPreferences(reader, preferences);
|
||||
} finally {
|
||||
compressedStream.close();
|
||||
zipFile.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* Copyright 2010 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package com.google.android.apps.mytracks.io.backup;
|
||||
|
||||
import com.google.android.apps.mytracks.Constants;
|
||||
|
||||
import android.app.backup.BackupAgent;
|
||||
import android.app.backup.BackupDataInput;
|
||||
import android.app.backup.BackupDataOutput;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Backup agent used to backup and restore all preferences.
|
||||
* We use a regular {@link BackupAgent} instead of the convenient helpers in
|
||||
* order to be future-proof (assuming we'll want to back up tracks later).
|
||||
*
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
public class MyTracksBackupAgent extends BackupAgent {
|
||||
private static final String PREFERENCES_ENTITY = "prefs";
|
||||
|
||||
@Override
|
||||
public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
|
||||
ParcelFileDescriptor newState) throws IOException {
|
||||
Log.i(Constants.TAG, "Performing backup");
|
||||
SharedPreferences preferences = this.getSharedPreferences(
|
||||
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
|
||||
|
||||
backupPreferences(data, preferences);
|
||||
Log.i(Constants.TAG, "Backup complete");
|
||||
}
|
||||
|
||||
private void backupPreferences(BackupDataOutput data,
|
||||
SharedPreferences preferences) throws IOException {
|
||||
PreferenceBackupHelper preferenceDumper = createPreferenceBackupHelper();
|
||||
byte[] dumpedContents = preferenceDumper.exportPreferences(preferences);
|
||||
data.writeEntityHeader(PREFERENCES_ENTITY, dumpedContents.length);
|
||||
data.writeEntityData(dumpedContents, dumpedContents.length);
|
||||
}
|
||||
|
||||
protected PreferenceBackupHelper createPreferenceBackupHelper() {
|
||||
return new PreferenceBackupHelper();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRestore(BackupDataInput data, int appVersionCode,
|
||||
ParcelFileDescriptor newState) throws IOException {
|
||||
Log.i(Constants.TAG, "Restoring from backup");
|
||||
while (data.readNextHeader()) {
|
||||
String key = data.getKey();
|
||||
Log.d(Constants.TAG, "Restoring entity " + key);
|
||||
if (key.equals(PREFERENCES_ENTITY)) {
|
||||
restorePreferences(data);
|
||||
} else {
|
||||
Log.e(Constants.TAG, "Found unknown backup entity: " + key);
|
||||
data.skipEntityData();
|
||||
}
|
||||
}
|
||||
Log.i(Constants.TAG, "Done restoring from backup");
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores all preferences from the backup.
|
||||
*
|
||||
* @param data the backup data to read from
|
||||
* @throws IOException if there are any errors while reading
|
||||
*/
|
||||
private void restorePreferences(BackupDataInput data) throws IOException {
|
||||
int dataSize = data.getDataSize();
|
||||
byte[] dataBuffer = new byte[dataSize];
|
||||
int read = data.readEntityData(dataBuffer, 0, dataSize);
|
||||
if (read != dataSize) {
|
||||
throw new IOException("Failed to read all the preferences data");
|
||||
}
|
||||
|
||||
SharedPreferences preferences = this.getSharedPreferences(
|
||||
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
|
||||
PreferenceBackupHelper importer = createPreferenceBackupHelper();
|
||||
importer.importPreferences(dataBuffer, preferences);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,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.backup;
|
||||
|
||||
import com.google.android.apps.mytracks.content.ContentTypeIds;
|
||||
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
|
||||
|
||||
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
|
||||
* @throws IOException if there are any errors while reading
|
||||
*/
|
||||
public void importPreferences(byte[] data, SharedPreferences preferences)
|
||||
throws IOException {
|
||||
ByteArrayInputStream bufStream = new ByteArrayInputStream(data);
|
||||
DataInputStream reader = new DataInputStream(bufStream);
|
||||
|
||||
importPreferences(reader, preferences);
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports all preferences from the given stream.
|
||||
*
|
||||
* @param reader the stream to read from
|
||||
* @param preferences the shared preferences to edit
|
||||
* @throws IOException if there are any errors while reading
|
||||
*/
|
||||
public void importPreferences(DataInputStream reader,
|
||||
SharedPreferences preferences) throws IOException {
|
||||
Editor editor = preferences.edit();
|
||||
editor.clear();
|
||||
|
||||
int numPreferences = reader.readInt();
|
||||
for (int i = 0; i < numPreferences; i++) {
|
||||
String name = reader.readUTF();
|
||||
byte typeId = reader.readByte();
|
||||
readAndSetPreference(name, typeId, reader, editor);
|
||||
}
|
||||
ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a single preference and sets it into the given editor.
|
||||
*
|
||||
* @param name the name of the preference to read
|
||||
* @param typeId the type ID of the preference to read
|
||||
* @param reader the reader to read from
|
||||
* @param editor the editor to set the preference in
|
||||
* @throws IOException if there are errors while reading
|
||||
*/
|
||||
private void readAndSetPreference(String name, byte typeId,
|
||||
DataInputStream reader, Editor editor) throws IOException {
|
||||
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,51 @@
|
||||
/*
|
||||
* Copyright 2012 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package com.google.android.apps.mytracks.io.docs;
|
||||
|
||||
import com.google.android.apps.mytracks.io.sendtogoogle.AbstractSendActivity;
|
||||
import com.google.android.apps.mytracks.io.sendtogoogle.AbstractSendAsyncTask;
|
||||
import com.google.android.apps.mytracks.io.sendtogoogle.SendRequest;
|
||||
import com.google.android.apps.mytracks.io.sendtogoogle.UploadResultActivity;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.content.Intent;
|
||||
|
||||
/**
|
||||
* An activity to send a track to Google Docs.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class SendDocsActivity extends AbstractSendActivity {
|
||||
|
||||
@Override
|
||||
protected AbstractSendAsyncTask createAsyncTask() {
|
||||
return new SendDocsAsyncTask(this, sendRequest.getTrackId(), sendRequest.getAccount());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getServiceName() {
|
||||
return getString(R.string.send_google_docs);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void startNextActivity(boolean success, boolean isCancel) {
|
||||
sendRequest.setDocsSuccess(success);
|
||||
Intent intent = new Intent(this, UploadResultActivity.class)
|
||||
.putExtra(SendRequest.SEND_REQUEST_KEY, sendRequest);
|
||||
startActivity(intent);
|
||||
finish();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
/*
|
||||
* Copyright 2012 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package com.google.android.apps.mytracks.io.docs;
|
||||
|
||||
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.io.gdata.GDataClientFactory;
|
||||
import com.google.android.apps.mytracks.io.gdata.docs.DocumentsClient;
|
||||
import com.google.android.apps.mytracks.io.gdata.docs.SpreadsheetsClient;
|
||||
import com.google.android.apps.mytracks.io.gdata.docs.XmlDocsGDataParserFactory;
|
||||
import com.google.android.apps.mytracks.io.sendtogoogle.AbstractSendAsyncTask;
|
||||
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.HttpException;
|
||||
import com.google.wireless.gdata.parser.ParseException;
|
||||
import com.google.wireless.gdata2.client.AuthenticationException;
|
||||
|
||||
import android.accounts.Account;
|
||||
import android.accounts.AccountManager;
|
||||
import android.accounts.AuthenticatorException;
|
||||
import android.accounts.OperationCanceledException;
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* AsyncTask to send a track to Google Docs.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class SendDocsAsyncTask extends AbstractSendAsyncTask {
|
||||
private static final int PROGRESS_GET_SPREADSHEET_ID = 0;
|
||||
private static final int PROGRESS_CREATE_SPREADSHEET = 25;
|
||||
private static final int PROGRESS_GET_WORKSHEET_ID = 50;
|
||||
private static final int PROGRESS_ADD_TRACK_INFO = 75;
|
||||
private static final int PROGRESS_COMPLETE = 100;
|
||||
|
||||
private static final String TAG = SendDocsAsyncTask.class.getSimpleName();
|
||||
|
||||
private final long trackId;
|
||||
private final Account account;
|
||||
private final Context context;
|
||||
private final MyTracksProviderUtils myTracksProviderUtils;
|
||||
private final GDataClient gDataClient;
|
||||
private final DocumentsClient documentsClient;
|
||||
private final SpreadsheetsClient spreadsheetsClient;
|
||||
|
||||
// The following variables are for per upload states
|
||||
private String documentsAuthToken;
|
||||
private String spreadsheetsAuthToken;
|
||||
private String spreadsheetId;
|
||||
private String worksheetId;
|
||||
|
||||
public SendDocsAsyncTask(SendDocsActivity activity, long trackId, Account account) {
|
||||
super(activity);
|
||||
this.trackId = trackId;
|
||||
this.account = account;
|
||||
|
||||
context = activity.getApplicationContext();
|
||||
myTracksProviderUtils = MyTracksProviderUtils.Factory.get(context);
|
||||
gDataClient = GDataClientFactory.getGDataClient(context);
|
||||
documentsClient = new DocumentsClient(
|
||||
gDataClient, new XmlDocsGDataParserFactory(new AndroidXmlParserFactory()));
|
||||
spreadsheetsClient = new SpreadsheetsClient(
|
||||
gDataClient, new XmlDocsGDataParserFactory(new AndroidXmlParserFactory()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void closeConnection() {
|
||||
if (gDataClient != null) {
|
||||
gDataClient.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void saveResult() {
|
||||
// No action for Google Docs
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean performTask() {
|
||||
// Reset the per upload states
|
||||
documentsAuthToken = null;
|
||||
spreadsheetsAuthToken = null;
|
||||
spreadsheetId = null;
|
||||
worksheetId = null;
|
||||
|
||||
try {
|
||||
documentsAuthToken = AccountManager.get(context).blockingGetAuthToken(
|
||||
account, documentsClient.getServiceName(), false);
|
||||
spreadsheetsAuthToken = AccountManager.get(context).blockingGetAuthToken(
|
||||
account, spreadsheetsClient.getServiceName(), false);
|
||||
} catch (OperationCanceledException e) {
|
||||
Log.d(TAG, "Unable to get auth token", e);
|
||||
return retryTask();
|
||||
} catch (AuthenticatorException e) {
|
||||
Log.d(TAG, "Unable to get auth token", e);
|
||||
return retryTask();
|
||||
} catch (IOException e) {
|
||||
Log.d(TAG, "Unable to get auth token", e);
|
||||
return retryTask();
|
||||
}
|
||||
|
||||
Track track = myTracksProviderUtils.getTrack(trackId);
|
||||
if (track == null) {
|
||||
Log.d(TAG, "Track is null");
|
||||
return false;
|
||||
}
|
||||
|
||||
String title = context.getString(R.string.my_tracks_app_name);
|
||||
if (track.getCategory() != null && !track.getCategory().equals("")) {
|
||||
title += "-" + track.getCategory();
|
||||
}
|
||||
|
||||
// Get the spreadsheet ID
|
||||
publishProgress(PROGRESS_GET_SPREADSHEET_ID);
|
||||
if (!fetchSpreadSheetId(title, false)) {
|
||||
return retryTask();
|
||||
}
|
||||
|
||||
// Create a new spreadsheet if necessary
|
||||
publishProgress(PROGRESS_CREATE_SPREADSHEET);
|
||||
if (spreadsheetId == null) {
|
||||
if (!createSpreadSheet(title)) {
|
||||
Log.d(TAG, "Unable to create a new spreadsheet");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 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.
|
||||
if (spreadsheetId == null) {
|
||||
if (!fetchSpreadSheetId(title, true)) {
|
||||
Log.d(TAG, "Unable to check if the new spreadsheet is created");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (spreadsheetId == null) {
|
||||
Log.d(TAG, "Unable to create a new spreadsheet");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get the worksheet ID
|
||||
publishProgress(PROGRESS_GET_WORKSHEET_ID);
|
||||
if (!fetchWorksheetId()) {
|
||||
return retryTask();
|
||||
}
|
||||
if (worksheetId == null) {
|
||||
Log.d(TAG, "Unable to get a worksheet ID");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Add the track info
|
||||
publishProgress(PROGRESS_ADD_TRACK_INFO);
|
||||
if (!addTrackInfo(track)) {
|
||||
Log.d(TAG, "Unable to add track info");
|
||||
return false;
|
||||
}
|
||||
|
||||
publishProgress(PROGRESS_COMPLETE);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void invalidateToken() {
|
||||
AccountManager.get(context).invalidateAuthToken(Constants.ACCOUNT_TYPE, documentsAuthToken);
|
||||
AccountManager.get(context).invalidateAuthToken(Constants.ACCOUNT_TYPE, spreadsheetsAuthToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the spreadsheet id. Sets the instance variable
|
||||
* {@link SendDocsAsyncTask#spreadsheetId}.
|
||||
*
|
||||
* @param title the spreadsheet title
|
||||
* @param waitFirst wait before checking
|
||||
* @return true if completes.
|
||||
*/
|
||||
private boolean fetchSpreadSheetId(String title, boolean waitFirst) {
|
||||
if (isCancelled()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (waitFirst) {
|
||||
try {
|
||||
Thread.sleep(5000);
|
||||
} catch (InterruptedException e) {
|
||||
Log.d(TAG, "Unable to wait", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
spreadsheetId = SendDocsUtils.getSpreadsheetId(title, documentsClient, documentsAuthToken);
|
||||
} catch (ParseException e) {
|
||||
Log.d(TAG, "Unable to fetch spreadsheet ID", e);
|
||||
return false;
|
||||
} catch (HttpException e) {
|
||||
Log.d(TAG, "Unable to fetch spreadsheet ID", e);
|
||||
return false;
|
||||
} catch (IOException e) {
|
||||
Log.d(TAG, "Unable to fetch spreadsheet ID", e);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (spreadsheetId == null) {
|
||||
// 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.d(TAG, "Unable to wait", e);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
spreadsheetId = SendDocsUtils.getSpreadsheetId(title, documentsClient, documentsAuthToken);
|
||||
} catch (ParseException e) {
|
||||
Log.d(TAG, "Unable to fetch spreadsheet ID", e);
|
||||
return false;
|
||||
} catch (HttpException e) {
|
||||
Log.d(TAG, "Unable to fetch spreadsheet ID", e);
|
||||
return false;
|
||||
} catch (IOException e) {
|
||||
Log.d(TAG, "Unable to fetch spreadsheet ID", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a spreadsheet. If successful, sets the instance variable
|
||||
* {@link SendDocsAsyncTask#spreadsheetId}.
|
||||
*
|
||||
* @param spreadsheetTitle the spreadsheet title
|
||||
* @return true if completes.
|
||||
*/
|
||||
private boolean createSpreadSheet(String spreadsheetTitle) {
|
||||
if (isCancelled()) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
spreadsheetId = SendDocsUtils.createSpreadsheet(spreadsheetTitle, documentsAuthToken, context);
|
||||
} catch (IOException e) {
|
||||
Log.d(TAG, "Unable to create spreadsheet", e);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the worksheet ID. Sets the instance variable
|
||||
* {@link SendDocsAsyncTask#worksheetId}.
|
||||
*
|
||||
* @return true if completes.
|
||||
*/
|
||||
private boolean fetchWorksheetId() {
|
||||
if (isCancelled()) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
worksheetId = SendDocsUtils.getWorksheetId(
|
||||
spreadsheetId, spreadsheetsClient, spreadsheetsAuthToken);
|
||||
} catch (IOException e) {
|
||||
Log.d(TAG, "Unable to fetch worksheet ID", e);
|
||||
return false;
|
||||
} catch (AuthenticationException e) {
|
||||
Log.d(TAG, "Unable to fetch worksheet ID", e);
|
||||
return false;
|
||||
} catch (ParseException e) {
|
||||
Log.d(TAG, "Unable to fetch worksheet ID", e);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds track info to a worksheet.
|
||||
*
|
||||
* @param track the track
|
||||
* @return true if completes.
|
||||
*/
|
||||
private boolean addTrackInfo(Track track) {
|
||||
if (isCancelled()) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
SendDocsUtils.addTrackInfo(track, spreadsheetId, worksheetId, spreadsheetsAuthToken, context);
|
||||
} catch (IOException e) {
|
||||
Log.d(TAG, "Unable to add track info", e);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,410 @@
|
||||
/*
|
||||
* Copyright 2012 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package com.google.android.apps.mytracks.io.docs;
|
||||
|
||||
import com.google.android.apps.mytracks.Constants;
|
||||
import com.google.android.apps.mytracks.content.Track;
|
||||
import com.google.android.apps.mytracks.io.gdata.docs.DocumentsClient;
|
||||
import com.google.android.apps.mytracks.io.gdata.docs.SpreadsheetsClient;
|
||||
import com.google.android.apps.mytracks.io.gdata.docs.SpreadsheetsClient.WorksheetEntry;
|
||||
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.apps.mytracks.util.UnitConversions;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.wireless.gdata.client.HttpException;
|
||||
import com.google.wireless.gdata.data.Entry;
|
||||
import com.google.wireless.gdata.parser.GDataParser;
|
||||
import com.google.wireless.gdata.parser.ParseException;
|
||||
import com.google.wireless.gdata2.client.AuthenticationException;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
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.net.URLEncoder;
|
||||
import java.text.NumberFormat;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Utilities for sending a track to Google Docs.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
* @author Matthew Simmons
|
||||
*/
|
||||
public class SendDocsUtils {
|
||||
private static final String GET_SPREADSHEET_BY_TITLE_URI =
|
||||
"https://docs.google.com/feeds/documents/private/full?"
|
||||
+ "category=mine,spreadsheet&title=%s&title-exact=true";
|
||||
private static final String CREATE_SPREADSHEET_URI =
|
||||
"https://docs.google.com/feeds/documents/private/full";
|
||||
private static final String GET_WORKSHEETS_URI =
|
||||
"https://spreadsheets.google.com/feeds/worksheets/%s/private/full";
|
||||
private static final String GET_WORKSHEET_URI =
|
||||
"https://spreadsheets.google.com/feeds/list/%s/%s/private/full";
|
||||
|
||||
private static final String SPREADSHEET_ID_PREFIX =
|
||||
"https://docs.google.com/feeds/documents/private/full/spreadsheet%3A";
|
||||
|
||||
private static final String CONTENT_TYPE = "Content-Type";
|
||||
private static final String ATOM_FEED_MIME_TYPE = "application/atom+xml";
|
||||
private static final String OPENDOCUMENT_SPREADSHEET_MIME_TYPE =
|
||||
"application/x-vnd.oasis.opendocument.spreadsheet";
|
||||
|
||||
private static final String AUTHORIZATION = "Authorization";
|
||||
private static final String AUTHORIZATION_PREFIX = "GoogleLogin auth=";
|
||||
|
||||
private static final String SLUG = "Slug";
|
||||
|
||||
// Google Docs can only parse numbers in the English locale.
|
||||
private static final NumberFormat NUMBER_FORMAT = NumberFormat.getNumberInstance(Locale.ENGLISH);
|
||||
private static final NumberFormat INTEGER_FORMAT = NumberFormat.getIntegerInstance(
|
||||
Locale.ENGLISH);
|
||||
|
||||
static {
|
||||
NUMBER_FORMAT.setMaximumFractionDigits(2);
|
||||
NUMBER_FORMAT.setMinimumFractionDigits(2);
|
||||
}
|
||||
|
||||
private static final String TAG = SendDocsUtils.class.getSimpleName();
|
||||
|
||||
private SendDocsUtils() {}
|
||||
|
||||
/**
|
||||
* Gets the spreadsheet id of a spreadsheet. Returns null if the spreadsheet
|
||||
* doesn't exist.
|
||||
*
|
||||
* @param title the title of the spreadsheet
|
||||
* @param documentsClient the documents client
|
||||
* @param authToken the auth token
|
||||
* @return spreadsheet id or null if it doesn't exist.
|
||||
*/
|
||||
public static String getSpreadsheetId(
|
||||
String title, DocumentsClient documentsClient, String authToken)
|
||||
throws IOException, ParseException, HttpException {
|
||||
GDataParser gDataParser = null;
|
||||
try {
|
||||
String uri = String.format(GET_SPREADSHEET_BY_TITLE_URI, URLEncoder.encode(title));
|
||||
gDataParser = documentsClient.getParserForFeed(Entry.class, uri, authToken);
|
||||
gDataParser.init();
|
||||
|
||||
while (gDataParser.hasMoreData()) {
|
||||
Entry entry = gDataParser.readNextEntry(null);
|
||||
String entryTitle = entry.getTitle();
|
||||
if (entryTitle.equals(title)) {
|
||||
return getEntryId(entry);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
} finally {
|
||||
if (gDataParser != null) {
|
||||
gDataParser.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the id from an entry. Returns null if not available.
|
||||
*
|
||||
* @param entry the entry
|
||||
*/
|
||||
@VisibleForTesting
|
||||
static String getEntryId(Entry entry) {
|
||||
String entryId = entry.getId();
|
||||
if (entryId.startsWith(SPREADSHEET_ID_PREFIX)) {
|
||||
return entryId.substring(SPREADSHEET_ID_PREFIX.length());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new spreadsheet with the given title. Returns the spreadsheet ID
|
||||
* if successful. Returns null otherwise. Note that it is possible that a new
|
||||
* spreadsheet is created, but the returned ID is null.
|
||||
*
|
||||
* @param title the title
|
||||
* @param authToken the auth token
|
||||
* @param context the context
|
||||
*/
|
||||
public static String createSpreadsheet(String title, String authToken, Context context)
|
||||
throws IOException {
|
||||
URL url = new URL(CREATE_SPREADSHEET_URI);
|
||||
URLConnection conn = url.openConnection();
|
||||
conn.addRequestProperty(CONTENT_TYPE, OPENDOCUMENT_SPREADSHEET_MIME_TYPE);
|
||||
conn.addRequestProperty(SLUG, title);
|
||||
conn.addRequestProperty(AUTHORIZATION, AUTHORIZATION_PREFIX + authToken);
|
||||
conn.setDoOutput(true);
|
||||
OutputStream outputStream = conn.getOutputStream();
|
||||
ResourceUtils.readBinaryFileToOutputStream(
|
||||
context, R.raw.mytracks_empty_spreadsheet, outputStream);
|
||||
|
||||
// Get the response
|
||||
BufferedReader bufferedReader = null;
|
||||
StringBuilder resultBuilder = new StringBuilder();
|
||||
try {
|
||||
bufferedReader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
|
||||
String line;
|
||||
while ((line = bufferedReader.readLine()) != null) {
|
||||
resultBuilder.append(line);
|
||||
}
|
||||
} 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.
|
||||
Log.d(TAG, "Unable to read result after creating a spreadsheet", e);
|
||||
return null;
|
||||
} finally {
|
||||
outputStream.close();
|
||||
if (bufferedReader != null) {
|
||||
bufferedReader.close();
|
||||
}
|
||||
}
|
||||
return getNewSpreadsheetId(resultBuilder.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the spreadsheet id from a create spreadsheet result.
|
||||
*
|
||||
* @param result the create spreadsheet result
|
||||
*/
|
||||
@VisibleForTesting
|
||||
static String getNewSpreadsheetId(String result) {
|
||||
int idTagIndex = result.indexOf("<id>");
|
||||
if (idTagIndex == -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int idTagCloseIndex = result.indexOf("</id>", idTagIndex);
|
||||
if (idTagCloseIndex == -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int idStringStart = result.indexOf(SPREADSHEET_ID_PREFIX, idTagIndex);
|
||||
if (idStringStart == -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return result.substring(idStringStart + SPREADSHEET_ID_PREFIX.length(), idTagCloseIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the first worksheet ID of a spreadsheet. Returns null if not
|
||||
* available.
|
||||
*
|
||||
* @param spreadsheetId the spreadsheet ID
|
||||
* @param spreadsheetClient the spreadsheet client
|
||||
* @param authToken the auth token
|
||||
*/
|
||||
public static String getWorksheetId(
|
||||
String spreadsheetId, SpreadsheetsClient spreadsheetClient, String authToken)
|
||||
throws IOException, AuthenticationException, ParseException {
|
||||
GDataParser gDataParser = null;
|
||||
try {
|
||||
String uri = String.format(GET_WORKSHEETS_URI, spreadsheetId);
|
||||
gDataParser = spreadsheetClient.getParserForWorksheetsFeed(uri, authToken);
|
||||
gDataParser.init();
|
||||
if (!gDataParser.hasMoreData()) {
|
||||
Log.d(TAG, "No worksheet");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get the first worksheet
|
||||
WorksheetEntry worksheetEntry =
|
||||
(WorksheetEntry) gDataParser.readNextEntry(new WorksheetEntry());
|
||||
return getWorksheetEntryId(worksheetEntry);
|
||||
} finally {
|
||||
if (gDataParser != null) {
|
||||
gDataParser.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the worksheet id from a worksheet entry. Returns null if not available.
|
||||
*
|
||||
* @param entry the worksheet entry
|
||||
*/
|
||||
@VisibleForTesting
|
||||
static String getWorksheetEntryId(WorksheetEntry entry) {
|
||||
String id = entry.getId();
|
||||
int lastSlash = id.lastIndexOf('/');
|
||||
if (lastSlash == -1) {
|
||||
Log.d(TAG, "No id");
|
||||
return null;
|
||||
}
|
||||
return id.substring(lastSlash + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a track's info as a row in a worksheet.
|
||||
*
|
||||
* @param track the track
|
||||
* @param spreadsheetId the spreadsheet ID
|
||||
* @param worksheetId the worksheet ID
|
||||
* @param authToken the auth token
|
||||
* @param context the context
|
||||
*/
|
||||
public static void addTrackInfo(
|
||||
Track track, String spreadsheetId, String worksheetId, String authToken, Context context)
|
||||
throws IOException {
|
||||
String worksheetUri = String.format(GET_WORKSHEET_URI, spreadsheetId, worksheetId);
|
||||
SharedPreferences prefs = context.getSharedPreferences(
|
||||
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
|
||||
boolean metricUnits = prefs.getBoolean(context.getString(R.string.metric_units_key), true);
|
||||
|
||||
addRow(worksheetUri, getRowContent(track, metricUnits, context), authToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the row content containing the track's info.
|
||||
*
|
||||
* @param track the track
|
||||
* @param metricUnits true to use metric
|
||||
* @param context the context
|
||||
*/
|
||||
@VisibleForTesting
|
||||
static String getRowContent(Track track, boolean metricUnits, Context context) {
|
||||
TripStatistics stats = track.getStatistics();
|
||||
|
||||
String distanceUnit = context.getString(
|
||||
metricUnits ? R.string.unit_kilometer : R.string.unit_mile);
|
||||
String speedUnit = context.getString(
|
||||
metricUnits ? R.string.unit_kilometer_per_hour : R.string.unit_mile_per_hour);
|
||||
String elevationUnit = context.getString(
|
||||
metricUnits ? R.string.unit_meter : R.string.unit_feet);
|
||||
|
||||
StringBuilder builder = new StringBuilder().append("<entry xmlns='http://www.w3.org/2005/Atom' "
|
||||
+ "xmlns:gsx='http://schemas.google.com/spreadsheets/2006/extended'>");
|
||||
appendTag(builder, "name", track.getName());
|
||||
appendTag(builder, "description", track.getDescription());
|
||||
appendTag(builder, "date", StringUtils.formatDateTime(context, stats.getStartTime()));
|
||||
appendTag(builder, "totaltime", StringUtils.formatElapsedTime(stats.getTotalTime()));
|
||||
appendTag(builder, "movingtime", StringUtils.formatElapsedTime(stats.getMovingTime()));
|
||||
appendTag(builder, "distance", getDistance(stats.getTotalDistance(), metricUnits));
|
||||
appendTag(builder, "distanceunit", distanceUnit);
|
||||
appendTag(builder, "averagespeed", getSpeed(stats.getAverageSpeed(), metricUnits));
|
||||
appendTag(builder, "averagemovingspeed", getSpeed(stats.getAverageMovingSpeed(), metricUnits));
|
||||
appendTag(builder, "maxspeed", getSpeed(stats.getMaxSpeed(), metricUnits));
|
||||
appendTag(builder, "speedunit", speedUnit);
|
||||
appendTag(builder, "elevationgain", getElevation(stats.getTotalElevationGain(), metricUnits));
|
||||
appendTag(builder, "minelevation", getElevation(stats.getMinElevation(), metricUnits));
|
||||
appendTag(builder, "maxelevation", getElevation(stats.getMaxElevation(), metricUnits));
|
||||
appendTag(builder, "elevationunit", elevationUnit);
|
||||
|
||||
if (track.getMapId().length() > 0) {
|
||||
appendTag(builder, "map",
|
||||
String.format("%s?msa=0&msid=%s", Constants.MAPSHOP_BASE_URL, track.getMapId()));
|
||||
}
|
||||
|
||||
builder.append("</entry>");
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends a name-value pair as a gsx tag to a string builder.
|
||||
*
|
||||
* @param stringBuilder the string builder
|
||||
* @param name the name
|
||||
* @param value the value
|
||||
*/
|
||||
@VisibleForTesting
|
||||
static void appendTag(StringBuilder stringBuilder, String name, String value) {
|
||||
stringBuilder
|
||||
.append("<gsx:")
|
||||
.append(name)
|
||||
.append(">")
|
||||
.append(StringUtils.stringAsCData(value))
|
||||
.append("</gsx:")
|
||||
.append(name)
|
||||
.append(">");
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the distance. Performs unit conversion and formatting.
|
||||
*
|
||||
* @param distanceInMeter the distance in meters
|
||||
* @param metricUnits true to use metric
|
||||
*/
|
||||
@VisibleForTesting
|
||||
static final String getDistance(double distanceInMeter, boolean metricUnits) {
|
||||
double distanceInKilometer = distanceInMeter * UnitConversions.M_TO_KM;
|
||||
double distance = metricUnits ? distanceInKilometer
|
||||
: distanceInKilometer * UnitConversions.KM_TO_MI;
|
||||
return NUMBER_FORMAT.format(distance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the speed. Performs unit conversion and formatting.
|
||||
*
|
||||
* @param speedInMeterPerSecond the speed in meters per second
|
||||
* @param metricUnits true to use metric
|
||||
*/
|
||||
@VisibleForTesting
|
||||
static final String getSpeed(double speedInMeterPerSecond, boolean metricUnits) {
|
||||
double speedInKilometerPerHour = speedInMeterPerSecond * UnitConversions.MS_TO_KMH;
|
||||
double speed = metricUnits ? speedInKilometerPerHour
|
||||
: speedInKilometerPerHour * UnitConversions.KM_TO_MI;
|
||||
return NUMBER_FORMAT.format(speed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the elevation. Performs unit conversion and formatting.
|
||||
*
|
||||
* @param elevationInMeter the elevation value in meters
|
||||
* @param metricUnits true to use metric
|
||||
*/
|
||||
@VisibleForTesting
|
||||
static final String getElevation(double elevationInMeter, boolean metricUnits) {
|
||||
double elevation = metricUnits ? elevationInMeter : elevationInMeter * UnitConversions.M_TO_FT;
|
||||
return INTEGER_FORMAT.format(elevation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a row to a Google Spreadsheet worksheet.
|
||||
*
|
||||
* @param worksheetUri the worksheet URI
|
||||
* @param rowContent the row content
|
||||
* @param authToken the auth token
|
||||
*/
|
||||
private static final void addRow(String worksheetUri, String rowContent, String authToken)
|
||||
throws IOException {
|
||||
URL url = new URL(worksheetUri);
|
||||
URLConnection conn = url.openConnection();
|
||||
conn.addRequestProperty(CONTENT_TYPE, ATOM_FEED_MIME_TYPE);
|
||||
conn.addRequestProperty(AUTHORIZATION, AUTHORIZATION_PREFIX + authToken);
|
||||
conn.setDoOutput(true);
|
||||
OutputStreamWriter writer = new OutputStreamWriter(conn.getOutputStream());
|
||||
writer.write(rowContent);
|
||||
writer.flush();
|
||||
|
||||
// Get the response
|
||||
BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
|
||||
while ((reader.readLine()) != null) {
|
||||
// Just read till the end
|
||||
}
|
||||
writer.close();
|
||||
reader.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
/*
|
||||
* Copyright 2010 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package com.google.android.apps.mytracks.io.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.FileUtils;
|
||||
|
||||
import android.location.Location;
|
||||
|
||||
import java.io.OutputStream;
|
||||
import java.io.PrintWriter;
|
||||
import java.text.NumberFormat;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
|
||||
private static final NumberFormat SHORT_FORMAT = NumberFormat.getInstance();
|
||||
|
||||
static {
|
||||
SHORT_FORMAT.setMaximumFractionDigits(4);
|
||||
}
|
||||
|
||||
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 = FileUtils.FILE_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 writeBeginWaypoints() {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeEndWaypoints() {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeWaypoint(Waypoint waypoint) {
|
||||
Location location = waypoint.getLocation();
|
||||
String timeStr = FileUtils.FILE_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,184 @@
|
||||
/*
|
||||
* 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.FileUtils;
|
||||
import com.google.android.apps.mytracks.util.StringUtils;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.content.Context;
|
||||
import android.location.Location;
|
||||
|
||||
import java.io.OutputStream;
|
||||
import java.io.PrintWriter;
|
||||
import java.text.NumberFormat;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Write track as GPX to a file.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class GpxTrackWriter implements TrackFormatWriter {
|
||||
|
||||
private static final NumberFormat ELEVATION_FORMAT = NumberFormat.getInstance(Locale.US);
|
||||
private static final NumberFormat COORDINATE_FORMAT = NumberFormat.getInstance(Locale.US);
|
||||
static {
|
||||
// GPX readers expect to see fractional numbers with US-style punctuation.
|
||||
// That is, they want periods for decimal points, rather than commas.
|
||||
ELEVATION_FORMAT.setMaximumFractionDigits(1);
|
||||
ELEVATION_FORMAT.setGroupingUsed(false);
|
||||
|
||||
COORDINATE_FORMAT.setMaximumFractionDigits(5);
|
||||
COORDINATE_FORMAT.setMaximumIntegerDigits(3);
|
||||
COORDINATE_FORMAT.setGroupingUsed(false);
|
||||
}
|
||||
|
||||
private final Context context;
|
||||
private Track track;
|
||||
private PrintWriter printWriter;
|
||||
|
||||
public GpxTrackWriter(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getExtension() {
|
||||
return TrackFileFormat.GPX.getExtension();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void prepare(Track aTrack, OutputStream outputStream) {
|
||||
this.track = aTrack;
|
||||
this.printWriter = new PrintWriter(outputStream);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (printWriter != null) {
|
||||
printWriter.close();
|
||||
printWriter = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeHeader() {
|
||||
if (printWriter != null) {
|
||||
printWriter.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
|
||||
printWriter.println("<gpx");
|
||||
printWriter.println("version=\"1.1\"");
|
||||
printWriter.println(
|
||||
"creator=\"" + context.getString(R.string.send_google_by_my_tracks, "", "") + "\"");
|
||||
printWriter.println("xmlns=\"http://www.topografix.com/GPX/1/1\"");
|
||||
printWriter.println(
|
||||
"xmlns:topografix=\"http://www.topografix.com/GPX/Private/TopoGrafix/0/1\"");
|
||||
printWriter.println("xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"");
|
||||
printWriter.println("xsi:schemaLocation=\"http://www.topografix.com/GPX/1/1"
|
||||
+ " http://www.topografix.com/GPX/1/1/gpx.xsd"
|
||||
+ " http://www.topografix.com/GPX/Private/TopoGrafix/0/1"
|
||||
+ " http://www.topografix.com/GPX/Private/TopoGrafix/0/1/topografix.xsd\">");
|
||||
printWriter.println("<metadata>");
|
||||
printWriter.println("<name>" + StringUtils.stringAsCData(track.getName()) + "</name>");
|
||||
printWriter.println("<desc>" + StringUtils.stringAsCData(track.getDescription()) + "</desc>");
|
||||
printWriter.println("</metadata>");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeFooter() {
|
||||
if (printWriter != null) {
|
||||
printWriter.println("</gpx>");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeBeginTrack(Location firstLocation) {
|
||||
if (printWriter != null) {
|
||||
printWriter.println("<trk>");
|
||||
printWriter.println("<name>" + StringUtils.stringAsCData(track.getName()) + "</name>");
|
||||
printWriter.println("<desc>" + StringUtils.stringAsCData(track.getDescription()) + "</desc>");
|
||||
printWriter.println("<extensions><topografix:color>c0c0c0</topografix:color></extensions>");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeEndTrack(Location lastLocation) {
|
||||
if (printWriter != null) {
|
||||
printWriter.println("</trk>");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeOpenSegment() {
|
||||
printWriter.println("<trkseg>");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeCloseSegment() {
|
||||
printWriter.println("</trkseg>");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeLocation(Location location) {
|
||||
if (printWriter != null) {
|
||||
printWriter.println("<trkpt " + formatLocation(location) + ">");
|
||||
printWriter.println("<ele>" + ELEVATION_FORMAT.format(location.getAltitude()) + "</ele>");
|
||||
printWriter.println(
|
||||
"<time>" + FileUtils.FILE_TIMESTAMP_FORMAT.format(location.getTime()) + "</time>");
|
||||
printWriter.println("</trkpt>");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeBeginWaypoints() {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeEndWaypoints() {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeWaypoint(Waypoint waypoint) {
|
||||
if (printWriter != null) {
|
||||
Location location = waypoint.getLocation();
|
||||
if (location != null) {
|
||||
printWriter.println("<wpt " + formatLocation(location) + ">");
|
||||
printWriter.println("<ele>" + ELEVATION_FORMAT.format(location.getAltitude()) + "</ele>");
|
||||
printWriter.println(
|
||||
"<time>" + FileUtils.FILE_TIMESTAMP_FORMAT.format(location.getTime()) + "</time>");
|
||||
printWriter.println("<name>" + StringUtils.stringAsCData(waypoint.getName()) + "</name>");
|
||||
printWriter.println(
|
||||
"<desc>" + StringUtils.stringAsCData(waypoint.getDescription()) + "</desc>");
|
||||
printWriter.println("</wpt>");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a location with latitude and longitude coordinates.
|
||||
*
|
||||
* @param location the location
|
||||
*/
|
||||
private String formatLocation(Location location) {
|
||||
return "lat=\"" + COORDINATE_FORMAT.format(location.getLatitude()) + "\" lon=\""
|
||||
+ COORDINATE_FORMAT.format(location.getLongitude()) + "\"";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* 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.ImportAllTracks;
|
||||
import com.google.android.apps.mytracks.util.UriUtils;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* An activity that imports a track from a file and displays the track in My Tracks.
|
||||
*
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
public class ImportActivity extends Activity {
|
||||
|
||||
@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))) {
|
||||
Log.e(TAG, "Received an intent with unsupported action: " + intent);
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!UriUtils.isFileUri(data)) {
|
||||
Log.e(TAG, "Received an intent with unsupported data: " + intent);
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
String path = data.getPath();
|
||||
Log.i(TAG, "Importing GPX file at " + path);
|
||||
new ImportAllTracks(this, path);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,362 @@
|
||||
/*
|
||||
* Copyright 2008 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package com.google.android.apps.mytracks.io.file;
|
||||
|
||||
import com.google.android.apps.mytracks.content.DescriptionGenerator;
|
||||
import com.google.android.apps.mytracks.content.DescriptionGeneratorImpl;
|
||||
import com.google.android.apps.mytracks.content.MyTracksLocation;
|
||||
import com.google.android.apps.mytracks.content.Sensor;
|
||||
import com.google.android.apps.mytracks.content.Sensor.SensorData;
|
||||
import com.google.android.apps.mytracks.content.Sensor.SensorDataSet;
|
||||
import com.google.android.apps.mytracks.content.Track;
|
||||
import com.google.android.apps.mytracks.content.Waypoint;
|
||||
import com.google.android.apps.mytracks.io.file.TrackWriterFactory.TrackFileFormat;
|
||||
import com.google.android.apps.mytracks.util.FileUtils;
|
||||
import com.google.android.apps.mytracks.util.StringUtils;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
|
||||
import android.content.Context;
|
||||
import android.location.Location;
|
||||
|
||||
import java.io.OutputStream;
|
||||
import java.io.PrintWriter;
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* Write track as KML to a file.
|
||||
*
|
||||
* @author Leif Hendrik Wilden
|
||||
*/
|
||||
public class KmlTrackWriter implements TrackFormatWriter {
|
||||
|
||||
private static final String WAYPOINT_STYLE = "waypoint";
|
||||
private static final String STATISTICS_STYLE = "statistics";
|
||||
private static final String START_STYLE = "start";
|
||||
private static final String END_STYLE = "end";
|
||||
private static final String TRACK_STYLE = "track";
|
||||
private static final String SCHEMA_ID = "schema";
|
||||
private static final String CADENCE = "cadence";
|
||||
private static final String HEART_RATE = "heart_rate";
|
||||
private static final String POWER = "power";
|
||||
private static final String BATTER_LEVEL = "battery_level";
|
||||
|
||||
private final Context context;
|
||||
private final DescriptionGenerator descriptionGenerator;
|
||||
private Track track;
|
||||
private PrintWriter printWriter;
|
||||
private ArrayList<Integer> powerList = new ArrayList<Integer>();
|
||||
private ArrayList<Integer> cadenceList = new ArrayList<Integer>();
|
||||
private ArrayList<Integer> heartRateList = new ArrayList<Integer>();
|
||||
private ArrayList<Integer> batteryLevelList = new ArrayList<Integer>();
|
||||
private boolean hasPower;
|
||||
private boolean hasCadence;
|
||||
private boolean hasHeartRate;
|
||||
private boolean hasBatteryLevel;
|
||||
|
||||
public KmlTrackWriter(Context context) {
|
||||
this(context, new DescriptionGeneratorImpl(context));
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
KmlTrackWriter(Context context, DescriptionGenerator descriptionGenerator) {
|
||||
this.context = context;
|
||||
this.descriptionGenerator = descriptionGenerator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getExtension() {
|
||||
return TrackFileFormat.KML.getExtension();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void prepare(Track aTrack, OutputStream outputStream) {
|
||||
this.track = aTrack;
|
||||
this.printWriter = new PrintWriter(outputStream);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (printWriter != null) {
|
||||
printWriter.close();
|
||||
printWriter = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeHeader() {
|
||||
if (printWriter != null) {
|
||||
printWriter.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
|
||||
printWriter.println("<kml xmlns=\"http://www.opengis.net/kml/2.2\"");
|
||||
printWriter.println("xmlns:atom=\"http://www.w3.org/2005/Atom\"");
|
||||
printWriter.println("xmlns:gx=\"http://www.google.com/kml/ext/2.2\">");
|
||||
printWriter.println("<Document>");
|
||||
printWriter.println("<open>1</open>");
|
||||
printWriter.println("<visibility>1</visibility>");
|
||||
printWriter.println(
|
||||
"<description>" + StringUtils.stringAsCData(track.getDescription()) + "</description>");
|
||||
printWriter.println("<name>" + StringUtils.stringAsCData(track.getName()) + "</name>");
|
||||
printWriter.println("<atom:author><atom:name>" + StringUtils.stringAsCData(
|
||||
context.getString(R.string.send_google_by_my_tracks, "", ""))
|
||||
+ "</atom:name></atom:author>");
|
||||
writeTrackStyle();
|
||||
writePlacemarkerStyle(
|
||||
START_STYLE, "http://maps.google.com/mapfiles/kml/paddle/grn-circle.png", 32, 1);
|
||||
writePlacemarkerStyle(
|
||||
END_STYLE, "http://maps.google.com/mapfiles/kml/paddle/red-circle.png", 32, 1);
|
||||
writePlacemarkerStyle(
|
||||
STATISTICS_STYLE, "http://maps.google.com/mapfiles/kml/pushpin/ylw-pushpin.png", 20, 2);
|
||||
writePlacemarkerStyle(
|
||||
WAYPOINT_STYLE, "http://maps.google.com/mapfiles/kml/pushpin/blue-pushpin.png", 20, 2);
|
||||
printWriter.println("<Schema id=\"" + SCHEMA_ID + "\">");
|
||||
writeSensorStyle(POWER, context.getString(R.string.description_sensor_power));
|
||||
writeSensorStyle(CADENCE, context.getString(R.string.description_sensor_cadence));
|
||||
writeSensorStyle(HEART_RATE, context.getString(R.string.description_sensor_heart_rate));
|
||||
writeSensorStyle(BATTER_LEVEL, context.getString(R.string.description_sensor_battery_level));
|
||||
printWriter.println("</Schema>");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeFooter() {
|
||||
if (printWriter != null) {
|
||||
printWriter.println("</Document>");
|
||||
printWriter.println("</kml>");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeBeginWaypoints() {
|
||||
if (printWriter != null) {
|
||||
printWriter.println(
|
||||
"<Folder><name>" + StringUtils.stringAsCData(context.getString(R.string.menu_markers))
|
||||
+ "</name>");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeEndWaypoints() {
|
||||
if (printWriter != null) {
|
||||
printWriter.println("</Folder>");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeWaypoint(Waypoint waypoint) {
|
||||
if (printWriter != null) {
|
||||
String styleName = waypoint.getType() == Waypoint.TYPE_STATISTICS ? STATISTICS_STYLE
|
||||
: WAYPOINT_STYLE;
|
||||
writePlacemark(
|
||||
waypoint.getName(), waypoint.getDescription(), styleName, waypoint.getLocation());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeBeginTrack(Location firstLocation) {
|
||||
if (printWriter != null) {
|
||||
String name = context.getString(R.string.marker_label_start, track.getName());
|
||||
writePlacemark(name, track.getDescription(), START_STYLE, firstLocation);
|
||||
printWriter.println("<Placemark>");
|
||||
printWriter.println(
|
||||
"<description>" + StringUtils.stringAsCData(track.getDescription()) + "</description>");
|
||||
printWriter.println("<name>" + StringUtils.stringAsCData(track.getName()) + "</name>");
|
||||
printWriter.println("<styleUrl>#" + TRACK_STYLE + "</styleUrl>");
|
||||
printWriter.println("<gx:MultiTrack>");
|
||||
printWriter.println("<altitudeMode>absolute</altitudeMode>");
|
||||
printWriter.println("<gx:interpolate>1</gx:interpolate>");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeEndTrack(Location lastLocation) {
|
||||
if (printWriter != null) {
|
||||
printWriter.println("</gx:MultiTrack>");
|
||||
printWriter.println("</Placemark>");
|
||||
String name = context.getString(R.string.marker_label_end, track.getName());
|
||||
String description = descriptionGenerator.generateTrackDescription(track, null, null);
|
||||
writePlacemark(name, description, END_STYLE, lastLocation);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeOpenSegment() {
|
||||
if (printWriter != null) {
|
||||
printWriter.println("<gx:Track>");
|
||||
hasPower = false;
|
||||
hasCadence = false;
|
||||
hasHeartRate = false;
|
||||
hasBatteryLevel = false;
|
||||
powerList.clear();
|
||||
cadenceList.clear();
|
||||
heartRateList.clear();
|
||||
batteryLevelList.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeCloseSegment() {
|
||||
if (printWriter != null) {
|
||||
printWriter.println("<ExtendedData>");
|
||||
printWriter.println("<SchemaData schemaUrl=\"#" + SCHEMA_ID + "\">");
|
||||
if (hasPower) {
|
||||
writeSensorData(powerList, POWER);
|
||||
}
|
||||
if (hasCadence) {
|
||||
writeSensorData(cadenceList, CADENCE);
|
||||
}
|
||||
if (hasHeartRate) {
|
||||
writeSensorData(heartRateList, HEART_RATE);
|
||||
}
|
||||
if (hasBatteryLevel) {
|
||||
writeSensorData(batteryLevelList, BATTER_LEVEL);
|
||||
}
|
||||
printWriter.println("</SchemaData>");
|
||||
printWriter.println("</ExtendedData>");
|
||||
printWriter.println("</gx:Track>");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeLocation(Location location) {
|
||||
if (printWriter != null) {
|
||||
printWriter.println(
|
||||
"<when>" + FileUtils.FILE_TIMESTAMP_FORMAT.format(location.getTime()) + "</when>");
|
||||
printWriter.println(
|
||||
"<gx:coord>" + location.getLongitude() + " " + location.getLatitude() + " "
|
||||
+ location.getAltitude() + "</gx:coord>");
|
||||
if (location instanceof MyTracksLocation) {
|
||||
SensorDataSet sensorDataSet = ((MyTracksLocation) location).getSensorDataSet();
|
||||
int power = -1;
|
||||
int cadence = -1;
|
||||
int heartRate = -1;
|
||||
int batteryLevel = -1;
|
||||
|
||||
if (sensorDataSet != null) {
|
||||
if (sensorDataSet.hasPower()) {
|
||||
SensorData sensorData = sensorDataSet.getPower();
|
||||
if (sensorData.hasValue() && sensorData.getState() == Sensor.SensorState.SENDING) {
|
||||
hasPower = true;
|
||||
power = sensorData.getValue();
|
||||
}
|
||||
}
|
||||
if (sensorDataSet.hasCadence()) {
|
||||
SensorData sensorData = sensorDataSet.getCadence();
|
||||
if (sensorData.hasValue() && sensorData.getState() == Sensor.SensorState.SENDING) {
|
||||
hasCadence = true;
|
||||
cadence = sensorData.getValue();
|
||||
}
|
||||
}
|
||||
if (sensorDataSet.hasHeartRate()) {
|
||||
SensorData sensorData = sensorDataSet.getHeartRate();
|
||||
if (sensorData.hasValue() && sensorData.getState() == Sensor.SensorState.SENDING) {
|
||||
hasHeartRate = true;
|
||||
heartRate = sensorData.getValue();
|
||||
}
|
||||
}
|
||||
if (sensorDataSet.hasBatteryLevel()) {
|
||||
SensorData sensorData = sensorDataSet.getBatteryLevel();
|
||||
if (sensorData.hasValue() && sensorData.getState() == Sensor.SensorState.SENDING) {
|
||||
hasBatteryLevel = true;
|
||||
batteryLevel = sensorData.getValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
powerList.add(power);
|
||||
cadenceList.add(cadence);
|
||||
heartRateList.add(heartRate);
|
||||
batteryLevelList.add(batteryLevel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the sensor data.
|
||||
*
|
||||
* @param list a list of sensor data
|
||||
* @param name the name of the sensor data
|
||||
*/
|
||||
private void writeSensorData(ArrayList<Integer> list, String name) {
|
||||
printWriter.println("<gx:SimpleArrayData name=\"" + name + "\">");
|
||||
for (int i = 0; i < list.size(); i++) {
|
||||
printWriter.println("<gx:value>" + list.get(i) + "</gx:value>");
|
||||
}
|
||||
printWriter.println("</gx:SimpleArrayData>");
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a placemark.
|
||||
*
|
||||
* @param name the name of the placemark
|
||||
* @param description the description
|
||||
* @param styleName the style name
|
||||
* @param location the location
|
||||
*/
|
||||
private void writePlacemark(
|
||||
String name, String description, String styleName, Location location) {
|
||||
if (location != null) {
|
||||
printWriter.println("<Placemark>");
|
||||
printWriter.println(
|
||||
"<description>" + StringUtils.stringAsCData(description) + "</description>");
|
||||
printWriter.println("<name>" + StringUtils.stringAsCData(name) + "</name>");
|
||||
printWriter.println("<styleUrl>#" + styleName + "</styleUrl>");
|
||||
printWriter.println("<Point>");
|
||||
printWriter.println(
|
||||
"<coordinates>" + location.getLongitude() + "," + location.getLatitude() + ","
|
||||
+ location.getAltitude() + "</coordinates>");
|
||||
printWriter.println("</Point>");
|
||||
printWriter.println("</Placemark>");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the track style.
|
||||
*/
|
||||
private void writeTrackStyle() {
|
||||
printWriter.println("<Style id=\"" + TRACK_STYLE + "\"><LineStyle>");
|
||||
printWriter.println("<color>7f0000ff</color><width>4</width>");
|
||||
printWriter.println("</LineStyle></Style>");
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a placemarker style.
|
||||
*
|
||||
* @param name the name of the style
|
||||
* @param url the url of the style icon
|
||||
* @param x the x position of the hotspot
|
||||
* @param y the y position of the hotspot
|
||||
*/
|
||||
private void writePlacemarkerStyle(String name, String url, int x, int y) {
|
||||
printWriter.println("<Style id=\"" + name + "\"><IconStyle>");
|
||||
printWriter.println("<scale>1.3</scale>");
|
||||
printWriter.println("<Icon><href>" + url + "</href></Icon>");
|
||||
printWriter.println(
|
||||
"<hotSpot x=\"" + x + "\" y=\"" + y + "\" xunits=\"pixels\" yunits=\"pixels\"/>");
|
||||
printWriter.println("</IconStyle></Style>");
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a sensor style.
|
||||
*
|
||||
* @param name the name of the sesnor
|
||||
* @param displayName the sensor display name
|
||||
*/
|
||||
private void writeSensorStyle(String name, String displayName) {
|
||||
printWriter.println("<gx:SimpleArrayField name=\"" + name + "\" type=\"int\">");
|
||||
printWriter.println(
|
||||
"<displayName>" + StringUtils.stringAsCData(displayName) + "</displayName>");
|
||||
printWriter.println("</gx:SimpleArrayField>");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
/*
|
||||
* 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.PlayTrackUtils;
|
||||
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_FILE_FORMAT = "file_format";
|
||||
public static final String EXTRA_SHARE_FILE = "share_file";
|
||||
public static final String EXTRA_PLAY_FILE = "play_file";
|
||||
|
||||
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 boolean playFile;
|
||||
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.track_action_save).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);
|
||||
playFile = intent.getBooleanExtra(EXTRA_PLAY_FILE, false);
|
||||
|
||||
writer = TrackWriterFactory.newWriter(this, providerUtils, trackId, format);
|
||||
if (writer == null) {
|
||||
Log.e(TAG, "Unable to build writer");
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
if (shareFile || playFile) {
|
||||
// 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();
|
||||
} if (playFile) {
|
||||
playWrittenFile();
|
||||
} 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.share_track_subject).toString());
|
||||
shareIntent.putExtra(Intent.EXTRA_TEXT,
|
||||
getResources().getText(R.string.share_track_file_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_picker_title).toString()));
|
||||
}
|
||||
|
||||
private void playWrittenFile() {
|
||||
if (!writer.wasSuccess()) {
|
||||
showResultDialog();
|
||||
return;
|
||||
}
|
||||
|
||||
Uri uri = Uri.fromFile(new File(writer.getAbsolutePath()));
|
||||
Intent intent = new Intent()
|
||||
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
.setDataAndType(uri, PlayTrackUtils.KML_MIME_TYPE);
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
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.generic_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.generic_success_title : R.string.generic_error_title);
|
||||
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.track_action_save));
|
||||
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,357 @@
|
||||
/*
|
||||
* 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.lib.R;
|
||||
import com.google.android.apps.mytracks.util.FileUtils;
|
||||
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.util.Date;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* 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 Rttsches
|
||||
*/
|
||||
public class TcxTrackWriter implements TrackFormatWriter {
|
||||
|
||||
// "Biking" related string IDs
|
||||
private static final int TCX_SPORT_BIKING_IDS[] = {
|
||||
R.string.activity_type_cycling,
|
||||
R.string.activity_type_dirt_bike,
|
||||
R.string.activity_type_mountain_biking,
|
||||
R.string.activity_type_road_biking,
|
||||
R.string.activity_type_track_cycling,
|
||||
};
|
||||
|
||||
// "Running" related string IDs
|
||||
private static final int TCX_SPORT_RUNNING_IDS[] = {
|
||||
R.string.activity_type_running,
|
||||
R.string.activity_type_speed_walking,
|
||||
R.string.activity_type_street_running,
|
||||
R.string.activity_type_track_running,
|
||||
R.string.activity_type_trail_running,
|
||||
R.string.activity_type_walking,
|
||||
};
|
||||
|
||||
// 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 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;
|
||||
}
|
||||
|
||||
@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 = FileUtils.FILE_TIMESTAMP_FORMAT.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>" + FileUtils.FILE_TIMESTAMP_FORMAT.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 writeBeginWaypoints() {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeEndWaypoints() {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
@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();
|
||||
|
||||
// category is possibly localized here, so compare it to localized string resources
|
||||
for (int i : TCX_SPORT_RUNNING_IDS) {
|
||||
if (category.equalsIgnoreCase(context.getResources().getString(i))) {
|
||||
return TCX_SPORT_RUNNING;
|
||||
}
|
||||
}
|
||||
for (int i : TCX_SPORT_BIKING_IDS) {
|
||||
if (category.equalsIgnoreCase(context.getResources().getString(i))) {
|
||||
return TCX_SPORT_BIKING;
|
||||
}
|
||||
}
|
||||
|
||||
// for tracks without localized activity type
|
||||
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,126 @@
|
||||
/*
|
||||
* 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 a track to file.
|
||||
*
|
||||
* The expected sequence of calls is:
|
||||
* <ol>
|
||||
* <li>{@link #prepare}
|
||||
* <li>{@link #writeHeader}
|
||||
* <li>{@link #writeBeginWaypoints}
|
||||
* <li>For each waypoint: {@link #writeWaypoint}
|
||||
* <li>{@link #writeEndWaypoints}
|
||||
* <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>{@link #writeFooter}
|
||||
* <li>{@link #close}
|
||||
* </ol>
|
||||
*
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
public interface TrackFormatWriter {
|
||||
|
||||
/**
|
||||
* Gets the file extension (i.e. gpx, kml, ...)
|
||||
*/
|
||||
public String getExtension();
|
||||
|
||||
/**
|
||||
* Sets up the writer to write the given track.
|
||||
*
|
||||
* @param track the track to write
|
||||
* @param outputStream the output stream to write the track to
|
||||
*/
|
||||
public void prepare(Track track, OutputStream outputStream);
|
||||
|
||||
/**
|
||||
* Closes the underlying file handler.
|
||||
*/
|
||||
public void close();
|
||||
|
||||
/**
|
||||
* Writes the header.
|
||||
*/
|
||||
public void writeHeader();
|
||||
|
||||
/**
|
||||
* Writes the footer.
|
||||
*/
|
||||
public void writeFooter();
|
||||
|
||||
/**
|
||||
* Writes the beginning of the waypoints.
|
||||
*/
|
||||
public void writeBeginWaypoints();
|
||||
|
||||
/**
|
||||
* Writes the end of the waypoints.
|
||||
*/
|
||||
public void writeEndWaypoints();
|
||||
|
||||
/**
|
||||
* Writes a waypoint.
|
||||
*
|
||||
* @param waypoint the waypoint
|
||||
*/
|
||||
public void writeWaypoint(Waypoint waypoint);
|
||||
|
||||
/**
|
||||
* Writes the beginning of the track.
|
||||
*
|
||||
* @param firstLocation the first location
|
||||
*/
|
||||
public void writeBeginTrack(Location firstLocation);
|
||||
|
||||
/**
|
||||
* Writes the end of the track.
|
||||
*
|
||||
* @param lastLocation the last location
|
||||
*/
|
||||
public void writeEndTrack(Location lastLocation);
|
||||
|
||||
/**
|
||||
* Writes the statements necessary to open a new segment.
|
||||
*/
|
||||
public void writeOpenSegment();
|
||||
|
||||
/**
|
||||
* Writes the statements necessary to close a segment.
|
||||
*/
|
||||
public void writeCloseSegment();
|
||||
|
||||
/**
|
||||
* Writes a location.
|
||||
*
|
||||
* @param location the location
|
||||
*/
|
||||
public void writeLocation(Location location);
|
||||
}
|
||||
@@ -0,0 +1,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(context);
|
||||
}
|
||||
},
|
||||
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,399 @@
|
||||
/*
|
||||
* 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.sd_card_error_write_file;
|
||||
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.sd_card_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.sd_card_error_write_file;
|
||||
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.sd_card_error_no_storage;
|
||||
return false;
|
||||
}
|
||||
if (!fileUtils.ensureDirectoryExists(directory)) {
|
||||
Log.i(Constants.TAG, "Could not create export directory.");
|
||||
errorMessage = R.string.sd_card_error_create_dir;
|
||||
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);
|
||||
boolean hasWaypoints = false;
|
||||
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()) {
|
||||
if (!hasWaypoints) {
|
||||
writer.writeBeginWaypoints();
|
||||
hasWaypoints = true;
|
||||
}
|
||||
Waypoint wpt = providerUtils.createWaypoint(cursor);
|
||||
writer.writeWaypoint(wpt);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
if (hasWaypoints) {
|
||||
writer.writeEndWaypoints();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.sd_card_success_write_file;
|
||||
}
|
||||
|
||||
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.generic_progress_title));
|
||||
dialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
|
||||
dialog.setMessage(activity.getString(R.string.sd_card_progress_write_file));
|
||||
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));
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright 2012 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package com.google.android.apps.mytracks.io.fusiontables;
|
||||
|
||||
import com.google.android.apps.mytracks.io.docs.SendDocsActivity;
|
||||
import com.google.android.apps.mytracks.io.sendtogoogle.AbstractSendActivity;
|
||||
import com.google.android.apps.mytracks.io.sendtogoogle.AbstractSendAsyncTask;
|
||||
import com.google.android.apps.mytracks.io.sendtogoogle.SendRequest;
|
||||
import com.google.android.apps.mytracks.io.sendtogoogle.UploadResultActivity;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.content.Intent;
|
||||
|
||||
/**
|
||||
* An activity to send a track to Google Fusion Tables.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class SendFusionTablesActivity extends AbstractSendActivity {
|
||||
|
||||
@Override
|
||||
protected AbstractSendAsyncTask createAsyncTask() {
|
||||
return new SendFusionTablesAsyncTask(this, sendRequest.getTrackId(), sendRequest.getAccount());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getServiceName() {
|
||||
return getString(R.string.send_google_fusion_tables);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void startNextActivity(boolean success, boolean isCancel) {
|
||||
sendRequest.setFusionTablesSuccess(success);
|
||||
|
||||
Class<?> next;
|
||||
if (isCancel) {
|
||||
next = UploadResultActivity.class;
|
||||
} else {
|
||||
if (sendRequest.isSendDocs()) {
|
||||
next = SendDocsActivity.class;
|
||||
} else {
|
||||
next = UploadResultActivity.class;
|
||||
}
|
||||
}
|
||||
Intent intent = new Intent(this, next).putExtra(SendRequest.SEND_REQUEST_KEY, sendRequest);
|
||||
startActivity(intent);
|
||||
finish();
|
||||
}
|
||||
}
|
||||
+463
@@ -0,0 +1,463 @@
|
||||
// Copyright 2012 Google Inc. All Rights Reserved.
|
||||
|
||||
package com.google.android.apps.mytracks.io.fusiontables;
|
||||
|
||||
import com.google.android.apps.mytracks.Constants;
|
||||
import com.google.android.apps.mytracks.content.DescriptionGenerator;
|
||||
import com.google.android.apps.mytracks.content.DescriptionGeneratorImpl;
|
||||
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
|
||||
import com.google.android.apps.mytracks.content.Track;
|
||||
import com.google.android.apps.mytracks.content.Waypoint;
|
||||
import com.google.android.apps.mytracks.io.sendtogoogle.AbstractSendAsyncTask;
|
||||
import com.google.android.apps.mytracks.io.sendtogoogle.SendToGoogleUtils;
|
||||
import com.google.android.apps.mytracks.stats.DoubleBuffer;
|
||||
import com.google.android.apps.mytracks.stats.TripStatisticsBuilder;
|
||||
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
|
||||
import com.google.android.apps.mytracks.util.LocationUtils;
|
||||
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.HttpTransport;
|
||||
import com.google.api.client.http.InputStreamContent;
|
||||
import com.google.api.client.util.Strings;
|
||||
|
||||
import android.accounts.Account;
|
||||
import android.accounts.AccountManager;
|
||||
import android.accounts.AuthenticatorException;
|
||||
import android.accounts.OperationCanceledException;
|
||||
import android.content.Context;
|
||||
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.io.InputStream;
|
||||
import java.net.URLEncoder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Vector;
|
||||
|
||||
/**
|
||||
* AsyncTask to send a track to Google Fusion Tables.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class SendFusionTablesAsyncTask extends AbstractSendAsyncTask {
|
||||
|
||||
private static final String APP_NAME_PREFIX = "Google-MyTracks-";
|
||||
private static final String SQL_KEY = "sql=";
|
||||
private static final String CONTENT_TYPE = "application/x-www-form-urlencoded";
|
||||
private static final String FUSION_TABLES_BASE_URL =
|
||||
"https://www.google.com/fusiontables/api/query";
|
||||
private static final int MAX_POINTS_PER_UPLOAD = 2048;
|
||||
private static final String GDATA_VERSION = "2";
|
||||
|
||||
private static final int PROGRESS_CREATE_TABLE = 0;
|
||||
private static final int PROGRESS_UNLIST_TABLE = 5;
|
||||
private static final int PROGRESS_UPLOAD_DATA_MIN = 10;
|
||||
private static final int PROGRESS_UPLOAD_DATA_MAX = 90;
|
||||
private static final int PROGRESS_UPLOAD_WAYPOINTS = 95;
|
||||
private static final int PROGRESS_COMPLETE = 100;
|
||||
|
||||
// See http://support.google.com/fusiontables/bin/answer.py?hl=en&answer=185991
|
||||
private static final String MARKER_TYPE_START = "large_green";
|
||||
private static final String MARKER_TYPE_END = "large_red";
|
||||
private static final String MARKER_TYPE_WAYPOINT = "large_yellow";
|
||||
|
||||
private static final String TAG = SendFusionTablesAsyncTask.class.getSimpleName();
|
||||
|
||||
private final Context context;
|
||||
private final long trackId;
|
||||
private final Account account;
|
||||
private final MyTracksProviderUtils myTracksProviderUtils;
|
||||
private final HttpRequestFactory httpRequestFactory;
|
||||
|
||||
// The following variables are for per upload states
|
||||
private String authToken;
|
||||
private String tableId;
|
||||
int currentSegment;
|
||||
|
||||
public SendFusionTablesAsyncTask(
|
||||
SendFusionTablesActivity activity, long trackId, Account account) {
|
||||
super(activity);
|
||||
this.trackId = trackId;
|
||||
this.account = account;
|
||||
|
||||
context = activity.getApplicationContext();
|
||||
myTracksProviderUtils = MyTracksProviderUtils.Factory.get(context);
|
||||
HttpTransport transport = ApiAdapterFactory.getApiAdapter().getHttpTransport();
|
||||
httpRequestFactory = transport.createRequestFactory(new MethodOverride());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void closeConnection() {
|
||||
// No action needed for Google Fusion Tables
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void saveResult() {
|
||||
Track track = myTracksProviderUtils.getTrack(trackId);
|
||||
if (track != null) {
|
||||
track.setTableId(tableId);
|
||||
myTracksProviderUtils.updateTrack(track);
|
||||
} else {
|
||||
Log.d(TAG, "No track");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean performTask() {
|
||||
// Reset the per upload states
|
||||
authToken = null;
|
||||
tableId = null;
|
||||
currentSegment = 1;
|
||||
|
||||
try {
|
||||
authToken = AccountManager.get(context).blockingGetAuthToken(
|
||||
account, SendFusionTablesUtils.SERVICE, false);
|
||||
} catch (OperationCanceledException e) {
|
||||
Log.d(TAG, "Unable to get auth token", e);
|
||||
return retryTask();
|
||||
} catch (AuthenticatorException e) {
|
||||
Log.d(TAG, "Unable to get auth token", e);
|
||||
return retryTask();
|
||||
} catch (IOException e) {
|
||||
Log.d(TAG, "Unable to get auth token", e);
|
||||
return retryTask();
|
||||
}
|
||||
|
||||
Track track = myTracksProviderUtils.getTrack(trackId);
|
||||
if (track == null) {
|
||||
Log.d(TAG, "Track is null");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create a new table
|
||||
publishProgress(PROGRESS_CREATE_TABLE);
|
||||
if (!createNewTable(track)) {
|
||||
// Retry upload in case the auth token is invalid
|
||||
return retryTask();
|
||||
}
|
||||
|
||||
// Unlist table
|
||||
publishProgress(PROGRESS_UNLIST_TABLE);
|
||||
if (!unlistTable()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Upload all the track points plus the start and end markers
|
||||
publishProgress(PROGRESS_UPLOAD_DATA_MIN);
|
||||
if (!uploadAllTrackPoints(track)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Upload all the waypoints
|
||||
publishProgress(PROGRESS_UPLOAD_WAYPOINTS);
|
||||
if (!uploadWaypoints()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
publishProgress(PROGRESS_COMPLETE);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void invalidateToken() {
|
||||
AccountManager.get(context).invalidateAuthToken(Constants.ACCOUNT_TYPE, authToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new table.
|
||||
*
|
||||
* @param track the track
|
||||
* @return true if success.
|
||||
*/
|
||||
private boolean createNewTable(Track track) {
|
||||
String query = "CREATE TABLE '" + SendFusionTablesUtils.escapeSqlString(track.getName())
|
||||
+ "' (name:STRING,description:STRING,geometry:LOCATION,marker:STRING)";
|
||||
return sendQuery(query, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlists a table.
|
||||
*
|
||||
* @return true if success.
|
||||
*/
|
||||
private boolean unlistTable() {
|
||||
String query = "UPDATE TABLE " + tableId + " SET VISIBILITY = UNLISTED";
|
||||
return sendQuery(query, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads all the points in a track.
|
||||
*
|
||||
* @param track the track
|
||||
* @return true if success.
|
||||
*/
|
||||
private boolean uploadAllTrackPoints(Track track) {
|
||||
Cursor locationsCursor = null;
|
||||
try {
|
||||
SharedPreferences prefs = context.getSharedPreferences(
|
||||
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
|
||||
boolean metricUnits = prefs.getBoolean(context.getString(R.string.metric_units_key), true);
|
||||
|
||||
locationsCursor = myTracksProviderUtils.getLocationsCursor(trackId, 0, -1, false);
|
||||
if (locationsCursor == null) {
|
||||
Log.d(TAG, "Location cursor is null");
|
||||
return false;
|
||||
}
|
||||
int locationsCount = locationsCursor.getCount();
|
||||
List<Location> locations = new ArrayList<Location>(MAX_POINTS_PER_UPLOAD);
|
||||
Location lastLocation = null;
|
||||
|
||||
// For chart server, limit the number of elevation readings to 250.
|
||||
int elevationSamplingFrequency = Math.max(1, (int) (locationsCount / 250.0));
|
||||
TripStatisticsBuilder tripStatisticsBuilder = new TripStatisticsBuilder(
|
||||
track.getStatistics().getStartTime());
|
||||
DoubleBuffer elevationBuffer = new DoubleBuffer(Constants.ELEVATION_SMOOTHING_FACTOR);
|
||||
Vector<Double> distances = new Vector<Double>();
|
||||
Vector<Double> elevations = new Vector<Double>();
|
||||
|
||||
for (int i = 0; i < locationsCount; i++) {
|
||||
locationsCursor.moveToPosition(i);
|
||||
|
||||
Location location = myTracksProviderUtils.createLocation(locationsCursor);
|
||||
locations.add(location);
|
||||
|
||||
if (i == 0) {
|
||||
// Create a start marker
|
||||
String name = context.getString(R.string.marker_label_start, track.getName());
|
||||
if (!createNewPoint(name, "", location, MARKER_TYPE_START)) {
|
||||
Log.d(TAG, "Unable to create the start marker");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Add to the distances and elevations vectors
|
||||
if (LocationUtils.isValidLocation(location)) {
|
||||
tripStatisticsBuilder.addLocation(location, location.getTime());
|
||||
// All points go into the smoothing buffer
|
||||
elevationBuffer.setNext(metricUnits ? location.getAltitude()
|
||||
: location.getAltitude() * UnitConversions.M_TO_FT);
|
||||
if (i % elevationSamplingFrequency == 0) {
|
||||
distances.add(tripStatisticsBuilder.getStatistics().getTotalDistance());
|
||||
elevations.add(elevationBuffer.getAverage());
|
||||
}
|
||||
lastLocation = location;
|
||||
}
|
||||
|
||||
// Upload periodically
|
||||
int readCount = i + 1;
|
||||
if (readCount % MAX_POINTS_PER_UPLOAD == 0) {
|
||||
if (!prepareAndUploadPoints(track, locations, false)) {
|
||||
Log.d(TAG, "Unable to upload points");
|
||||
return false;
|
||||
}
|
||||
updateProgress(readCount, locationsCount);
|
||||
locations.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// Do a final upload with the remaining locations
|
||||
if (!prepareAndUploadPoints(track, locations, true)) {
|
||||
Log.d(TAG, "Unable to upload points");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create an end marker
|
||||
if (lastLocation != null) {
|
||||
distances.add(tripStatisticsBuilder.getStatistics().getTotalDistance());
|
||||
elevations.add(elevationBuffer.getAverage());
|
||||
DescriptionGenerator descriptionGenerator = new DescriptionGeneratorImpl(context);
|
||||
track.setDescription("<p>" + track.getDescription() + "</p><p>"
|
||||
+ descriptionGenerator.generateTrackDescription(track, distances, elevations) + "</p>");
|
||||
String name = context.getString(R.string.marker_label_end, track.getName());
|
||||
if (!createNewPoint(name, track.getDescription(), lastLocation, MARKER_TYPE_END)) {
|
||||
Log.d(TAG, "Unable to create the end marker");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
} finally {
|
||||
if (locationsCursor != null) {
|
||||
locationsCursor.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares and uploads a list of locations from a track.
|
||||
*
|
||||
* @param track the track
|
||||
* @param locations the locations from the track
|
||||
* @param lastBatch true if it is the last batch of locations
|
||||
*/
|
||||
private boolean prepareAndUploadPoints(Track track, List<Location> locations, boolean lastBatch) {
|
||||
// Prepare locations
|
||||
ArrayList<Track> splitTracks = SendToGoogleUtils.prepareLocations(track, locations);
|
||||
|
||||
// Upload segments
|
||||
boolean onlyOneSegment = lastBatch && currentSegment == 1 && splitTracks.size() == 1;
|
||||
for (Track splitTrack : splitTracks) {
|
||||
if (!onlyOneSegment) {
|
||||
splitTrack.setName(context.getString(
|
||||
R.string.send_google_track_part_label, splitTrack.getName(), currentSegment));
|
||||
}
|
||||
if (!createNewLineString(splitTrack)) {
|
||||
Log.d(TAG, "Upload points failed");
|
||||
return false;
|
||||
}
|
||||
currentSegment++;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads all the waypoints.
|
||||
*
|
||||
* @return true if success.
|
||||
*/
|
||||
private boolean uploadWaypoints() {
|
||||
Cursor cursor = null;
|
||||
try {
|
||||
cursor = myTracksProviderUtils.getWaypointsCursor(
|
||||
trackId, 0, Constants.MAX_LOADED_WAYPOINTS_POINTS);
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
// This will skip the first waypoint (it carries the stats for the
|
||||
// track).
|
||||
while (cursor.moveToNext()) {
|
||||
Waypoint wpt = myTracksProviderUtils.createWaypoint(cursor);
|
||||
if (!createNewPoint(
|
||||
wpt.getName(), wpt.getDescription(), wpt.getLocation(), MARKER_TYPE_WAYPOINT)) {
|
||||
Log.d(TAG, "Upload waypoints failed");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new row in Google Fusion Tables representing a marker as a
|
||||
* point.
|
||||
*
|
||||
* @param name the marker name
|
||||
* @param description the marker description
|
||||
* @param location the marker location
|
||||
* @param type the marker type
|
||||
* @return true if success.
|
||||
*/
|
||||
private boolean createNewPoint(
|
||||
String name, String description, Location location, String type) {
|
||||
String query = "INSERT INTO " + tableId + " (name,description,geometry,marker) VALUES "
|
||||
+ SendFusionTablesUtils.formatSqlValues(
|
||||
name, description, SendFusionTablesUtils.getKmlPoint(location), type);
|
||||
return sendQuery(query, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new row in Google Fusion Tables representing the track as a
|
||||
* line segment.
|
||||
*
|
||||
* @param track the track
|
||||
* @return true if success.
|
||||
*/
|
||||
private boolean createNewLineString(Track track) {
|
||||
String query = "INSERT INTO " + tableId + " (name,description,geometry) VALUES "
|
||||
+ SendFusionTablesUtils.formatSqlValues(track.getName(), track.getDescription(),
|
||||
SendFusionTablesUtils.getKmlLineString(track.getLocations()));
|
||||
return sendQuery(query, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a query to Google Fusion Tables.
|
||||
*
|
||||
* @param query the Fusion Tables SQL query
|
||||
* @param setTableId true to set the table id
|
||||
* @return true if success.
|
||||
*/
|
||||
private boolean sendQuery(String query, boolean setTableId) {
|
||||
Log.d(TAG, "SendQuery: " + query);
|
||||
|
||||
if (isCancelled()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
GenericUrl url = new GenericUrl(FUSION_TABLES_BASE_URL);
|
||||
String sql = SQL_KEY + URLEncoder.encode(query);
|
||||
ByteArrayInputStream inputStream = new ByteArrayInputStream(Strings.toBytesUtf8(sql));
|
||||
InputStreamContent inputStreamContent = new InputStreamContent(null, inputStream);
|
||||
HttpRequest request;
|
||||
try {
|
||||
request = httpRequestFactory.buildPostRequest(url, inputStreamContent);
|
||||
} catch (IOException e) {
|
||||
Log.d(TAG, "Unable to build request", e);
|
||||
return false;
|
||||
}
|
||||
|
||||
GoogleHeaders headers = new GoogleHeaders();
|
||||
headers.setApplicationName(APP_NAME_PREFIX + SystemUtils.getMyTracksVersion(context));
|
||||
headers.gdataVersion = GDATA_VERSION;
|
||||
headers.setGoogleLogin(authToken);
|
||||
headers.setContentType(CONTENT_TYPE);
|
||||
request.setHeaders(headers);
|
||||
|
||||
HttpResponse response;
|
||||
try {
|
||||
response = request.execute();
|
||||
} catch (IOException e) {
|
||||
Log.d(TAG, "Unable to execute request", e);
|
||||
return false;
|
||||
}
|
||||
boolean isSuccess = response.isSuccessStatusCode();
|
||||
if (isSuccess) {
|
||||
InputStream content;
|
||||
try {
|
||||
content = response.getContent();
|
||||
} catch (IOException e) {
|
||||
Log.d(TAG, "Unable to get response", e);
|
||||
return false;
|
||||
}
|
||||
if (setTableId) {
|
||||
tableId = SendFusionTablesUtils.getTableId(content);
|
||||
if (tableId == null) {
|
||||
Log.d(TAG, "tableId is null");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.d(TAG,
|
||||
"sendQuery failed: " + response.getStatusMessage() + ": " + response.getStatusCode());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the progress based on the number of locations uploaded.
|
||||
*
|
||||
* @param uploaded the number of uploaded locations
|
||||
* @param total the number of total locations
|
||||
*/
|
||||
private void updateProgress(int uploaded, int total) {
|
||||
double totalPercentage = uploaded / total;
|
||||
double scaledPercentage = totalPercentage
|
||||
* (PROGRESS_UPLOAD_DATA_MAX - PROGRESS_UPLOAD_DATA_MIN) + PROGRESS_UPLOAD_DATA_MIN;
|
||||
publishProgress((int) scaledPercentage);
|
||||
}
|
||||
}
|
||||
|
||||
+196
@@ -0,0 +1,196 @@
|
||||
/*
|
||||
* Copyright 2012 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package com.google.android.apps.mytracks.io.fusiontables;
|
||||
|
||||
import com.google.android.apps.mytracks.content.Track;
|
||||
import com.google.android.apps.mytracks.stats.TripStatistics;
|
||||
import com.google.api.client.util.Strings;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
|
||||
import android.location.Location;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Utilities for sending a track to Google Fusion Tables.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class SendFusionTablesUtils {
|
||||
|
||||
public static final String SERVICE = "fusiontables";
|
||||
private static final String UTF8 = "UTF8";
|
||||
private static final String TABLE_ID = "tableid";
|
||||
private static final String MAP_URL = "https://www.google.com/fusiontables/embedviz?"
|
||||
+ "viz=MAP&q=select+col0,+col1,+col2,+col3+from+%s+&h=false&lat=%f&lng=%f&z=%d&t=1&l=col2";
|
||||
private static final String TAG = SendFusionTablesUtils.class.getSimpleName();
|
||||
|
||||
private SendFusionTablesUtils() {}
|
||||
|
||||
/**
|
||||
* Gets the url to visualize a fusion table on a map.
|
||||
*
|
||||
* @param track the track
|
||||
* @return the url.
|
||||
*/
|
||||
public static String getMapUrl(Track track) {
|
||||
if (track == null || track.getStatistics() == null || track.getTableId() == null) {
|
||||
Log.e(TAG, "Invalid track");
|
||||
return null;
|
||||
}
|
||||
|
||||
// TODO(jshih): Determine the 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, MAP_URL, track.getTableId(), latE6 / 1.E6, lonE6 / 1.E6, z);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats an array of values as a SQL VALUES like
|
||||
* ('value1','value2',...,'value_n'). Escapes single quotes with two single
|
||||
* quotes.
|
||||
*
|
||||
* @param values an array of values to format
|
||||
* @return the formated SQL VALUES.
|
||||
*/
|
||||
public static String formatSqlValues(String... values) {
|
||||
StringBuilder builder = new StringBuilder("(");
|
||||
for (int i = 0; i < values.length; i++) {
|
||||
if (i > 0) {
|
||||
builder.append(',');
|
||||
}
|
||||
builder.append('\'');
|
||||
builder.append(escapeSqlString(values[i]));
|
||||
builder.append('\'');
|
||||
}
|
||||
builder.append(")");
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes a SQL string. Escapes single quotes with two single quotes.
|
||||
*
|
||||
* @param string the string
|
||||
* @return the escaped string.
|
||||
*/
|
||||
public static String escapeSqlString(String string) {
|
||||
return string.replaceAll("'", "''");
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a KML Point value representing a location.
|
||||
*
|
||||
* @param location the location
|
||||
* @return the KML Point value.
|
||||
*/
|
||||
public static String getKmlPoint(Location location) {
|
||||
StringBuilder builder = new StringBuilder("<Point><coordinates>");
|
||||
if (location != null) {
|
||||
appendLocation(location, builder);
|
||||
}
|
||||
builder.append("</coordinates></Point>");
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a KML LineString value representing an array of locations.
|
||||
*
|
||||
* @param locations the locations.
|
||||
* @return the KML LineString value.
|
||||
*/
|
||||
public static String getKmlLineString(ArrayList<Location> locations) {
|
||||
StringBuilder builder = new StringBuilder("<LineString><coordinates>");
|
||||
if (locations != null) {
|
||||
for (int i = 0; i < locations.size(); i++) {
|
||||
if (i != 0) {
|
||||
builder.append(' ');
|
||||
}
|
||||
appendLocation(locations.get(i), builder);
|
||||
}
|
||||
}
|
||||
builder.append("</coordinates></LineString>");
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends a location to a string builder using "longitude,latitude[,altitude]" format.
|
||||
*
|
||||
* @param location the location
|
||||
* @param builder the string builder
|
||||
*/
|
||||
@VisibleForTesting
|
||||
static void appendLocation(Location location, StringBuilder builder) {
|
||||
builder.append(location.getLongitude()).append(",").append(location.getLatitude());
|
||||
if (location.hasAltitude()) {
|
||||
builder.append(",");
|
||||
builder.append(location.getAltitude());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the table id from an input streawm.
|
||||
*
|
||||
* @param inputStream input stream
|
||||
* @return table id or null if not available.
|
||||
*/
|
||||
public static String getTableId(InputStream inputStream) {
|
||||
if (inputStream == null) {
|
||||
Log.d(TAG, "inputStream is null");
|
||||
return null;
|
||||
}
|
||||
byte[] result = new byte[1024];
|
||||
int read;
|
||||
try {
|
||||
read = inputStream.read(result);
|
||||
} catch (IOException e) {
|
||||
Log.d(TAG, "Unable to read result", e);
|
||||
return null;
|
||||
}
|
||||
if (read == -1) {
|
||||
Log.d(TAG, "no data read");
|
||||
return null;
|
||||
}
|
||||
String s;
|
||||
try {
|
||||
s = new String(result, 0, read, UTF8);
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
Log.d(TAG, "Unable to parse result", e);
|
||||
return null;
|
||||
}
|
||||
|
||||
String[] lines = s.split(Strings.LINE_SEPARATOR);
|
||||
if (lines.length > 1 && lines[0].equals(TABLE_ID)) {
|
||||
// returns the next line
|
||||
return lines[1];
|
||||
} else {
|
||||
Log.d(TAG, "Response is not valid: " + s);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,459 @@
|
||||
/*
|
||||
* Copyright 2010 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package com.google.android.apps.mytracks.io.gdata;
|
||||
|
||||
import com.google.wireless.gdata.client.GDataClient;
|
||||
import com.google.wireless.gdata.client.HttpException;
|
||||
import com.google.wireless.gdata.client.QueryParams;
|
||||
import com.google.wireless.gdata.data.StringUtils;
|
||||
import com.google.wireless.gdata.parser.ParseException;
|
||||
import com.google.wireless.gdata.serializer.GDataSerializer;
|
||||
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URLEncoder;
|
||||
import java.util.zip.GZIPInputStream;
|
||||
|
||||
import org.apache.http.Header;
|
||||
import org.apache.http.HttpEntity;
|
||||
import org.apache.http.HttpResponse;
|
||||
import org.apache.http.StatusLine;
|
||||
import org.apache.http.client.HttpClient;
|
||||
import org.apache.http.client.methods.HttpGet;
|
||||
import org.apache.http.client.methods.HttpPost;
|
||||
import org.apache.http.client.methods.HttpUriRequest;
|
||||
import org.apache.http.entity.AbstractHttpEntity;
|
||||
import org.apache.http.entity.ByteArrayEntity;
|
||||
import org.apache.http.entity.InputStreamEntity;
|
||||
import org.apache.http.impl.client.DefaultHttpClient;
|
||||
|
||||
/**
|
||||
* Implementation of a GDataClient using GoogleHttpClient to make HTTP requests.
|
||||
* Always issues GETs and POSTs, using the X-HTTP-Method-Override header when a
|
||||
* PUT or DELETE is desired, to avoid issues with firewalls, etc., that do not
|
||||
* allow methods other than GET or POST.
|
||||
*/
|
||||
public class AndroidGDataClient implements GDataClient {
|
||||
|
||||
private static final String TAG = "GDataClient";
|
||||
private static final boolean DEBUG = false;
|
||||
|
||||
private static final String X_HTTP_METHOD_OVERRIDE = "X-HTTP-Method-Override";
|
||||
|
||||
private static final int MAX_REDIRECTS = 10;
|
||||
|
||||
private final HttpClient httpClient;
|
||||
|
||||
/**
|
||||
* Interface for creating HTTP requests. Used by
|
||||
* {@link AndroidGDataClient#createAndExecuteMethod}, since HttpUriRequest
|
||||
* does not allow for changing the URI after creation, e.g., when you want to
|
||||
* follow a redirect.
|
||||
*/
|
||||
private interface HttpRequestCreator {
|
||||
HttpUriRequest createRequest(URI uri);
|
||||
}
|
||||
|
||||
private static class GetRequestCreator implements HttpRequestCreator {
|
||||
public HttpUriRequest createRequest(URI uri) {
|
||||
return new HttpGet(uri);
|
||||
}
|
||||
}
|
||||
|
||||
private static class PostRequestCreator implements HttpRequestCreator {
|
||||
private final String mMethodOverride;
|
||||
private final HttpEntity mEntity;
|
||||
|
||||
public PostRequestCreator(String methodOverride, HttpEntity entity) {
|
||||
mMethodOverride = methodOverride;
|
||||
mEntity = entity;
|
||||
}
|
||||
|
||||
public HttpUriRequest createRequest(URI uri) {
|
||||
HttpPost post = new HttpPost(uri);
|
||||
if (mMethodOverride != null) {
|
||||
post.addHeader(X_HTTP_METHOD_OVERRIDE, mMethodOverride);
|
||||
}
|
||||
post.setEntity(mEntity);
|
||||
return post;
|
||||
}
|
||||
}
|
||||
|
||||
// MAJOR TODO: make this work across redirects (if we can reset the
|
||||
// InputStream).
|
||||
// OR, read the bits into a local buffer (yuck, the media could be large).
|
||||
private static class MediaPutRequestCreator implements HttpRequestCreator {
|
||||
private final InputStream mMediaInputStream;
|
||||
private final String mContentType;
|
||||
|
||||
public MediaPutRequestCreator(InputStream mediaInputStream,
|
||||
String contentType) {
|
||||
mMediaInputStream = mediaInputStream;
|
||||
mContentType = contentType;
|
||||
}
|
||||
|
||||
public HttpUriRequest createRequest(URI uri) {
|
||||
HttpPost post = new HttpPost(uri);
|
||||
post.addHeader(X_HTTP_METHOD_OVERRIDE, "PUT");
|
||||
InputStreamEntity entity =
|
||||
new InputStreamEntity(mMediaInputStream, -1 /* read until EOF */);
|
||||
entity.setContentType(mContentType);
|
||||
post.setEntity(entity);
|
||||
return post;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new AndroidGDataClient.
|
||||
*/
|
||||
public AndroidGDataClient() {
|
||||
httpClient = new DefaultHttpClient();
|
||||
}
|
||||
|
||||
public void close() {
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
*
|
||||
* @see GDataClient#encodeUri(java.lang.String)
|
||||
*/
|
||||
public String encodeUri(String uri) {
|
||||
String encodedUri;
|
||||
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 (DEBUG) {
|
||||
for (Header h : request.getAllHeaders()) {
|
||||
Log.v(TAG, h.getName() + ": " + h.getValue());
|
||||
}
|
||||
Log.d(TAG, "Executing " + request.getRequestLine().toString());
|
||||
}
|
||||
|
||||
response = null;
|
||||
|
||||
try {
|
||||
response = httpClient.execute(request);
|
||||
} catch (IOException ioe) {
|
||||
Log.w(TAG, "Unable to execute HTTP request." + ioe);
|
||||
throw ioe;
|
||||
}
|
||||
|
||||
StatusLine statusLine = response.getStatusLine();
|
||||
if (statusLine == null) {
|
||||
Log.w(TAG, "StatusLine is null.");
|
||||
throw new NullPointerException(
|
||||
"StatusLine is null -- should not happen.");
|
||||
}
|
||||
|
||||
if (Log.isLoggable(TAG, Log.DEBUG)) {
|
||||
Log.d(TAG, response.getStatusLine().toString());
|
||||
for (Header h : response.getAllHeaders()) {
|
||||
Log.d(TAG, h.getName() + ": " + h.getValue());
|
||||
}
|
||||
}
|
||||
status = statusLine.getStatusCode();
|
||||
|
||||
HttpEntity entity = response.getEntity();
|
||||
|
||||
if ((status >= 200) && (status < 300) && entity != null) {
|
||||
return getUngzippedContent(entity);
|
||||
}
|
||||
|
||||
// TODO: handle 301, 307?
|
||||
// TODO: let the http client handle the redirects, if we can be sure we'll
|
||||
// never get a
|
||||
// redirect on POST.
|
||||
if (status == 302) {
|
||||
// consume the content, so the connection can be closed.
|
||||
entity.consumeContent();
|
||||
Header location = response.getFirstHeader("Location");
|
||||
if (location == null) {
|
||||
if (Log.isLoggable(TAG, Log.DEBUG)) {
|
||||
Log.d(TAG, "Redirect requested but no Location " + "specified.");
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (Log.isLoggable(TAG, Log.DEBUG)) {
|
||||
Log.d(TAG, "Following redirect to " + location.getValue());
|
||||
}
|
||||
try {
|
||||
uri = new URI(location.getValue());
|
||||
} catch (URISyntaxException use) {
|
||||
if (Log.isLoggable(TAG, Log.DEBUG)) {
|
||||
Log.d(TAG, "Unable to parse " + location.getValue() + " as URI.",
|
||||
use);
|
||||
throw new IOException("Unable to parse " + location.getValue()
|
||||
+ " as URI.");
|
||||
}
|
||||
break;
|
||||
}
|
||||
--redirectsLeft;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (Log.isLoggable(TAG, Log.VERBOSE)) {
|
||||
Log.v(TAG, "Received " + status + " status code.");
|
||||
}
|
||||
String errorMessage = null;
|
||||
HttpEntity entity = response.getEntity();
|
||||
try {
|
||||
if (entity != null) {
|
||||
InputStream in = entity.getContent();
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
byte[] buf = new byte[8192];
|
||||
int bytesRead = -1;
|
||||
while ((bytesRead = in.read(buf)) != -1) {
|
||||
baos.write(buf, 0, bytesRead);
|
||||
}
|
||||
// TODO: use appropriate encoding, picked up from Content-Type.
|
||||
errorMessage = new String(baos.toByteArray());
|
||||
if (Log.isLoggable(TAG, Log.VERBOSE)) {
|
||||
Log.v(TAG, errorMessage);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (entity != null) {
|
||||
entity.consumeContent();
|
||||
}
|
||||
}
|
||||
String exceptionMessage = "Received " + status + " status code";
|
||||
if (errorMessage != null) {
|
||||
exceptionMessage += (": " + errorMessage);
|
||||
}
|
||||
throw new HttpException(exceptionMessage, status, null /* InputStream */);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the input stream from a response entity. If the entity is gzipped
|
||||
* then this will get a stream over the uncompressed data.
|
||||
*
|
||||
* @param entity the entity whose content should be read
|
||||
* @return the input stream to read from
|
||||
* @throws IOException
|
||||
*/
|
||||
private static InputStream getUngzippedContent(HttpEntity entity)
|
||||
throws IOException {
|
||||
InputStream responseStream = entity.getContent();
|
||||
if (responseStream == null) {
|
||||
return responseStream;
|
||||
}
|
||||
Header header = entity.getContentEncoding();
|
||||
if (header == null) {
|
||||
return responseStream;
|
||||
}
|
||||
String contentEncoding = header.getValue();
|
||||
if (contentEncoding == null) {
|
||||
return responseStream;
|
||||
}
|
||||
if (contentEncoding.contains("gzip")){
|
||||
responseStream = new GZIPInputStream(responseStream);
|
||||
}
|
||||
return responseStream;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
*
|
||||
* @see GDataClient#getFeedAsStream(java.lang.String, java.lang.String)
|
||||
*/
|
||||
public InputStream getFeedAsStream(String feedUrl, String authToken)
|
||||
throws HttpException, IOException {
|
||||
|
||||
InputStream in =
|
||||
createAndExecuteMethod(new GetRequestCreator(), feedUrl, authToken);
|
||||
if (in != null) {
|
||||
return in;
|
||||
}
|
||||
throw new IOException("Unable to access feed.");
|
||||
}
|
||||
|
||||
public InputStream getMediaEntryAsStream(String mediaEntryUrl,
|
||||
String authToken) throws HttpException, IOException {
|
||||
|
||||
InputStream in =
|
||||
createAndExecuteMethod(new GetRequestCreator(), mediaEntryUrl,
|
||||
authToken);
|
||||
|
||||
if (in != null) {
|
||||
return in;
|
||||
}
|
||||
throw new IOException("Unable to access media entry.");
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
*
|
||||
* @see GDataClient#createEntry
|
||||
*/
|
||||
public InputStream createEntry(String feedUrl, String authToken,
|
||||
GDataSerializer entry) throws HttpException, IOException {
|
||||
|
||||
HttpEntity entity =
|
||||
createEntityForEntry(entry, GDataSerializer.FORMAT_CREATE);
|
||||
InputStream in =
|
||||
createAndExecuteMethod(new PostRequestCreator(null /* override */,
|
||||
entity), feedUrl, authToken);
|
||||
if (in != null) {
|
||||
return in;
|
||||
}
|
||||
throw new IOException("Unable to create entry.");
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
*
|
||||
* @see GDataClient#updateEntry
|
||||
*/
|
||||
public InputStream updateEntry(String editUri, String authToken,
|
||||
GDataSerializer entry) throws HttpException, IOException {
|
||||
HttpEntity entity =
|
||||
createEntityForEntry(entry, GDataSerializer.FORMAT_UPDATE);
|
||||
InputStream in =
|
||||
createAndExecuteMethod(new PostRequestCreator("PUT", entity), editUri,
|
||||
authToken);
|
||||
if (in != null) {
|
||||
return in;
|
||||
}
|
||||
throw new IOException("Unable to update entry.");
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
*
|
||||
* @see GDataClient#deleteEntry
|
||||
*/
|
||||
public void deleteEntry(String editUri, String authToken)
|
||||
throws HttpException, IOException {
|
||||
if (StringUtils.isEmpty(editUri)) {
|
||||
throw new IllegalArgumentException(
|
||||
"you must specify an non-empty edit url");
|
||||
}
|
||||
InputStream in =
|
||||
createAndExecuteMethod(
|
||||
new PostRequestCreator("DELETE", null /* entity */), editUri,
|
||||
authToken);
|
||||
if (in == null) {
|
||||
throw new IOException("Unable to delete entry.");
|
||||
}
|
||||
try {
|
||||
in.close();
|
||||
} catch (IOException ioe) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
public InputStream updateMediaEntry(String editUri, String authToken,
|
||||
InputStream mediaEntryInputStream, String contentType)
|
||||
throws HttpException, IOException {
|
||||
InputStream in =
|
||||
createAndExecuteMethod(new MediaPutRequestCreator(
|
||||
mediaEntryInputStream, contentType), editUri, authToken);
|
||||
if (in != null) {
|
||||
return in;
|
||||
}
|
||||
throw new IOException("Unable to write media entry.");
|
||||
}
|
||||
|
||||
private HttpEntity createEntityForEntry(GDataSerializer entry, int format)
|
||||
throws IOException {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
try {
|
||||
entry.serialize(baos, format);
|
||||
} catch (IOException ioe) {
|
||||
Log.e(TAG, "Unable to serialize entry.", ioe);
|
||||
throw ioe;
|
||||
} catch (ParseException pe) {
|
||||
Log.e(TAG, "Unable to serialize entry.", pe);
|
||||
throw new IOException("Unable to serialize entry: " + pe.getMessage());
|
||||
}
|
||||
|
||||
byte[] entryBytes = baos.toByteArray();
|
||||
|
||||
if (entryBytes != null && Log.isLoggable(TAG, Log.DEBUG)) {
|
||||
try {
|
||||
Log.d(TAG, "Serialized entry: " + new String(entryBytes, "UTF-8"));
|
||||
} catch (UnsupportedEncodingException uee) {
|
||||
// should not happen
|
||||
throw new IllegalStateException("UTF-8 should be supported!", uee);
|
||||
}
|
||||
}
|
||||
|
||||
AbstractHttpEntity entity = new ByteArrayEntity(entryBytes);
|
||||
entity.setContentType(entry.getContentType());
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright 2010 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package com.google.android.apps.mytracks.io.gdata;
|
||||
|
||||
import com.google.android.apps.mytracks.Constants;
|
||||
import com.google.wireless.gdata.client.GDataClient;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* This factory will fetch the right class for the platform.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class GDataClientFactory {
|
||||
|
||||
private GDataClientFactory() { }
|
||||
|
||||
/**
|
||||
* Creates a new GData client.
|
||||
* This factory will fetch the right class for the platform.
|
||||
* @return A GDataClient appropriate for this platform
|
||||
*/
|
||||
public static GDataClient getGDataClient(Context context) {
|
||||
// TODO This should be moved into ApiAdapter
|
||||
try {
|
||||
// Try to use the official unbundled gdata client implementation.
|
||||
// This should work on Froyo and beyond.
|
||||
return new com.google.android.common.gdata.AndroidGDataClient(context);
|
||||
} catch (LinkageError e) {
|
||||
// On all other platforms use the client implementation packaged in the
|
||||
// apk.
|
||||
Log.i(Constants.TAG, "Using mytracks AndroidGDataClient.", e);
|
||||
return new com.google.android.apps.mytracks.io.gdata.AndroidGDataClient();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,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,46 @@
|
||||
/*
|
||||
* 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.gdata.docs;
|
||||
|
||||
import com.google.wireless.gdata.client.GDataClient;
|
||||
import com.google.wireless.gdata.client.GDataParserFactory;
|
||||
import com.google.wireless.gdata.client.GDataServiceClient;
|
||||
|
||||
/**
|
||||
* GDataServiceClient for accessing Google Documents. This is not a full
|
||||
* implementation.
|
||||
*/
|
||||
public class DocumentsClient extends GDataServiceClient {
|
||||
/** The name of the service, dictated to be 'wise' by the protocol. */
|
||||
public static final String SERVICE = "writely";
|
||||
|
||||
/**
|
||||
* Creates a new DocumentsClient.
|
||||
*
|
||||
* @param client The GDataClient that should be used to authenticate requests,
|
||||
* retrieve feeds, etc
|
||||
* @param parserFactory The GDataParserFactory that should be used to obtain
|
||||
* GDataParsers used by this client
|
||||
*/
|
||||
public DocumentsClient(GDataClient client, GDataParserFactory parserFactory) {
|
||||
super(client, parserFactory);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getServiceName() {
|
||||
return SERVICE;
|
||||
}
|
||||
}
|
||||
+269
@@ -0,0 +1,269 @@
|
||||
/*
|
||||
* Copyright 2007 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, 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.docs;
|
||||
|
||||
import com.google.wireless.gdata.client.GDataClient;
|
||||
import com.google.wireless.gdata.client.GDataParserFactory;
|
||||
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.data.StringUtils;
|
||||
import com.google.wireless.gdata.parser.GDataParser;
|
||||
import com.google.wireless.gdata.parser.ParseException;
|
||||
import com.google.wireless.gdata.serializer.GDataSerializer;
|
||||
import com.google.wireless.gdata2.client.AuthenticationException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* GDataServiceClient for accessing Google Spreadsheets. This client can access
|
||||
* and parse all of the Spreadsheets feed types: Spreadsheets feed, Worksheets
|
||||
* feed, List feed, and Cells feed. Read operations are supported on all feed
|
||||
* types, but only the List and Cells feeds support write operations. (This is a
|
||||
* limitation of the protocol, not this API. Such write access may be added to
|
||||
* the protocol in the future, requiring changes to this implementation.)
|
||||
*
|
||||
* Only 'private' visibility and 'full' projections are currently supported.
|
||||
*/
|
||||
public class SpreadsheetsClient extends GDataServiceClient {
|
||||
/** The name of the service, dictated to be 'wise' by the protocol. */
|
||||
public static final String SERVICE = "wise";
|
||||
|
||||
/** Standard base feed url for spreadsheets. */
|
||||
public static final String SPREADSHEETS_BASE_FEED_URL =
|
||||
"http://spreadsheets.google.com/feeds/spreadsheets/private/full";
|
||||
|
||||
/**
|
||||
* Represents an entry in a GData Spreadsheets meta-feed.
|
||||
*/
|
||||
public static class SpreadsheetEntry extends Entry { }
|
||||
|
||||
/**
|
||||
* Represents an entry in a GData Worksheets meta-feed.
|
||||
*/
|
||||
public static class WorksheetEntry extends Entry { }
|
||||
|
||||
/**
|
||||
* Creates a new SpreadsheetsClient. Uses the standard base URL for
|
||||
* spreadsheets feeds.
|
||||
*
|
||||
* @param client The GDataClient that should be used to authenticate requests,
|
||||
* retrieve feeds, etc
|
||||
*/
|
||||
public SpreadsheetsClient(GDataClient client,
|
||||
GDataParserFactory spreadsheetFactory) {
|
||||
super(client, spreadsheetFactory);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getServiceName() {
|
||||
return SERVICE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a parser for the specified feed type.
|
||||
*
|
||||
* @param feedEntryClass the Class of entry type that will be parsed, which
|
||||
* lets this method figure out which parser to create
|
||||
* @param feedUri the URI of the feed to be fetched and parsed
|
||||
* @param authToken the current authToken to use for the request
|
||||
* @return a parser for the indicated feed
|
||||
* @throws AuthenticationException if the authToken is not valid
|
||||
* @throws ParseException if the response from the server could not be parsed
|
||||
*/
|
||||
private GDataParser getParserForTypedFeed(
|
||||
Class<? extends Entry> feedEntryClass, String feedUri, String authToken)
|
||||
throws AuthenticationException, ParseException, IOException {
|
||||
GDataClient gDataClient = getGDataClient();
|
||||
GDataParserFactory gDataParserFactory = getGDataParserFactory();
|
||||
|
||||
try {
|
||||
InputStream is = gDataClient.getFeedAsStream(feedUri, authToken);
|
||||
return gDataParserFactory.createParser(feedEntryClass, is);
|
||||
} catch (HttpException e) {
|
||||
convertHttpExceptionForReads("Could not fetch parser feed.", e);
|
||||
return null; // never reached
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an HTTP exception that happened while reading into the equivalent
|
||||
* local exception.
|
||||
*/
|
||||
public void convertHttpExceptionForReads(String message, HttpException cause)
|
||||
throws AuthenticationException, IOException {
|
||||
switch (cause.getStatusCode()) {
|
||||
case HttpException.SC_FORBIDDEN:
|
||||
case HttpException.SC_UNAUTHORIZED:
|
||||
throw new AuthenticationException(message, cause);
|
||||
case HttpException.SC_GONE:
|
||||
default:
|
||||
throw new IOException(message + ": " + cause.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Entry createEntry(String feedUri, String authToken, Entry entry)
|
||||
throws ParseException, IOException {
|
||||
|
||||
GDataParserFactory factory = getGDataParserFactory();
|
||||
GDataSerializer serializer = factory.createSerializer(entry);
|
||||
|
||||
InputStream is;
|
||||
try {
|
||||
is = getGDataClient().createEntry(feedUri, authToken, serializer);
|
||||
} catch (HttpException e) {
|
||||
convertHttpExceptionForWrites(entry.getClass(),
|
||||
"Could not update entry.", e);
|
||||
return null; // never reached.
|
||||
}
|
||||
|
||||
GDataParser parser = factory.createParser(entry.getClass(), is);
|
||||
try {
|
||||
return parser.parseStandaloneEntry();
|
||||
} finally {
|
||||
parser.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a GDataParser for the indicated feed. The parser can be used to
|
||||
* access the contents of URI. WARNING: because we cannot reliably infer the
|
||||
* feed type from the URI alone, this method assumes the default feed type!
|
||||
* This is probably NOT what you want. Please use the getParserFor[Type]Feed
|
||||
* methods.
|
||||
*
|
||||
* @param feedEntryClass
|
||||
* @param feedUri the URI of the feed to be fetched and parsed
|
||||
* @param authToken the current authToken to use for the request
|
||||
* @return a parser for the indicated feed
|
||||
* @throws ParseException if the response from the server could not be parsed
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public GDataParser getParserForFeed(
|
||||
Class feedEntryClass, String feedUri, String authToken)
|
||||
throws ParseException, IOException {
|
||||
try {
|
||||
return getParserForTypedFeed(SpreadsheetEntry.class, feedUri, authToken);
|
||||
} catch (AuthenticationException e) {
|
||||
throw new IOException("Authentication Failure: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a parser for a Worksheets meta-feed.
|
||||
*
|
||||
* @param feedUri the URI of the feed to be fetched and parsed
|
||||
* @param authToken the current authToken to use for the request
|
||||
* @return a parser for the indicated feed
|
||||
* @throws AuthenticationException if the authToken is not valid
|
||||
* @throws ParseException if the response from the server could not be parsed
|
||||
*/
|
||||
public GDataParser getParserForWorksheetsFeed(
|
||||
String feedUri, String authToken)
|
||||
throws AuthenticationException, ParseException, IOException {
|
||||
return getParserForTypedFeed(WorksheetEntry.class, feedUri, authToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an entry. The URI to be updated is taken from <code>entry</code>.
|
||||
* Note that only entries in List and Cells feeds can be updated, so
|
||||
* <code>entry</code> must be of the corresponding type; other types will
|
||||
* result in an exception.
|
||||
*
|
||||
* @param entry the entry to be updated; must include its URI
|
||||
* @param authToken the current authToken to be used for the operation
|
||||
* @return An Entry containing the re-parsed version of the entry returned by
|
||||
* the server in response to the update
|
||||
* @throws ParseException if the server returned an error, if the server's
|
||||
* response was unparseable (unlikely), or if <code>entry</code> is of
|
||||
* a read-only type
|
||||
* @throws IOException on network error
|
||||
*/
|
||||
@Override
|
||||
public Entry updateEntry(Entry entry, String authToken)
|
||||
throws ParseException, IOException {
|
||||
GDataParserFactory factory = getGDataParserFactory();
|
||||
GDataSerializer serializer = factory.createSerializer(entry);
|
||||
|
||||
String editUri = entry.getEditUri();
|
||||
if (StringUtils.isEmpty(editUri)) {
|
||||
throw new ParseException("No edit URI -- cannot update.");
|
||||
}
|
||||
|
||||
InputStream is;
|
||||
try {
|
||||
is = getGDataClient().updateEntry(editUri, authToken, serializer);
|
||||
} catch (HttpException e) {
|
||||
convertHttpExceptionForWrites(entry.getClass(),
|
||||
"Could not update entry.", e);
|
||||
return null; // never reached
|
||||
}
|
||||
|
||||
GDataParser parser = factory.createParser(entry.getClass(), is);
|
||||
try {
|
||||
return parser.parseStandaloneEntry();
|
||||
} finally {
|
||||
parser.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an HTTP exception which happened while writing to the equivalent
|
||||
* local exception.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private void convertHttpExceptionForWrites(
|
||||
Class entryClass, String message, HttpException cause)
|
||||
throws ParseException, IOException {
|
||||
switch (cause.getStatusCode()) {
|
||||
case HttpException.SC_CONFLICT:
|
||||
if (entryClass != null) {
|
||||
InputStream is = cause.getResponseStream();
|
||||
if (is != null) {
|
||||
parseEntry(entryClass, cause.getResponseStream());
|
||||
}
|
||||
}
|
||||
throw new IOException(message);
|
||||
case HttpException.SC_BAD_REQUEST:
|
||||
throw new ParseException(message + ": " + cause);
|
||||
case HttpException.SC_FORBIDDEN:
|
||||
case HttpException.SC_UNAUTHORIZED:
|
||||
throw new IOException(message);
|
||||
default:
|
||||
throw new IOException(message + ": " + cause.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses one entry from the input stream.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private Entry parseEntry(Class entryClass, InputStream is)
|
||||
throws ParseException, IOException {
|
||||
GDataParser parser = null;
|
||||
try {
|
||||
parser = getGDataParserFactory().createParser(entryClass, is);
|
||||
return parser.parseStandaloneEntry();
|
||||
} finally {
|
||||
if (parser != null) {
|
||||
parser.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* 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.docs;
|
||||
|
||||
import com.google.wireless.gdata.client.GDataParserFactory;
|
||||
import com.google.wireless.gdata.data.Entry;
|
||||
import com.google.wireless.gdata.parser.GDataParser;
|
||||
import com.google.wireless.gdata.parser.ParseException;
|
||||
import com.google.wireless.gdata.parser.xml.XmlGDataParser;
|
||||
import com.google.wireless.gdata.parser.xml.XmlParserFactory;
|
||||
import com.google.wireless.gdata.serializer.GDataSerializer;
|
||||
import com.google.wireless.gdata.serializer.xml.XmlEntryGDataSerializer;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
/**
|
||||
* Factory of Xml parsers for gdata maps data.
|
||||
*/
|
||||
public class XmlDocsGDataParserFactory implements GDataParserFactory {
|
||||
private XmlParserFactory xmlFactory;
|
||||
|
||||
public XmlDocsGDataParserFactory(XmlParserFactory xmlFactory) {
|
||||
this.xmlFactory = xmlFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GDataParser createParser(InputStream is) throws ParseException {
|
||||
try {
|
||||
return new XmlGDataParser(is, xmlFactory.createParser());
|
||||
} catch (XmlPullParserException e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public GDataParser createParser(Class cls, InputStream is)
|
||||
throws ParseException {
|
||||
try {
|
||||
return createParserForClass(is);
|
||||
} catch (XmlPullParserException e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private GDataParser createParserForClass(InputStream is)
|
||||
throws ParseException, XmlPullParserException {
|
||||
return new XmlGDataParser(is, xmlFactory.createParser());
|
||||
}
|
||||
|
||||
@Override
|
||||
public GDataSerializer createSerializer(Entry en) {
|
||||
return new XmlEntryGDataSerializer(xmlFactory, en);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// Copyright 2010 Google Inc. All Rights Reserved.
|
||||
package com.google.android.apps.mytracks.io.gdata.maps;
|
||||
|
||||
import com.google.wireless.gdata.data.Entry;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* GData entry for a map feature.
|
||||
*/
|
||||
public class MapFeatureEntry extends Entry {
|
||||
|
||||
private String mPrivacy = null;
|
||||
private Map<String, String> mAttributes = new HashMap<String, String>();
|
||||
|
||||
public void setPrivacy(String privacy) {
|
||||
mPrivacy = privacy;
|
||||
}
|
||||
|
||||
public String getPrivacy() {
|
||||
return mPrivacy;
|
||||
}
|
||||
|
||||
public void setAttribute(String name, String value) {
|
||||
mAttributes.put(name, value);
|
||||
}
|
||||
|
||||
public void removeAttribute(String name) {
|
||||
mAttributes.remove(name);
|
||||
}
|
||||
|
||||
public Map<String, String> getAllAttributes() {
|
||||
return mAttributes;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
// Copyright 2010 Google Inc. All Rights Reserved.
|
||||
package com.google.android.apps.mytracks.io.gdata.maps;
|
||||
|
||||
import com.google.wireless.gdata.client.GDataClient;
|
||||
import com.google.wireless.gdata.client.GDataParserFactory;
|
||||
import com.google.wireless.gdata.client.GDataServiceClient;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* Client to talk to Google Maps via GData.
|
||||
*/
|
||||
public class MapsClient extends GDataServiceClient {
|
||||
private static final boolean DEBUG = false;
|
||||
public static final boolean LOG_COMMUNICATION = false;
|
||||
|
||||
private static final String MAPS_BASE_FEED_URL =
|
||||
"http://maps.google.com/maps/feeds/";
|
||||
private static final String MAPS_MAP_FEED_PATH = "maps/default/full";
|
||||
private static final String MAPS_FEATURE_FEED_PATH_BEFORE_MAPID = "features/";
|
||||
private static final String MAPS_FEATURE_FEED_PATH_AFTER_MAPID = "/full";
|
||||
private static final String MAPS_VERSION_FEED_PATH_FORMAT =
|
||||
"%smaps/%s/versions/%s/full/%s";
|
||||
|
||||
private static final String MAP_ENTRY_ID_BEFORE_USER_ID = "maps/feeds/maps/";
|
||||
private static final String MAP_ENTRY_ID_BETWEEN_USER_ID_AND_MAP_ID = "/";
|
||||
private static final String V2_ONLY_PARAM = "?v=2.0";
|
||||
|
||||
public MapsClient(GDataClient dataClient,
|
||||
GDataParserFactory dataParserFactory) {
|
||||
super(dataClient, dataParserFactory);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getServiceName() {
|
||||
return MapsConstants.SERVICE_NAME;
|
||||
}
|
||||
|
||||
public static String buildMapUrl(String mapId) {
|
||||
return MapsConstants.MAPSHOP_BASE_URL + "?msa=0&msid=" + mapId;
|
||||
}
|
||||
|
||||
public static String getMapsFeed() {
|
||||
if (DEBUG) {
|
||||
Log.d("Maps Client", "Requesting map feed:");
|
||||
}
|
||||
return MAPS_BASE_FEED_URL + MAPS_MAP_FEED_PATH + V2_ONLY_PARAM;
|
||||
}
|
||||
|
||||
public static String getFeaturesFeed(String mapid) {
|
||||
StringBuilder feed = new StringBuilder();
|
||||
feed.append(MAPS_BASE_FEED_URL);
|
||||
feed.append(MAPS_FEATURE_FEED_PATH_BEFORE_MAPID);
|
||||
feed.append(mapid);
|
||||
feed.append(MAPS_FEATURE_FEED_PATH_AFTER_MAPID);
|
||||
feed.append(V2_ONLY_PARAM);
|
||||
return feed.toString();
|
||||
}
|
||||
|
||||
public static String getMapIdFromMapEntryId(String entryId) {
|
||||
String userId = null;
|
||||
String mapId = null;
|
||||
if (DEBUG) {
|
||||
Log.d("Maps GData Client", "Getting mapid from entry id: " + entryId);
|
||||
}
|
||||
int userIdStart =
|
||||
entryId.indexOf(MAP_ENTRY_ID_BEFORE_USER_ID)
|
||||
+ MAP_ENTRY_ID_BEFORE_USER_ID.length();
|
||||
int userIdEnd =
|
||||
entryId.indexOf(MAP_ENTRY_ID_BETWEEN_USER_ID_AND_MAP_ID, userIdStart);
|
||||
if (userIdStart >= 0 && userIdEnd < entryId.length()
|
||||
&& userIdStart <= userIdEnd) {
|
||||
userId = entryId.substring(userIdStart, userIdEnd);
|
||||
}
|
||||
int mapIdStart =
|
||||
entryId.indexOf(MAP_ENTRY_ID_BETWEEN_USER_ID_AND_MAP_ID, userIdEnd)
|
||||
+ MAP_ENTRY_ID_BETWEEN_USER_ID_AND_MAP_ID.length();
|
||||
if (mapIdStart >= 0 && mapIdStart < entryId.length()) {
|
||||
mapId = entryId.substring(mapIdStart);
|
||||
}
|
||||
if (userId == null) {
|
||||
userId = "";
|
||||
}
|
||||
if (mapId == null) {
|
||||
mapId = "";
|
||||
}
|
||||
if (DEBUG) {
|
||||
Log.d("Maps GData Client", "Got user id: " + userId);
|
||||
Log.d("Maps GData Client", "Got map id: " + mapId);
|
||||
}
|
||||
return userId + "." + mapId;
|
||||
}
|
||||
|
||||
public static String getVersionFeed(String versionUserId,
|
||||
String versionClient, String currentVersion) {
|
||||
return String.format(MAPS_VERSION_FEED_PATH_FORMAT,
|
||||
MAPS_BASE_FEED_URL, versionUserId,
|
||||
versionClient, currentVersion);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
// Copyright 2009 Google Inc. All Rights Reserved.
|
||||
package com.google.android.apps.mytracks.io.gdata.maps;
|
||||
|
||||
/**
|
||||
* Constants for Google Maps.
|
||||
*/
|
||||
public class MapsConstants {
|
||||
static final String MAPSHOP_BASE_URL =
|
||||
"https://maps.google.com/maps/ms";
|
||||
public static final String SERVICE_NAME = "local";
|
||||
|
||||
/**
|
||||
* Private constructor to prevent instantiation.
|
||||
*/
|
||||
private MapsConstants() { }
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
// Copyright 2009 Google Inc. All Rights Reserved.
|
||||
package com.google.android.apps.mytracks.io.gdata.maps;
|
||||
|
||||
import com.google.android.maps.GeoPoint;
|
||||
|
||||
import java.util.Random;
|
||||
import java.util.Vector;
|
||||
|
||||
/**
|
||||
* MapsFeature contains all of the data associated with a feature in Google
|
||||
* Maps, where a feature is a marker, line, or shape. Some of the data is stored
|
||||
* in a {@link MapsFeatureMetadata} object so that it can be more efficiently
|
||||
* transmitted to other activities.
|
||||
*/
|
||||
public class MapsFeature {
|
||||
|
||||
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 MapsFeatureMetadata featureInfo = new MapsFeatureMetadata();
|
||||
|
||||
private final Random random = new Random();
|
||||
|
||||
/**
|
||||
* Initializes a valid but empty feature. It will default to a
|
||||
* {@link #MARKER} with a blue placemark with a dot as an icon at the
|
||||
* location (0, 0).
|
||||
*/
|
||||
public MapsFeature() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new point to the end of this feature.
|
||||
*
|
||||
* @param 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.
|
||||
*/
|
||||
public void generateAndroidId() {
|
||||
long time = System.currentTimeMillis();
|
||||
int rand = random.nextInt(10000);
|
||||
androidId = time + "." + rand;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the current local id for this feature if one is available.
|
||||
*
|
||||
* @return The local id for this feature
|
||||
*/
|
||||
public String getAndroidId() {
|
||||
return androidId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the current (html) description of this feature. The description
|
||||
* is stored in the feature metadata.
|
||||
*
|
||||
* @return The description of this feature
|
||||
*/
|
||||
public String getDescription() {
|
||||
return featureInfo.getDescription();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the description of this feature. That description is stored in the
|
||||
* feature metadata.
|
||||
*
|
||||
* @param description The new description of this feature
|
||||
*/
|
||||
public void setDescription(String description) {
|
||||
featureInfo.setDescription(description);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,88 @@
|
||||
// Copyright 2009 Google Inc. All Rights Reserved.
|
||||
package com.google.android.apps.mytracks.io.gdata.maps;
|
||||
|
||||
|
||||
/**
|
||||
* Metadata about a maps feature.
|
||||
*/
|
||||
class MapsFeatureMetadata {
|
||||
|
||||
private static final String BLUE_DOT_URL =
|
||||
"http://maps.google.com/mapfiles/ms/micons/blue-dot.png";
|
||||
private static final int DEFAULT_COLOR = 0x800000FF;
|
||||
private static final int DEFAULT_FILL_COLOR = 0xC00000FF;
|
||||
|
||||
private String title;
|
||||
private String description;
|
||||
private int type;
|
||||
private int color;
|
||||
private int lineWidth;
|
||||
private int fillColor;
|
||||
private String iconUrl;
|
||||
|
||||
public MapsFeatureMetadata() {
|
||||
title = "";
|
||||
description = "";
|
||||
type = MapsFeature.MARKER;
|
||||
color = DEFAULT_COLOR;
|
||||
lineWidth = 5;
|
||||
fillColor = DEFAULT_FILL_COLOR;
|
||||
iconUrl = BLUE_DOT_URL;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public int getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setType(int type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public int getColor() {
|
||||
return color;
|
||||
}
|
||||
|
||||
public void setColor(int color) {
|
||||
this.color = color;
|
||||
}
|
||||
|
||||
public int getLineWidth() {
|
||||
return lineWidth;
|
||||
}
|
||||
|
||||
public void setLineWidth(int width) {
|
||||
lineWidth = width;
|
||||
}
|
||||
|
||||
public int getFillColor() {
|
||||
return fillColor;
|
||||
}
|
||||
|
||||
public void setFillColor(int color) {
|
||||
fillColor = color;
|
||||
}
|
||||
|
||||
public String getIconUrl() {
|
||||
return iconUrl;
|
||||
}
|
||||
|
||||
public void setIconUrl(String url) {
|
||||
iconUrl = url;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
// Copyright 2009 Google Inc. All Rights Reserved.
|
||||
package com.google.android.apps.mytracks.io.gdata.maps;
|
||||
|
||||
import com.google.wireless.gdata.data.Entry;
|
||||
import com.google.wireless.gdata.data.StringUtils;
|
||||
|
||||
import android.graphics.Color;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.StringWriter;
|
||||
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
import org.xmlpull.v1.XmlPullParserFactory;
|
||||
import org.xmlpull.v1.XmlSerializer;
|
||||
|
||||
/**
|
||||
* Converter from GData objects to Maps objects.
|
||||
*/
|
||||
public class MapsGDataConverter {
|
||||
|
||||
private final XmlSerializer xmlSerializer;
|
||||
|
||||
public MapsGDataConverter() throws XmlPullParserException {
|
||||
xmlSerializer = XmlPullParserFactory.newInstance().newSerializer();
|
||||
}
|
||||
|
||||
public static MapsMapMetadata getMapMetadataForEntry(
|
||||
MapFeatureEntry entry) {
|
||||
MapsMapMetadata metadata = new MapsMapMetadata();
|
||||
if ("public".equals(entry.getPrivacy())) {
|
||||
metadata.setSearchable(true);
|
||||
} else {
|
||||
metadata.setSearchable(false);
|
||||
}
|
||||
metadata.setTitle(entry.getTitle());
|
||||
metadata.setDescription(entry.getSummary());
|
||||
|
||||
String editUri = entry.getEditUri();
|
||||
if (editUri != null) {
|
||||
metadata.setGDataEditUri(editUri);
|
||||
}
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
public static String getMapidForEntry(Entry entry) {
|
||||
return MapsClient.getMapIdFromMapEntryId(entry.getId());
|
||||
}
|
||||
|
||||
public static Entry getMapEntryForMetadata(MapsMapMetadata metadata) {
|
||||
MapFeatureEntry entry = new MapFeatureEntry();
|
||||
entry.setEditUri(metadata.getGDataEditUri());
|
||||
entry.setTitle(metadata.getTitle());
|
||||
entry.setSummary(metadata.getDescription());
|
||||
entry.setPrivacy(metadata.getSearchable() ? "public" : "unlisted");
|
||||
entry.setAuthor("android");
|
||||
entry.setEmail("nobody@google.com");
|
||||
return entry;
|
||||
}
|
||||
|
||||
public MapFeatureEntry getEntryForFeature(MapsFeature feature) {
|
||||
MapFeatureEntry entry = new MapFeatureEntry();
|
||||
entry.setTitle(feature.getTitle());
|
||||
entry.setAuthor("android");
|
||||
entry.setEmail("nobody@google.com");
|
||||
entry.setCategoryScheme("http://schemas.google.com/g/2005#kind");
|
||||
entry.setCategory("http://schemas.google.com/g/2008#mapfeature");
|
||||
entry.setEditUri("");
|
||||
if (!StringUtils.isEmpty(feature.getAndroidId())) {
|
||||
entry.setAttribute("_androidId", feature.getAndroidId());
|
||||
}
|
||||
try {
|
||||
StringWriter writer = new StringWriter();
|
||||
xmlSerializer.setOutput(writer);
|
||||
xmlSerializer.startTag(null, "Placemark");
|
||||
xmlSerializer.attribute(null, "xmlns", "http://earth.google.com/kml/2.2");
|
||||
xmlSerializer.startTag(null, "Style");
|
||||
if (feature.getType() == MapsFeature.MARKER) {
|
||||
xmlSerializer.startTag(null, "IconStyle");
|
||||
xmlSerializer.startTag(null, "Icon");
|
||||
xmlSerializer.startTag(null, "href");
|
||||
xmlSerializer.text(feature.getIconUrl());
|
||||
xmlSerializer.endTag(null, "href");
|
||||
xmlSerializer.endTag(null, "Icon");
|
||||
xmlSerializer.endTag(null, "IconStyle");
|
||||
} else {
|
||||
xmlSerializer.startTag(null, "LineStyle");
|
||||
xmlSerializer.startTag(null, "color");
|
||||
int color = feature.getColor();
|
||||
// Reverse the color because KML is ABGR and Android is ARGB
|
||||
xmlSerializer.text(Integer.toHexString(
|
||||
Color.argb(Color.alpha(color), Color.blue(color),
|
||||
Color.green(color), Color.red(color))));
|
||||
xmlSerializer.endTag(null, "color");
|
||||
xmlSerializer.startTag(null, "width");
|
||||
xmlSerializer.text(Integer.toString(feature.getLineWidth()));
|
||||
xmlSerializer.endTag(null, "width");
|
||||
xmlSerializer.endTag(null, "LineStyle");
|
||||
|
||||
if (feature.getType() == MapsFeature.SHAPE) {
|
||||
xmlSerializer.startTag(null, "PolyStyle");
|
||||
xmlSerializer.startTag(null, "color");
|
||||
int fcolor = feature.getFillColor();
|
||||
// Reverse the color because KML is ABGR and Android is ARGB
|
||||
xmlSerializer.text(Integer.toHexString(Color.argb(Color.alpha(fcolor),
|
||||
Color.blue(fcolor), Color.green(fcolor), Color.red(fcolor))));
|
||||
xmlSerializer.endTag(null, "color");
|
||||
xmlSerializer.startTag(null, "fill");
|
||||
xmlSerializer.text("1");
|
||||
xmlSerializer.endTag(null, "fill");
|
||||
xmlSerializer.startTag(null, "outline");
|
||||
xmlSerializer.text("1");
|
||||
xmlSerializer.endTag(null, "outline");
|
||||
xmlSerializer.endTag(null, "PolyStyle");
|
||||
}
|
||||
}
|
||||
xmlSerializer.endTag(null, "Style");
|
||||
xmlSerializer.startTag(null, "name");
|
||||
xmlSerializer.text(feature.getTitle());
|
||||
xmlSerializer.endTag(null, "name");
|
||||
xmlSerializer.startTag(null, "description");
|
||||
xmlSerializer.cdsect(feature.getDescription());
|
||||
xmlSerializer.endTag(null, "description");
|
||||
StringBuilder pointBuilder = new StringBuilder();
|
||||
for (int i = 0; i < feature.getPointCount(); ++i) {
|
||||
if (i > 0) {
|
||||
pointBuilder.append('\n');
|
||||
}
|
||||
pointBuilder.append(feature.getPoint(i).getLongitudeE6() / 1e6);
|
||||
pointBuilder.append(',');
|
||||
pointBuilder.append(feature.getPoint(i).getLatitudeE6() / 1e6);
|
||||
pointBuilder.append(",0.000000");
|
||||
}
|
||||
String pointString = pointBuilder.toString();
|
||||
if (feature.getType() == MapsFeature.MARKER) {
|
||||
xmlSerializer.startTag(null, "Point");
|
||||
xmlSerializer.startTag(null, "coordinates");
|
||||
xmlSerializer.text(pointString);
|
||||
xmlSerializer.endTag(null, "coordinates");
|
||||
xmlSerializer.endTag(null, "Point");
|
||||
} else if (feature.getType() == MapsFeature.LINE) {
|
||||
xmlSerializer.startTag(null, "LineString");
|
||||
xmlSerializer.startTag(null, "tessellate");
|
||||
xmlSerializer.text("1");
|
||||
xmlSerializer.endTag(null, "tessellate");
|
||||
xmlSerializer.startTag(null, "coordinates");
|
||||
xmlSerializer.text(pointString);
|
||||
xmlSerializer.endTag(null, "coordinates");
|
||||
xmlSerializer.endTag(null, "LineString");
|
||||
} else {
|
||||
xmlSerializer.startTag(null, "Polygon");
|
||||
xmlSerializer.startTag(null, "outerBoundaryIs");
|
||||
xmlSerializer.startTag(null, "LinearRing");
|
||||
xmlSerializer.startTag(null, "tessellate");
|
||||
xmlSerializer.text("1");
|
||||
xmlSerializer.endTag(null, "tessellate");
|
||||
xmlSerializer.startTag(null, "coordinates");
|
||||
xmlSerializer.text(pointString + "\n"
|
||||
+ Double.toString(feature.getPoint(0).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,52 @@
|
||||
// Copyright 2009 Google Inc. All Rights Reserved.
|
||||
package com.google.android.apps.mytracks.io.gdata.maps;
|
||||
|
||||
/**
|
||||
* Metadata about a Google Maps map.
|
||||
*/
|
||||
public class MapsMapMetadata {
|
||||
|
||||
private String title;
|
||||
private String description;
|
||||
private String gdataEditUri;
|
||||
private boolean searchable;
|
||||
|
||||
public MapsMapMetadata() {
|
||||
title = "";
|
||||
description = "";
|
||||
gdataEditUri = "";
|
||||
searchable = false;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public boolean getSearchable() {
|
||||
return searchable;
|
||||
}
|
||||
|
||||
public void setSearchable(boolean searchable) {
|
||||
this.searchable = searchable;
|
||||
}
|
||||
|
||||
public String getGDataEditUri() {
|
||||
return gdataEditUri;
|
||||
}
|
||||
|
||||
public void setGDataEditUri(String editUri) {
|
||||
this.gdataEditUri = editUri;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
// Copyright 2009 Google Inc. All Rights Reserved.
|
||||
package com.google.android.apps.mytracks.io.gdata.maps;
|
||||
|
||||
import com.google.wireless.gdata.data.Entry;
|
||||
import com.google.wireless.gdata.data.Feed;
|
||||
import com.google.wireless.gdata.data.XmlUtils;
|
||||
import com.google.wireless.gdata.parser.ParseException;
|
||||
import com.google.wireless.gdata.parser.xml.XmlGDataParser;
|
||||
|
||||
import org.xmlpull.v1.XmlPullParser;
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* Parser for XML gdata maps data.
|
||||
*/
|
||||
class XmlMapsGDataParser extends XmlGDataParser {
|
||||
|
||||
public XmlMapsGDataParser(InputStream is, XmlPullParser xpp)
|
||||
throws ParseException {
|
||||
super(is, xpp);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Feed createFeed() {
|
||||
return new Feed();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Entry createEntry() {
|
||||
return new MapFeatureEntry();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handleExtraElementInFeed(Feed feed) {
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handleExtraLinkInEntry(
|
||||
String rel, String type, String href, Entry entry)
|
||||
throws XmlPullParserException, IOException {
|
||||
if (!(entry instanceof MapFeatureEntry)) {
|
||||
throw new IllegalArgumentException("Expected MapFeatureEntry!");
|
||||
}
|
||||
if (rel.endsWith("#view")) {
|
||||
return;
|
||||
}
|
||||
super.handleExtraLinkInEntry(rel, type, href, entry);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the current entry in the XML document. Assumes that the parser is
|
||||
* currently pointing just after an <entry>.
|
||||
*
|
||||
* @param plainEntry The entry that will be filled.
|
||||
* @throws XmlPullParserException Thrown if the XML cannot be parsed.
|
||||
* @throws IOException Thrown if the underlying inputstream cannot be read.
|
||||
*/
|
||||
@Override
|
||||
protected void handleEntry(Entry plainEntry)
|
||||
throws XmlPullParserException, IOException, ParseException {
|
||||
XmlPullParser parser = getParser();
|
||||
if (!(plainEntry instanceof MapFeatureEntry)) {
|
||||
throw new IllegalArgumentException("Expected MapFeatureEntry!");
|
||||
}
|
||||
MapFeatureEntry entry = (MapFeatureEntry) plainEntry;
|
||||
|
||||
int eventType = parser.getEventType();
|
||||
entry.setPrivacy("public");
|
||||
while (eventType != XmlPullParser.END_DOCUMENT) {
|
||||
switch (eventType) {
|
||||
case XmlPullParser.START_TAG:
|
||||
String name = parser.getName();
|
||||
if ("entry".equals(name)) {
|
||||
// stop parsing here.
|
||||
return;
|
||||
} else if ("id".equals(name)) {
|
||||
entry.setId(XmlUtils.extractChildText(parser));
|
||||
} else if ("title".equals(name)) {
|
||||
entry.setTitle(XmlUtils.extractChildText(parser));
|
||||
} else if ("link".equals(name)) {
|
||||
String rel = parser.getAttributeValue(null /* ns */, "rel");
|
||||
String type = parser.getAttributeValue(null /* ns */, "type");
|
||||
String href = parser.getAttributeValue(null /* ns */, "href");
|
||||
if ("edit".equals(rel)) {
|
||||
entry.setEditUri(href);
|
||||
} else if ("alternate".equals(rel) && "text/html".equals(type)) {
|
||||
entry.setHtmlUri(href);
|
||||
} else {
|
||||
handleExtraLinkInEntry(rel, type, href, entry);
|
||||
}
|
||||
} else if ("summary".equals(name)) {
|
||||
entry.setSummary(XmlUtils.extractChildText(parser));
|
||||
} else if ("content".equals(name)) {
|
||||
StringBuilder contentBuilder = new StringBuilder();
|
||||
int parentDepth = parser.getDepth();
|
||||
while (parser.getEventType() != XmlPullParser.END_DOCUMENT) {
|
||||
int etype = parser.next();
|
||||
switch (etype) {
|
||||
case XmlPullParser.START_TAG:
|
||||
contentBuilder.append('<');
|
||||
contentBuilder.append(parser.getName());
|
||||
contentBuilder.append('>');
|
||||
break;
|
||||
case XmlPullParser.TEXT:
|
||||
contentBuilder.append("<![CDATA[");
|
||||
contentBuilder.append(parser.getText());
|
||||
contentBuilder.append("]]>");
|
||||
break;
|
||||
case XmlPullParser.END_TAG:
|
||||
if (parser.getDepth() > parentDepth) {
|
||||
contentBuilder.append("</");
|
||||
contentBuilder.append(parser.getName());
|
||||
contentBuilder.append('>');
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (etype == XmlPullParser.END_TAG
|
||||
&& parser.getDepth() == parentDepth) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
entry.setContent(contentBuilder.toString());
|
||||
} else if ("category".equals(name)) {
|
||||
String category = parser.getAttributeValue(null /* ns */, "term");
|
||||
if (category != null && category.length() > 0) {
|
||||
entry.setCategory(category);
|
||||
}
|
||||
String categoryScheme =
|
||||
parser.getAttributeValue(null /* ns */, "scheme");
|
||||
if (categoryScheme != null && category.length() > 0) {
|
||||
entry.setCategoryScheme(categoryScheme);
|
||||
}
|
||||
} else if ("published".equals(name)) {
|
||||
entry.setPublicationDate(XmlUtils.extractChildText(parser));
|
||||
} else if ("updated".equals(name)) {
|
||||
entry.setUpdateDate(XmlUtils.extractChildText(parser));
|
||||
} else if ("deleted".equals(name)) {
|
||||
entry.setDeleted(true);
|
||||
} else if ("draft".equals(name)) {
|
||||
String draft = XmlUtils.extractChildText(parser);
|
||||
entry.setPrivacy("yes".equals(draft) ? "unlisted" : "public");
|
||||
} else if ("customProperty".equals(name)) {
|
||||
String attrName = parser.getAttributeValue(null, "name");
|
||||
String attrValue = XmlUtils.extractChildText(parser);
|
||||
entry.setAttribute(attrName, attrValue);
|
||||
} else if ("deleted".equals(name)) {
|
||||
entry.setDeleted(true);
|
||||
} else {
|
||||
handleExtraElementInEntry(entry);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
eventType = parser.next();
|
||||
}
|
||||
}
|
||||
}
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
// Copyright 2010 Google Inc. All Rights Reserved.
|
||||
package com.google.android.apps.mytracks.io.gdata.maps;
|
||||
|
||||
import com.google.wireless.gdata.client.GDataParserFactory;
|
||||
import com.google.wireless.gdata.data.Entry;
|
||||
import com.google.wireless.gdata.parser.GDataParser;
|
||||
import com.google.wireless.gdata.parser.ParseException;
|
||||
import com.google.wireless.gdata.parser.xml.XmlGDataParser;
|
||||
import com.google.wireless.gdata.parser.xml.XmlParserFactory;
|
||||
import com.google.wireless.gdata.serializer.GDataSerializer;
|
||||
import com.google.wireless.gdata.serializer.xml.XmlEntryGDataSerializer;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
/**
|
||||
* Factory of Xml parsers for gdata maps data.
|
||||
*/
|
||||
public class XmlMapsGDataParserFactory implements GDataParserFactory {
|
||||
private XmlParserFactory xmlFactory;
|
||||
|
||||
public XmlMapsGDataParserFactory(XmlParserFactory xmlFactory) {
|
||||
this.xmlFactory = xmlFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GDataParser createParser(InputStream is) throws ParseException {
|
||||
is = maybeLogCommunication(is);
|
||||
try {
|
||||
return new XmlGDataParser(is, xmlFactory.createParser());
|
||||
} catch (XmlPullParserException e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public GDataParser createParser(Class cls, InputStream is)
|
||||
throws ParseException {
|
||||
is = maybeLogCommunication(is);
|
||||
try {
|
||||
return createParserForClass(cls, is);
|
||||
} catch (XmlPullParserException e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private InputStream maybeLogCommunication(InputStream is)
|
||||
throws ParseException {
|
||||
if (MapsClient.LOG_COMMUNICATION) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
byte[] buffer = new byte[2048];
|
||||
try {
|
||||
for (int n = is.read(buffer); n >= 0; n = is.read(buffer)) {
|
||||
String part = new String(buffer, 0, n);
|
||||
builder.append(part);
|
||||
Log.d("Response part", part);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new ParseException("Could not read stream", e);
|
||||
}
|
||||
String whole = builder.toString();
|
||||
Log.d("Response", whole);
|
||||
is = new ByteArrayInputStream(whole.getBytes());
|
||||
}
|
||||
return is;
|
||||
}
|
||||
|
||||
private GDataParser createParserForClass(
|
||||
Class<? extends Entry> cls, InputStream is)
|
||||
throws ParseException, XmlPullParserException {
|
||||
if (cls == MapFeatureEntry.class) {
|
||||
return new XmlMapsGDataParser(is, xmlFactory.createParser());
|
||||
} else {
|
||||
return new XmlGDataParser(is, xmlFactory.createParser());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public GDataSerializer createSerializer(Entry en) {
|
||||
if (en instanceof MapFeatureEntry) {
|
||||
return new XmlMapsGDataSerializer(xmlFactory, (MapFeatureEntry) en);
|
||||
} else {
|
||||
return new XmlEntryGDataSerializer(xmlFactory, en);
|
||||
}
|
||||
}
|
||||
}
|
||||
+262
@@ -0,0 +1,262 @@
|
||||
// Copyright 2010 Google Inc. All Rights Reserved.
|
||||
package com.google.android.apps.mytracks.io.gdata.maps;
|
||||
|
||||
import com.google.wireless.gdata.data.StringUtils;
|
||||
import com.google.wireless.gdata.parser.ParseException;
|
||||
import com.google.wireless.gdata.parser.xml.XmlGDataParser;
|
||||
import com.google.wireless.gdata.parser.xml.XmlParserFactory;
|
||||
import com.google.wireless.gdata.serializer.xml.XmlEntryGDataSerializer;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.util.Map;
|
||||
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
import org.xmlpull.v1.XmlSerializer;
|
||||
|
||||
/**
|
||||
* Serializer of maps data for GData.
|
||||
*/
|
||||
class XmlMapsGDataSerializer extends XmlEntryGDataSerializer {
|
||||
|
||||
private static final String APP_NAMESPACE = "http://www.w3.org/2007/app";
|
||||
|
||||
private MapFeatureEntry entry;
|
||||
private XmlParserFactory factory;
|
||||
private OutputStream stream;
|
||||
|
||||
public XmlMapsGDataSerializer(XmlParserFactory factory, MapFeatureEntry entry) {
|
||||
super(factory, entry);
|
||||
|
||||
this.factory = factory;
|
||||
this.entry = entry;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void serialize(OutputStream out, int format)
|
||||
throws IOException, ParseException {
|
||||
XmlSerializer serializer = null;
|
||||
try {
|
||||
serializer = factory.createSerializer();
|
||||
} catch (XmlPullParserException e) {
|
||||
throw new ParseException("Unable to create XmlSerializer.", e);
|
||||
}
|
||||
|
||||
ByteArrayOutputStream printStream;
|
||||
|
||||
if (MapsClient.LOG_COMMUNICATION) {
|
||||
printStream = new ByteArrayOutputStream();
|
||||
|
||||
serializer.setOutput(printStream, "UTF-8");
|
||||
} else {
|
||||
serializer.setOutput(out, "UTF-8");
|
||||
}
|
||||
|
||||
serializer.startDocument("UTF-8", Boolean.FALSE);
|
||||
|
||||
declareEntryNamespaces(serializer);
|
||||
serializer.startTag(XmlGDataParser.NAMESPACE_ATOM_URI, "entry");
|
||||
|
||||
if (MapsClient.LOG_COMMUNICATION) {
|
||||
stream = printStream;
|
||||
} else {
|
||||
stream = out;
|
||||
}
|
||||
serializeEntryContents(serializer, format);
|
||||
|
||||
serializer.endTag(XmlGDataParser.NAMESPACE_ATOM_URI, "entry");
|
||||
serializer.endDocument();
|
||||
serializer.flush();
|
||||
|
||||
if (MapsClient.LOG_COMMUNICATION) {
|
||||
Log.d("Request", printStream.toString());
|
||||
out.write(printStream.toByteArray());
|
||||
stream = out;
|
||||
}
|
||||
}
|
||||
|
||||
private final void declareEntryNamespaces(XmlSerializer serializer)
|
||||
throws IOException {
|
||||
serializer.setPrefix(
|
||||
"" /* default ns */, XmlGDataParser.NAMESPACE_ATOM_URI);
|
||||
serializer.setPrefix(
|
||||
XmlGDataParser.NAMESPACE_GD, XmlGDataParser.NAMESPACE_GD_URI);
|
||||
declareExtraEntryNamespaces(serializer);
|
||||
}
|
||||
|
||||
private final void serializeEntryContents(XmlSerializer serializer,
|
||||
int format) throws IOException {
|
||||
if (format != FORMAT_CREATE) {
|
||||
serializeId(serializer, entry.getId());
|
||||
}
|
||||
|
||||
serializeTitle(serializer, entry.getTitle());
|
||||
|
||||
if (format != FORMAT_CREATE) {
|
||||
serializeLink(serializer,
|
||||
"edit" /* rel */, entry.getEditUri(), null /* type */);
|
||||
serializeLink(serializer,
|
||||
"alternate" /* rel */, entry.getHtmlUri(), "text/html" /* type */);
|
||||
}
|
||||
|
||||
serializeSummary(serializer, entry.getSummary());
|
||||
serializeContent(serializer, entry.getContent());
|
||||
serializeAuthor(serializer, entry.getAuthor(), entry.getEmail());
|
||||
serializeCategory(serializer,
|
||||
entry.getCategory(), entry.getCategoryScheme());
|
||||
|
||||
if (format == FORMAT_FULL) {
|
||||
serializePublicationDate(serializer, entry.getPublicationDate());
|
||||
}
|
||||
|
||||
if (format != FORMAT_CREATE) {
|
||||
serializeUpdateDate(serializer, entry.getUpdateDate());
|
||||
}
|
||||
|
||||
serializeExtraEntryContents(serializer, format);
|
||||
}
|
||||
|
||||
private static void serializeId(XmlSerializer serializer, String id)
|
||||
throws IOException {
|
||||
if (StringUtils.isEmpty(id)) {
|
||||
return;
|
||||
}
|
||||
serializer.startTag(null /* ns */, "id");
|
||||
serializer.text(id);
|
||||
serializer.endTag(null /* ns */, "id");
|
||||
}
|
||||
|
||||
private static void serializeTitle(XmlSerializer serializer, String title)
|
||||
throws IOException {
|
||||
if (StringUtils.isEmpty(title)) {
|
||||
return;
|
||||
}
|
||||
serializer.startTag(null /* ns */, "title");
|
||||
serializer.text(title);
|
||||
serializer.endTag(null /* ns */, "title");
|
||||
}
|
||||
|
||||
public static void serializeLink(XmlSerializer serializer, String rel,
|
||||
String href, String type) throws IOException {
|
||||
if (StringUtils.isEmpty(href)) {
|
||||
return;
|
||||
}
|
||||
serializer.startTag(null /* ns */, "link");
|
||||
serializer.attribute(null /* ns */, "rel", rel);
|
||||
serializer.attribute(null /* ns */, "href", href);
|
||||
if (!StringUtils.isEmpty(type)) {
|
||||
serializer.attribute(null /* ns */, "type", type);
|
||||
}
|
||||
serializer.endTag(null /* ns */, "link");
|
||||
}
|
||||
|
||||
private static void serializeSummary(XmlSerializer serializer, String summary)
|
||||
throws IOException {
|
||||
if (StringUtils.isEmpty(summary)) {
|
||||
return;
|
||||
}
|
||||
serializer.startTag(null /* ns */, "summary");
|
||||
serializer.text(summary);
|
||||
serializer.endTag(null /* ns */, "summary");
|
||||
}
|
||||
|
||||
private void serializeContent(XmlSerializer serializer, String content)
|
||||
throws IOException {
|
||||
if (content == null) {
|
||||
return;
|
||||
}
|
||||
serializer.startTag(null /* ns */, "content");
|
||||
if (content.contains("</Placemark>")) {
|
||||
serializer.attribute(
|
||||
null /* ns */, "type", "application/vnd.google-earth.kml+xml");
|
||||
serializer.flush();
|
||||
stream.write(content.getBytes());
|
||||
} else {
|
||||
serializer.text(content);
|
||||
}
|
||||
serializer.endTag(null /* ns */, "content");
|
||||
}
|
||||
|
||||
private static void serializeAuthor(XmlSerializer serializer, String author,
|
||||
String email) throws IOException {
|
||||
if (StringUtils.isEmpty(author) || StringUtils.isEmpty(email)) {
|
||||
return;
|
||||
}
|
||||
serializer.startTag(null /* ns */, "author");
|
||||
serializer.startTag(null /* ns */, "name");
|
||||
serializer.text(author);
|
||||
serializer.endTag(null /* ns */, "name");
|
||||
serializer.startTag(null /* ns */, "email");
|
||||
serializer.text(email);
|
||||
serializer.endTag(null /* ns */, "email");
|
||||
serializer.endTag(null /* ns */, "author");
|
||||
}
|
||||
|
||||
private static void serializeCategory(XmlSerializer serializer,
|
||||
String category, String categoryScheme) throws IOException {
|
||||
if (StringUtils.isEmpty(category) && StringUtils.isEmpty(categoryScheme)) {
|
||||
return;
|
||||
}
|
||||
serializer.startTag(null /* ns */, "category");
|
||||
if (!StringUtils.isEmpty(category)) {
|
||||
serializer.attribute(null /* ns */, "term", category);
|
||||
}
|
||||
if (!StringUtils.isEmpty(categoryScheme)) {
|
||||
serializer.attribute(null /* ns */, "scheme", categoryScheme);
|
||||
}
|
||||
serializer.endTag(null /* ns */, "category");
|
||||
}
|
||||
|
||||
private static void serializePublicationDate(XmlSerializer serializer,
|
||||
String publicationDate) throws IOException {
|
||||
if (StringUtils.isEmpty(publicationDate)) {
|
||||
return;
|
||||
}
|
||||
serializer.startTag(null /* ns */, "published");
|
||||
serializer.text(publicationDate);
|
||||
serializer.endTag(null /* ns */, "published");
|
||||
}
|
||||
|
||||
private static void serializeUpdateDate(XmlSerializer serializer,
|
||||
String updateDate) throws IOException {
|
||||
if (StringUtils.isEmpty(updateDate)) {
|
||||
return;
|
||||
}
|
||||
serializer.startTag(null /* ns */, "updated");
|
||||
serializer.text(updateDate);
|
||||
serializer.endTag(null /* ns */, "updated");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void serializeExtraEntryContents(XmlSerializer serializer,
|
||||
int format) throws IOException {
|
||||
Map<String, String> attrs = entry.getAllAttributes();
|
||||
for (Map.Entry<String, String> attr : attrs.entrySet()) {
|
||||
serializer.startTag("http://schemas.google.com/g/2005", "customProperty");
|
||||
serializer.attribute(null, "name", attr.getKey());
|
||||
serializer.text(attr.getValue());
|
||||
serializer.endTag("http://schemas.google.com/g/2005", "customProperty");
|
||||
}
|
||||
String privacy = entry.getPrivacy();
|
||||
if (!StringUtils.isEmpty(privacy)) {
|
||||
serializer.setPrefix("app", APP_NAMESPACE);
|
||||
if ("public".equals(privacy)) {
|
||||
serializer.startTag(APP_NAMESPACE, "control");
|
||||
serializer.startTag(APP_NAMESPACE, "draft");
|
||||
serializer.text("no");
|
||||
serializer.endTag(APP_NAMESPACE, "draft");
|
||||
serializer.endTag(APP_NAMESPACE, "control");
|
||||
}
|
||||
if ("unlisted".equals(privacy)) {
|
||||
serializer.startTag(APP_NAMESPACE, "control");
|
||||
serializer.startTag(APP_NAMESPACE, "draft");
|
||||
serializer.text("yes");
|
||||
serializer.endTag(APP_NAMESPACE, "draft");
|
||||
serializer.endTag(APP_NAMESPACE, "control");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
/*
|
||||
* Copyright 2012 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package com.google.android.apps.mytracks.io.maps;
|
||||
|
||||
import com.google.android.apps.mytracks.io.gdata.maps.MapsMapMetadata;
|
||||
import com.google.android.apps.mytracks.io.sendtogoogle.SendRequest;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.app.Dialog;
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.DialogInterface.OnCancelListener;
|
||||
import android.content.DialogInterface.OnClickListener;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Color;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.ListView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* An activity to choose a Google Map.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class ChooseMapActivity extends Activity {
|
||||
|
||||
private static final int PROGRESS_DIALOG = 1;
|
||||
private static final int ERROR_DIALOG = 2;
|
||||
|
||||
private SendRequest sendRequest;
|
||||
private ChooseMapAsyncTask asyncTask;
|
||||
private ProgressDialog progressDialog;
|
||||
private ArrayAdapter<ListItem> arrayAdapter;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
sendRequest = getIntent().getParcelableExtra(SendRequest.SEND_REQUEST_KEY);
|
||||
setContentView(R.layout.choose_map);
|
||||
|
||||
arrayAdapter = new ArrayAdapter<ListItem>(this, R.layout.choose_map_item, new ArrayList<
|
||||
ListItem>()) {
|
||||
@Override
|
||||
public View getView(int position, View convertView, ViewGroup parent) {
|
||||
if (convertView == null) {
|
||||
convertView = getLayoutInflater().inflate(R.layout.choose_map_item, parent, false);
|
||||
}
|
||||
MapsMapMetadata mapData = getItem(position).getMapData();
|
||||
|
||||
TextView title = (TextView) convertView.findViewById(R.id.choose_map_list_item_title);
|
||||
title.setText(mapData.getTitle());
|
||||
|
||||
TextView description = (TextView) convertView.findViewById(
|
||||
R.id.choose_map_list_item_description);
|
||||
String descriptionText = mapData.getDescription();
|
||||
if (descriptionText == null || descriptionText.equals("")) {
|
||||
description.setVisibility(View.GONE);
|
||||
} else {
|
||||
description.setVisibility(View.VISIBLE);
|
||||
description.setText(descriptionText);
|
||||
}
|
||||
|
||||
TextView searchStatus = (TextView) convertView.findViewById(
|
||||
R.id.choose_map_list_item_search_status);
|
||||
searchStatus.setTextColor(mapData.getSearchable() ? Color.RED : Color.GREEN);
|
||||
searchStatus.setText(mapData.getSearchable() ? R.string.maps_list_public_label
|
||||
: R.string.maps_list_unlisted_label);
|
||||
return convertView;
|
||||
}
|
||||
};
|
||||
|
||||
ListView list = (ListView) findViewById(R.id.choose_map_list_view);
|
||||
list.setEmptyView(findViewById(R.id.choose_map_empty_view));
|
||||
list.setOnItemClickListener(new AdapterView.OnItemClickListener() {
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
|
||||
startNextActivity(arrayAdapter.getItem(position).getMapId());
|
||||
}
|
||||
});
|
||||
list.setAdapter(arrayAdapter);
|
||||
|
||||
Object retained = getLastNonConfigurationInstance();
|
||||
if (retained instanceof ChooseMapAsyncTask) {
|
||||
asyncTask = (ChooseMapAsyncTask) retained;
|
||||
asyncTask.setActivity(this);
|
||||
} else {
|
||||
asyncTask = new ChooseMapAsyncTask(this, sendRequest.getAccount());
|
||||
asyncTask.execute();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object onRetainNonConfigurationInstance() {
|
||||
asyncTask.setActivity(null);
|
||||
return asyncTask;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Dialog onCreateDialog(int id) {
|
||||
switch (id) {
|
||||
case PROGRESS_DIALOG:
|
||||
progressDialog = new ProgressDialog(this);
|
||||
progressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
|
||||
progressDialog.setMessage(getString(R.string.maps_list_loading));
|
||||
progressDialog.setCancelable(true);
|
||||
progressDialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
|
||||
@Override
|
||||
public void onCancel(DialogInterface dialog) {
|
||||
asyncTask.cancel(true);
|
||||
finish();
|
||||
}
|
||||
});
|
||||
progressDialog.setIcon(android.R.drawable.ic_dialog_info);
|
||||
progressDialog.setTitle(R.string.generic_progress_title);
|
||||
return progressDialog;
|
||||
case ERROR_DIALOG:
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||
builder.setIcon(android.R.drawable.ic_dialog_alert);
|
||||
builder.setTitle(R.string.generic_error_title);
|
||||
builder.setMessage(R.string.maps_list_error);
|
||||
builder.setPositiveButton(R.string.generic_ok, new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int arg1) {
|
||||
finish();
|
||||
}
|
||||
});
|
||||
builder.setOnCancelListener(new OnCancelListener() {
|
||||
@Override
|
||||
public void onCancel(DialogInterface dialog) {
|
||||
finish();
|
||||
}
|
||||
});
|
||||
return builder.create();
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes when the associated AsyncTask completes.
|
||||
*
|
||||
* @param success true if success
|
||||
* @param mapIds an array of map ids
|
||||
* @param mapData an array of map data
|
||||
*/
|
||||
public void onAsyncTaskCompleted(
|
||||
boolean success, ArrayList<String> mapIds, ArrayList<MapsMapMetadata> mapData) {
|
||||
removeDialog(PROGRESS_DIALOG);
|
||||
if (success) {
|
||||
arrayAdapter.clear();
|
||||
// To prevent displaying the emptyView message momentarily before the
|
||||
// arrayAdapter is set, don't set the emptyView message in the xml layout.
|
||||
// Instead, set it only when needed.
|
||||
if (mapIds.size() == 0) {
|
||||
TextView emptyView = (TextView) findViewById(R.id.choose_map_empty_view);
|
||||
emptyView.setText(R.string.maps_list_no_maps);
|
||||
} else {
|
||||
for (int i = 0; i < mapIds.size(); i++) {
|
||||
arrayAdapter.add(new ListItem(mapIds.get(i), mapData.get(i)));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
showDialog(ERROR_DIALOG);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the progress dialog.
|
||||
*/
|
||||
public void showProgressDialog() {
|
||||
showDialog(PROGRESS_DIALOG);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the next activity, {@link SendMapsActivity}.
|
||||
*
|
||||
* @param mapId the chosen map id
|
||||
*/
|
||||
private void startNextActivity(String mapId) {
|
||||
sendRequest.setMapId(mapId);
|
||||
Intent intent = new Intent(this, SendMapsActivity.class)
|
||||
.putExtra(SendRequest.SEND_REQUEST_KEY, sendRequest);
|
||||
startActivity(intent);
|
||||
finish();
|
||||
}
|
||||
|
||||
/**
|
||||
* A class containing {@link ChooseMapActivity} list item.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
private class ListItem {
|
||||
private String mapId;
|
||||
private MapsMapMetadata mapData;
|
||||
|
||||
private ListItem(String mapId, MapsMapMetadata mapData) {
|
||||
this.mapId = mapId;
|
||||
this.mapData = mapData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the map id.
|
||||
*/
|
||||
public String getMapId() {
|
||||
return mapId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the map data.
|
||||
*/
|
||||
public MapsMapMetadata getMapData() {
|
||||
return mapData;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
/*
|
||||
* Copyright 2012 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package com.google.android.apps.mytracks.io.maps;
|
||||
|
||||
import com.google.android.apps.mytracks.Constants;
|
||||
import com.google.android.apps.mytracks.io.gdata.GDataClientFactory;
|
||||
import com.google.android.apps.mytracks.io.gdata.maps.MapFeatureEntry;
|
||||
import com.google.android.apps.mytracks.io.gdata.maps.MapsClient;
|
||||
import com.google.android.apps.mytracks.io.gdata.maps.MapsConstants;
|
||||
import com.google.android.apps.mytracks.io.gdata.maps.MapsGDataConverter;
|
||||
import com.google.android.apps.mytracks.io.gdata.maps.MapsMapMetadata;
|
||||
import com.google.android.apps.mytracks.io.gdata.maps.XmlMapsGDataParserFactory;
|
||||
import com.google.android.common.gdata.AndroidXmlParserFactory;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.wireless.gdata.client.GDataClient;
|
||||
import com.google.wireless.gdata.client.HttpException;
|
||||
import com.google.wireless.gdata.parser.GDataParser;
|
||||
import com.google.wireless.gdata.parser.ParseException;
|
||||
|
||||
import android.accounts.Account;
|
||||
import android.accounts.AccountManager;
|
||||
import android.accounts.AuthenticatorException;
|
||||
import android.accounts.OperationCanceledException;
|
||||
import android.content.Context;
|
||||
import android.os.AsyncTask;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* AsyncTask for {@link ChooseMapActivity} to get all the maps from Google Maps.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class ChooseMapAsyncTask extends AsyncTask<Void, Integer, Boolean> {
|
||||
private static final String TAG = ChooseMapAsyncTask.class.getSimpleName();
|
||||
|
||||
private ChooseMapActivity activity;
|
||||
private final Account account;
|
||||
private final Context context;
|
||||
private final GDataClient gDataClient;
|
||||
private final MapsClient mapsClient;
|
||||
|
||||
/**
|
||||
* True if can retry sending to Google Fusion Tables.
|
||||
*/
|
||||
private boolean canRetry;
|
||||
|
||||
/**
|
||||
* True if the AsyncTask has completed.
|
||||
*/
|
||||
private boolean completed;
|
||||
|
||||
/**
|
||||
* True if the result is success.
|
||||
*/
|
||||
private boolean success;
|
||||
|
||||
// The following variables are for per request states
|
||||
private String authToken;
|
||||
private ArrayList<String> mapIds;
|
||||
private ArrayList<MapsMapMetadata> mapData;
|
||||
|
||||
public ChooseMapAsyncTask(ChooseMapActivity activity, Account account) {
|
||||
this.activity = activity;
|
||||
this.account = account;
|
||||
|
||||
context = activity.getApplicationContext();
|
||||
gDataClient = GDataClientFactory.getGDataClient(context);
|
||||
mapsClient = new MapsClient(
|
||||
gDataClient, new XmlMapsGDataParserFactory(new AndroidXmlParserFactory()));
|
||||
|
||||
canRetry = true;
|
||||
completed = false;
|
||||
success = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the activity associated with this AyncTask.
|
||||
*
|
||||
* @param activity the activity.
|
||||
*/
|
||||
public void setActivity(ChooseMapActivity activity) {
|
||||
this.activity = activity;
|
||||
if (completed && activity != null) {
|
||||
activity.onAsyncTaskCompleted(success, mapIds, mapData);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
activity.showProgressDialog();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Boolean doInBackground(Void... params) {
|
||||
return getMaps();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCancelled() {
|
||||
closeClient();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Boolean result) {
|
||||
closeClient();
|
||||
success = result;
|
||||
completed = true;
|
||||
if (activity != null) {
|
||||
activity.onAsyncTaskCompleted(success, mapIds, mapData);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the gdata client.
|
||||
*/
|
||||
private void closeClient() {
|
||||
if (gDataClient != null) {
|
||||
gDataClient.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all the maps from Google Maps.
|
||||
*
|
||||
* @return true if success.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
boolean getMaps() {
|
||||
// Reset the per request states
|
||||
authToken = null;
|
||||
mapIds = new ArrayList<String>();
|
||||
mapData = new ArrayList<MapsMapMetadata>();
|
||||
|
||||
try {
|
||||
authToken = AccountManager.get(context).blockingGetAuthToken(
|
||||
account, MapsConstants.SERVICE_NAME, false);
|
||||
} catch (OperationCanceledException e) {
|
||||
Log.d(TAG, "Unable to get auth token", e);
|
||||
return retryUpload();
|
||||
} catch (AuthenticatorException e) {
|
||||
Log.d(TAG, "Unable to get auth token", e);
|
||||
return retryUpload();
|
||||
} catch (IOException e) {
|
||||
Log.d(TAG, "Unable to get auth token", e);
|
||||
return retryUpload();
|
||||
}
|
||||
|
||||
if (isCancelled()) {
|
||||
return false;
|
||||
}
|
||||
GDataParser gDataParser = null;
|
||||
try {
|
||||
gDataParser = mapsClient.getParserForFeed(
|
||||
MapFeatureEntry.class, MapsClient.getMapsFeed(), authToken);
|
||||
gDataParser.init();
|
||||
while (gDataParser.hasMoreData()) {
|
||||
MapFeatureEntry entry = (MapFeatureEntry) gDataParser.readNextEntry(null);
|
||||
mapIds.add(MapsGDataConverter.getMapidForEntry(entry));
|
||||
mapData.add(MapsGDataConverter.getMapMetadataForEntry(entry));
|
||||
}
|
||||
} catch (ParseException e) {
|
||||
Log.d(TAG, "Unable to get maps", e);
|
||||
return retryUpload();
|
||||
} catch (IOException e) {
|
||||
Log.d(TAG, "Unable to get maps", e);
|
||||
return retryUpload();
|
||||
} catch (HttpException e) {
|
||||
Log.d(TAG, "Unable to get maps", e);
|
||||
return retryUpload();
|
||||
} finally {
|
||||
if (gDataParser != null) {
|
||||
gDataParser.close();
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retries upload. Invalidates the authToken. If can retry, invokes
|
||||
* {@link ChooseMapAsyncTask#getMaps()}. Returns false if cannot retry.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
boolean retryUpload() {
|
||||
if (isCancelled()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
AccountManager.get(context).invalidateAuthToken(Constants.ACCOUNT_TYPE, authToken);
|
||||
if (canRetry) {
|
||||
canRetry = false;
|
||||
return getMaps();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the complete status of task.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
boolean getCompleted() {
|
||||
return completed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the complete status of task.
|
||||
* @param completed
|
||||
*/
|
||||
@VisibleForTesting
|
||||
void setCompleted(boolean completed) {
|
||||
this.completed = completed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the status of canRetry.
|
||||
* @param completed status of canRetry
|
||||
*/
|
||||
@VisibleForTesting
|
||||
void setCanRetry(boolean canRetry) {
|
||||
this.canRetry = canRetry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the status of canRetry.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
boolean getCanRetry() {
|
||||
return canRetry;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright 2012 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package com.google.android.apps.mytracks.io.maps;
|
||||
|
||||
import com.google.android.apps.mytracks.io.docs.SendDocsActivity;
|
||||
import com.google.android.apps.mytracks.io.fusiontables.SendFusionTablesActivity;
|
||||
import com.google.android.apps.mytracks.io.sendtogoogle.AbstractSendActivity;
|
||||
import com.google.android.apps.mytracks.io.sendtogoogle.AbstractSendAsyncTask;
|
||||
import com.google.android.apps.mytracks.io.sendtogoogle.SendRequest;
|
||||
import com.google.android.apps.mytracks.io.sendtogoogle.UploadResultActivity;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.content.Intent;
|
||||
|
||||
/**
|
||||
* An activity to send a track to Google Maps.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class SendMapsActivity extends AbstractSendActivity {
|
||||
|
||||
@Override
|
||||
protected AbstractSendAsyncTask createAsyncTask() {
|
||||
return new SendMapsAsyncTask(
|
||||
this, sendRequest.getTrackId(), sendRequest.getAccount(), sendRequest.getMapId());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getServiceName() {
|
||||
return getString(R.string.send_google_maps);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void startNextActivity(boolean success, boolean isCancel) {
|
||||
sendRequest.setMapsSuccess(success);
|
||||
|
||||
Class<?> next;
|
||||
if (isCancel) {
|
||||
next = UploadResultActivity.class;
|
||||
} else {
|
||||
if (sendRequest.isSendFusionTables()) {
|
||||
next = SendFusionTablesActivity.class;
|
||||
} else if (sendRequest.isSendDocs()) {
|
||||
next = SendDocsActivity.class;
|
||||
} else {
|
||||
next = UploadResultActivity.class;
|
||||
}
|
||||
}
|
||||
Intent intent = new Intent(this, next).putExtra(SendRequest.SEND_REQUEST_KEY, sendRequest);
|
||||
startActivity(intent);
|
||||
finish();
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user