forked from upstream-mirrors/OpenTracks
Synced with head. Resolved conflicts.
This commit is contained in:
@@ -0,0 +1,276 @@
|
||||
/*
|
||||
* 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 {
|
||||
|
||||
//////////////////////////////////////////////
|
||||
// Library Version Information
|
||||
//////////////////////////////////////////////
|
||||
public static final int ANT_LIBRARY_VERSION_CODE = 5;
|
||||
public static final int ANT_LIBRARY_VERSION_MAJOR = 1;
|
||||
public static final int ANT_LIBRARY_VERSION_MINOR = 4;
|
||||
public static final String ANT_LIBRARY_VERSION_NAME = String.valueOf(ANT_LIBRARY_VERSION_MAJOR) + "." + String.valueOf(ANT_LIBRARY_VERSION_MINOR);
|
||||
|
||||
//////////////////////////////////////////////
|
||||
// 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,34 @@
|
||||
/*
|
||||
* 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_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_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,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,62 @@
|
||||
/*
|
||||
* Copyright 2010 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package com.google.android.apps.mytracks;
|
||||
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.content.Context;
|
||||
import android.preference.Preference;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
/**
|
||||
* A preference for an ANT device pairing.
|
||||
* Currently this shows the ID and lets the user clear that ID for future pairing.
|
||||
* TODO: Support pairing from this preference.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class AntPreference extends Preference {
|
||||
|
||||
public AntPreference(Context context) {
|
||||
super(context);
|
||||
init();
|
||||
}
|
||||
|
||||
public AntPreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init();
|
||||
}
|
||||
|
||||
private void init() {
|
||||
int sensorId = getPersistedInt(0);
|
||||
if (sensorId == 0) {
|
||||
setSummary(R.string.settings_ant_not_paired);
|
||||
} else {
|
||||
setSummary(String.format(getContext().getString(R.string.settings_ant_paired), sensorId));
|
||||
}
|
||||
|
||||
// Add actions to allow repairing.
|
||||
setOnPreferenceClickListener(
|
||||
new OnPreferenceClickListener() {
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
AntPreference.this.persistInt(0);
|
||||
setSummary(R.string.settings_ant_not_paired);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -252,7 +252,7 @@ public class ChartActivity extends Activity implements
|
||||
setVolumeControlStream(volumeStream);
|
||||
|
||||
requestWindowFeature(Window.FEATURE_NO_TITLE);
|
||||
setContentView(R.layout.mytracks_elevation);
|
||||
setContentView(R.layout.mytracks_charts);
|
||||
ViewGroup layout = (ViewGroup) findViewById(R.id.elevation_chart);
|
||||
cv = new ChartView(this);
|
||||
cv.setMode(this.mode);
|
||||
|
||||
@@ -21,6 +21,7 @@ import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.Paint.Style;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
|
||||
@@ -36,16 +37,96 @@ public class ChartValueSeries {
|
||||
private final Path path = new Path();
|
||||
private final Paint fillPaint;
|
||||
private final Paint strokePaint;
|
||||
private final int rounding;
|
||||
private final Paint labelPaint;
|
||||
private final ZoomSettings zoomSettings;
|
||||
|
||||
private String title;
|
||||
private double min = 0;
|
||||
private double max = 1;
|
||||
private int effectiveMax = 0;
|
||||
private int absoluteMax = Integer.MIN_VALUE;
|
||||
private int effectiveMin = 0;
|
||||
private double spread = 0;
|
||||
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.
|
||||
@@ -54,18 +135,30 @@ public class ChartValueSeries {
|
||||
* @param formatString The format of the decimal format for this series
|
||||
* @param fill The paint for filling the chart
|
||||
* @param stroke The paint for stroking the outside the chart, optional
|
||||
* @param rounding The factor to round the values by
|
||||
* @param zoomSettings The settings related to zooming
|
||||
*
|
||||
* TODO: Get rid of Context and inject appropriate values instead.
|
||||
*/
|
||||
public ChartValueSeries(Context context,
|
||||
String formatString,
|
||||
Paint fill,
|
||||
Paint stroke,
|
||||
int rounding,
|
||||
int titleId) {
|
||||
public ChartValueSeries(Context context, String formatString, int fillColor, int strokeColor,
|
||||
ZoomSettings zoomSettings, int titleId) {
|
||||
this.format = new DecimalFormat(formatString);
|
||||
this.fillPaint = fill;
|
||||
this.strokePaint = stroke;
|
||||
this.rounding = rounding;
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -97,20 +190,7 @@ public class ChartValueSeries {
|
||||
* @return The interval between markers
|
||||
*/
|
||||
public int getInterval() {
|
||||
// Try to find 5 even looking intervals.
|
||||
int interval = (int) (spread / 5);
|
||||
int minInterval = rounding / 4;
|
||||
if (interval < minInterval) {
|
||||
// We won't be able to find 5 even intervals.
|
||||
return minInterval;
|
||||
} else if (interval < rounding) {
|
||||
// The desired interval is less than the rounding value.
|
||||
// We will have less than 5 intervals.
|
||||
return rounding;
|
||||
} else {
|
||||
// Round the interval.
|
||||
return (interval / rounding) * rounding;
|
||||
}
|
||||
return interval;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -125,14 +205,18 @@ public class ChartValueSeries {
|
||||
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 / rounding)) * rounding + rounding;
|
||||
effectiveMax = ((int) (max / interval)) * interval + interval;
|
||||
// Round it down.
|
||||
effectiveMin = ((int) (min / rounding)) * rounding;
|
||||
effectiveMin = ((int) (min / interval)) * interval;
|
||||
if (min < 0) {
|
||||
effectiveMin -= rounding;
|
||||
effectiveMin -= interval;
|
||||
}
|
||||
spread = getMax() - effectiveMin;
|
||||
spread = effectiveMax - effectiveMin;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -152,16 +236,10 @@ public class ChartValueSeries {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The minimum of the rounded up max value and the effective max.
|
||||
* @return The rounded up maximum value
|
||||
*/
|
||||
public int getMax() {
|
||||
// Return the effective max if no absolute is set
|
||||
// or the effective max is less than the absolute.
|
||||
if ((absoluteMax == Integer.MIN_VALUE) || (effectiveMax < absoluteMax)) {
|
||||
return effectiveMax;
|
||||
} else {
|
||||
return absoluteMax;
|
||||
}
|
||||
return effectiveMax;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -189,9 +267,11 @@ public class ChartValueSeries {
|
||||
* @return The paint for this series
|
||||
*/
|
||||
Paint getPaint() {
|
||||
return (strokePaint == null)
|
||||
? fillPaint
|
||||
: strokePaint;
|
||||
return strokePaint == null ? fillPaint : strokePaint;
|
||||
}
|
||||
|
||||
public Paint getLabelPaint() {
|
||||
return labelPaint;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -219,14 +299,6 @@ public class ChartValueSeries {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
public int getAbsoluteMax() {
|
||||
return absoluteMax;
|
||||
}
|
||||
|
||||
public void setAbsoluteMax(int absoluteMax) {
|
||||
this.absoluteMax = absoluteMax;
|
||||
}
|
||||
|
||||
public boolean hasData() {
|
||||
return monitor.hasData();
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
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;
|
||||
@@ -25,9 +26,11 @@ 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.Style;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.Paint.Align;
|
||||
import android.graphics.Paint.Style;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.VelocityTracker;
|
||||
@@ -60,14 +63,35 @@ public class ChartView extends View {
|
||||
*/
|
||||
private int zoomLevel = 1;
|
||||
private final int minZoomLevel = 1;
|
||||
private int maxZoomLevel = 5;
|
||||
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;
|
||||
private static final int BOTTOM_BORDER = 135;
|
||||
|
||||
/**
|
||||
* Device scaled top border of the chart.
|
||||
*/
|
||||
private int topBorder;
|
||||
|
||||
/**
|
||||
* Unscaled bottom border of the chart.
|
||||
*/
|
||||
private static final float BOTTOM_BORDER = 40;
|
||||
|
||||
/**
|
||||
* Device scaled bottom border of the chart.
|
||||
*/
|
||||
private int bottomBorder;
|
||||
|
||||
private static final int RIGHT_BORDER = 17;
|
||||
/** Space to leave for drawing the unit labels */
|
||||
private static final int UNIT_BORDER = 15;
|
||||
@@ -103,6 +127,7 @@ public class ChartView extends View {
|
||||
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;
|
||||
@@ -123,8 +148,6 @@ public class ChartView extends View {
|
||||
private boolean metricUnits = true;
|
||||
private boolean showPointer = false;
|
||||
|
||||
private boolean yLabelMask[] = null;
|
||||
|
||||
/** Display chart versus distance or time */
|
||||
public enum Mode {
|
||||
BY_DISTANCE, BY_TIME
|
||||
@@ -149,6 +172,9 @@ public class ChartView extends View {
|
||||
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);
|
||||
@@ -174,45 +200,47 @@ public class ChartView extends View {
|
||||
public void setUpChartValueSeries(Context context) {
|
||||
series = new ChartValueSeries[NUM_SERIES];
|
||||
|
||||
// Create the paints
|
||||
Paint fillPaint = new Paint();
|
||||
fillPaint.setStyle(Style.FILL);
|
||||
fillPaint.setColor(context.getResources().getColor(R.color.green));
|
||||
fillPaint.setAntiAlias(true);
|
||||
|
||||
// Create the value series
|
||||
// Create the value series.
|
||||
series[ELEVATION_SERIES] =
|
||||
new ChartValueSeries(context,
|
||||
"###,###",
|
||||
fillPaint,
|
||||
null,
|
||||
100,
|
||||
R.color.elevation_fill,
|
||||
R.color.elevation_border,
|
||||
new ZoomSettings(MAX_INTERVALS,
|
||||
new int[] {5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000}),
|
||||
R.string.elevation);
|
||||
|
||||
series[SPEED_SERIES] = getSeries(context, R.color.blue_transparent,
|
||||
R.color.blue, "###,###.0", R.string.speed);
|
||||
series[POWER_SERIES] = getSeries(context, R.color.red_transparent,
|
||||
R.color.red, "###,###", R.string.power);
|
||||
series[POWER_SERIES].setAbsoluteMax(1500);
|
||||
series[CADENCE_SERIES] = getSeries(context, R.color.purple_transparent,
|
||||
R.color.purple, "###,###", R.string.cadence);
|
||||
series[HEART_RATE_SERIES] = getSeries(context, R.color.yellow_transparent,
|
||||
R.color.yellow, "###,###", R.string.heart_rate);
|
||||
}
|
||||
|
||||
private ChartValueSeries getSeries(Context context,
|
||||
int fillColor, int strokeColor, String format, int seriesName) {
|
||||
Paint fillPaint = new Paint();
|
||||
fillPaint.setStyle(Style.FILL);
|
||||
fillPaint.setColor(context.getResources().getColor(fillColor));
|
||||
fillPaint.setAntiAlias(true);
|
||||
|
||||
Paint strokePaint = new Paint();
|
||||
strokePaint.setStyle(Style.STROKE);
|
||||
strokePaint.setColor(context.getResources().getColor(strokeColor));
|
||||
strokePaint.setAntiAlias(true);
|
||||
return new ChartValueSeries(context, format, fillPaint, strokePaint, 5,
|
||||
seriesName);
|
||||
series[SPEED_SERIES] =
|
||||
new ChartValueSeries(context,
|
||||
"###,###",
|
||||
R.color.speed_fill,
|
||||
R.color.speed_border,
|
||||
new ZoomSettings(MAX_INTERVALS, 0, Integer.MIN_VALUE,
|
||||
new int[] {5, 10, 20, 50}),
|
||||
R.string.speed);
|
||||
series[POWER_SERIES] =
|
||||
new ChartValueSeries(context,
|
||||
"###,###",
|
||||
R.color.power_fill,
|
||||
R.color.power_border,
|
||||
new ZoomSettings(MAX_INTERVALS, 0, 1000, new int[] {5, 50, 100, 200}),
|
||||
R.string.power);
|
||||
series[CADENCE_SERIES] =
|
||||
new ChartValueSeries(context,
|
||||
"###,###",
|
||||
R.color.cadence_fill,
|
||||
R.color.cadence_border,
|
||||
new ZoomSettings(MAX_INTERVALS, 0, Integer.MIN_VALUE,
|
||||
new int[] {5, 10, 25, 50}),
|
||||
R.string.cadence);
|
||||
series[HEART_RATE_SERIES] =
|
||||
new ChartValueSeries(context,
|
||||
"###,###",
|
||||
R.color.heartrate_fill,
|
||||
R.color.heartrate_border,
|
||||
new ZoomSettings(MAX_INTERVALS, 0, Integer.MIN_VALUE,
|
||||
new int[] {25, 50}),
|
||||
R.string.heart_rate);
|
||||
}
|
||||
|
||||
public void clearWaypoints() {
|
||||
@@ -277,7 +305,7 @@ public class ChartView extends View {
|
||||
data.add(theData);
|
||||
addDataPointInternal(theData);
|
||||
updateDimensions();
|
||||
setupPath();
|
||||
setUpPath();
|
||||
}
|
||||
|
||||
private void addDataPointInternal(double[] theData) {
|
||||
@@ -308,7 +336,7 @@ public class ChartView extends View {
|
||||
addDataPointInternal(d);
|
||||
}
|
||||
updateDimensions();
|
||||
setupPath();
|
||||
setUpPath();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -342,7 +370,7 @@ public class ChartView extends View {
|
||||
public void zoomIn() {
|
||||
if (canZoomIn()) {
|
||||
zoomLevel++;
|
||||
setupPath();
|
||||
setUpPath();
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
@@ -359,7 +387,7 @@ public class ChartView extends View {
|
||||
scrollX = effectiveWidth * (zoomLevel - 1);
|
||||
scrollTo(scrollX, 0);
|
||||
}
|
||||
setupPath();
|
||||
setUpPath();
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
@@ -525,12 +553,12 @@ public class ChartView extends View {
|
||||
@Override
|
||||
protected synchronized void onDraw(Canvas c) {
|
||||
if (w != c.getWidth() || h != c.getHeight()) {
|
||||
// Dimensions have changed (for example due to orientation change):
|
||||
// Dimensions have changed (for example due to orientation change).
|
||||
w = c.getWidth();
|
||||
h = c.getHeight();
|
||||
effectiveWidth = Math.max(0, w - leftBorder - RIGHT_BORDER);
|
||||
effectiveHeight = Math.max(0, h - TOP_BORDER - BOTTOM_BORDER);
|
||||
setupPath();
|
||||
effectiveHeight = Math.max(0, h - topBorder - bottomBorder);
|
||||
setUpPath();
|
||||
}
|
||||
c.save();
|
||||
c.drawColor(Color.WHITE);
|
||||
@@ -540,16 +568,21 @@ public class ChartView extends View {
|
||||
c.restore();
|
||||
return;
|
||||
}
|
||||
|
||||
c.save();
|
||||
c.clipRect(leftBorder + 1 + getScrollX(), topBorder + 1,
|
||||
w - RIGHT_BORDER + getScrollX() - 1, h - bottomBorder - 1);
|
||||
|
||||
// Draw the data series
|
||||
drawGrid(c);
|
||||
|
||||
// Draw the data series.
|
||||
for (ChartValueSeries cvs : series) {
|
||||
if (cvs.isEnabled() && cvs.hasData()) {
|
||||
cvs.drawPath(c);
|
||||
}
|
||||
}
|
||||
drawGrid(c);
|
||||
|
||||
// Draw the waypoints:
|
||||
|
||||
// Draw the waypoints.
|
||||
for (int i = 1; i < waypoints.size(); i++) {
|
||||
final Waypoint waypoint = waypoints.get(i);
|
||||
if (waypoint.getLocation() == null) {
|
||||
@@ -558,8 +591,8 @@ public class ChartView extends View {
|
||||
c.save();
|
||||
|
||||
final float x = getWaypointX(waypoint);
|
||||
c.drawLine(x, h - BOTTOM_BORDER, x, TOP_BORDER, gridPaint);
|
||||
c.translate(x - (markerWidth / 2), markerHeight);
|
||||
c.drawLine(x, h - bottomBorder, x, topBorder, gridPaint);
|
||||
c.translate(x - markerWidth / 2, markerHeight);
|
||||
if (waypoints.get(i).getType() == Waypoint.TYPE_STATISTICS) {
|
||||
statsMarker.draw(c);
|
||||
} else {
|
||||
@@ -568,24 +601,27 @@ public class ChartView extends View {
|
||||
c.restore();
|
||||
}
|
||||
|
||||
// Draw the axis and labels:
|
||||
c.restore();
|
||||
|
||||
// Draw the axis and labels.
|
||||
drawXLabels(c);
|
||||
drawXAxis(c);
|
||||
drawSeriesTitles(c);
|
||||
|
||||
yLabelMask = new boolean[effectiveHeight / FONT_HEIGHT + 1];
|
||||
|
||||
c.translate(getScrollX(), 0);
|
||||
drawYAxis(c);
|
||||
final int spacer = 5;
|
||||
int x = leftBorder - spacer;
|
||||
for (ChartValueSeries cvs : series) {
|
||||
if (cvs.isEnabled() && cvs.hasData()) {
|
||||
drawYLabels(cvs, c);
|
||||
x -= drawYLabels(cvs, c, x) + spacer;
|
||||
}
|
||||
}
|
||||
c.restore();
|
||||
if (showPointer && data.size() > 0) {
|
||||
if (showPointer && !data.isEmpty()) {
|
||||
c.translate(getX(maxX) - pointer.getIntrinsicWidth() / 2,
|
||||
(getY(series[0], data.get(data.size() - 1)[1])
|
||||
- pointer.getIntrinsicHeight() / 2 - 12));
|
||||
getY(series[0], data.get(data.size() - 1)[1])
|
||||
- pointer.getIntrinsicHeight() / 2 - 12);
|
||||
pointer.draw(c);
|
||||
}
|
||||
}
|
||||
@@ -600,8 +636,8 @@ public class ChartView extends View {
|
||||
int j = 0;
|
||||
for (ChartValueSeries cvs : series) {
|
||||
if (cvs.isEnabled() && cvs.hasData()) {
|
||||
int y = (int) (w * zoomLevel * ((double) (++j) / sections));
|
||||
c.drawText(cvs.getTitle(), y, TOP_BORDER, cvs.getPaint());
|
||||
int x = (int) (w * (double) ++j / sections) + getScrollX();
|
||||
c.drawText(cvs.getTitle(), x, topBorder, cvs.getLabelPaint());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -610,28 +646,35 @@ public class ChartView extends View {
|
||||
* Sets up the path that is used to draw the histogram in onDraw(). The path
|
||||
* needs to be updated any time after the data or histogram dimensions change.
|
||||
*/
|
||||
private synchronized void setupPath() {
|
||||
private synchronized void setUpPath() {
|
||||
for (ChartValueSeries cvs : series) {
|
||||
cvs.getPath().reset();
|
||||
}
|
||||
if (data.size() < 1) {
|
||||
if (data.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// All of the data points to the respective series.
|
||||
for (int i = 0; i < data.size(); i++) {
|
||||
// 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++) {
|
||||
for (int j = 0; j < min; ++j) {
|
||||
ChartValueSeries cvs = series[j];
|
||||
Path path = cvs.getPath();
|
||||
path.lineTo(getX(d[0]), getY(cvs, d[j + 1]));
|
||||
int x = getX(d[0]);
|
||||
int y = getY(cvs, d[j + 1]);
|
||||
if (i == 0) {
|
||||
path.moveTo(x, y);
|
||||
} else {
|
||||
path.lineTo(x, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Close the path.
|
||||
int yCorner = TOP_BORDER + effectiveHeight;
|
||||
int yCorner = topBorder + effectiveHeight;
|
||||
int xCorner = getX(data.get(0)[0]);
|
||||
int min = series.length;
|
||||
for (int j = 0; j < min; j++) {
|
||||
@@ -675,14 +718,20 @@ public class ChartView extends View {
|
||||
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) {
|
||||
maxLength = Math.max(maxLength,
|
||||
cvs.getMaxLabelLength());
|
||||
if (cvs.isEnabled() && cvs.hasData()) {
|
||||
maxLength += cvs.getMaxLabelLength();
|
||||
}
|
||||
}
|
||||
leftBorder = 4 + 7 * maxLength;
|
||||
leftBorder = 4 + 8 * maxLength;
|
||||
effectiveWidth = w - leftBorder - RIGHT_BORDER;
|
||||
effectiveHeight = h - TOP_BORDER - BOTTOM_BORDER;
|
||||
float density = getContext().getResources().getDisplayMetrics().density;
|
||||
bottomBorder = (int) (density * BOTTOM_BORDER);
|
||||
topBorder = (int) (density * TOP_BORDER);
|
||||
effectiveHeight = h - topBorder - bottomBorder;
|
||||
}
|
||||
|
||||
private int getX(double distance) {
|
||||
@@ -690,28 +739,25 @@ public class ChartView extends View {
|
||||
}
|
||||
|
||||
private int getY(ChartValueSeries cvs, double y) {
|
||||
return TOP_BORDER + effectiveHeight
|
||||
return topBorder + effectiveHeight
|
||||
- (int) ((y - cvs.getMin()) * effectiveHeight / cvs.getSpread());
|
||||
}
|
||||
|
||||
private void drawXLabels(Canvas c) {
|
||||
int numLabels = 0;
|
||||
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 {
|
||||
if (interval < 5) {
|
||||
interval = 2;
|
||||
} else if (interval < 10) {
|
||||
interval = 5;
|
||||
} else {
|
||||
interval = (interval / 10) * 10;
|
||||
}
|
||||
interval = (interval / 10) * 10;
|
||||
}
|
||||
drawXLabel(c, 0, shortFormat);
|
||||
numLabels++;
|
||||
int numLabels = 1;
|
||||
for (int i = 1; i * interval < maxX; i++) {
|
||||
drawXLabel(c, i * interval, shortFormat);
|
||||
numLabels++;
|
||||
@@ -721,102 +767,78 @@ public class ChartView extends View {
|
||||
}
|
||||
}
|
||||
|
||||
private void drawYLabels(ChartValueSeries cvs, Canvas c) {
|
||||
int numLabels = 0;
|
||||
drawYLabel(cvs, c, 0);
|
||||
final int y0 = getY(cvs, 0);
|
||||
numLabels++;
|
||||
private float drawYLabels(ChartValueSeries cvs, Canvas c, int x) {
|
||||
int interval = cvs.getInterval();
|
||||
for (int i = 0; i * interval < cvs.getSpread(); i++) {
|
||||
final int y = i * interval + cvs.getMin();
|
||||
final int yi = getY(cvs, y);
|
||||
if (Math.abs(y0 - yi) > 15) {
|
||||
drawYLabel(cvs, c, y);
|
||||
numLabels++;
|
||||
}
|
||||
}
|
||||
if (numLabels < 3) {
|
||||
drawYLabel(cvs, c, cvs.getMax());
|
||||
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;
|
||||
}
|
||||
|
||||
private void drawXLabel(Canvas c, double x, boolean shortFormat) {
|
||||
if (x < 0) {
|
||||
return;
|
||||
}
|
||||
String s =
|
||||
(mode == Mode.BY_DISTANCE)
|
||||
? (shortFormat ? xShortFormat.format(x) : xFormat.format(x))
|
||||
: StringUtils.formatTime((long) x);
|
||||
c.drawText(s,
|
||||
getX(x),
|
||||
effectiveHeight + UNIT_BORDER + TOP_BORDER,
|
||||
effectiveHeight + UNIT_BORDER + topBorder,
|
||||
labelPaint);
|
||||
}
|
||||
|
||||
private void drawYLabel(ChartValueSeries cvs, Canvas c, int y) {
|
||||
int desiredY =
|
||||
(int) ((y - cvs.getMin()) * effectiveHeight / cvs.getSpread());
|
||||
|
||||
// Make sure we don't write one label on top of another.
|
||||
int slot = desiredY / FONT_HEIGHT;
|
||||
if (slot < 0) {
|
||||
return;
|
||||
}
|
||||
if (yLabelMask[slot]) {
|
||||
desiredY = ++slot * FONT_HEIGHT;
|
||||
}
|
||||
desiredY = TOP_BORDER + effectiveHeight + 5 - desiredY;
|
||||
if (slot < yLabelMask.length) {
|
||||
yLabelMask[slot] = true;
|
||||
}
|
||||
c.drawText(cvs.getFormat().format(y),
|
||||
2, desiredY,
|
||||
cvs.getPaint());
|
||||
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);
|
||||
}
|
||||
|
||||
private void drawXAxis(Canvas canvas) {
|
||||
float rightEdge = getX(maxX);
|
||||
canvas.drawLine(leftBorder, effectiveHeight + TOP_BORDER, rightEdge,
|
||||
effectiveHeight + TOP_BORDER, borderPaint);
|
||||
final int y = effectiveHeight + topBorder;
|
||||
canvas.drawLine(leftBorder, y, rightEdge, y, borderPaint);
|
||||
Context c = getContext();
|
||||
String s =
|
||||
(mode == Mode.BY_DISTANCE)
|
||||
? (metricUnits ? c.getString(R.string.kilometer)
|
||||
: c.getString(R.string.mile)) : (c.getString(R.string.min));
|
||||
canvas.drawText(s,
|
||||
rightEdge,
|
||||
effectiveHeight + .2f * UNIT_BORDER + TOP_BORDER,
|
||||
labelPaint);
|
||||
String s = mode == Mode.BY_DISTANCE
|
||||
? (metricUnits ? c.getString(R.string.kilometer) : c.getString(R.string.mile))
|
||||
: c.getString(R.string.min);
|
||||
canvas.drawText(s, rightEdge, effectiveHeight + .2f * UNIT_BORDER + topBorder, labelPaint);
|
||||
}
|
||||
|
||||
private void drawYAxis(Canvas canvas) {
|
||||
canvas.drawLine(leftBorder, UNIT_BORDER + TOP_BORDER,
|
||||
leftBorder, effectiveHeight + TOP_BORDER,
|
||||
borderPaint);
|
||||
canvas.drawRect(0, 0,
|
||||
leftBorder - 1, effectiveHeight + TOP_BORDER + 1,
|
||||
clearPaint);
|
||||
leftBorder - 1, effectiveHeight + topBorder + UNIT_BORDER + 1,
|
||||
clearPaint);
|
||||
canvas.drawLine(leftBorder, UNIT_BORDER + topBorder,
|
||||
leftBorder, effectiveHeight + topBorder,
|
||||
borderPaint);
|
||||
for (int i = 1; i < MAX_INTERVALS; ++i) {
|
||||
int y = i * effectiveHeight / MAX_INTERVALS + topBorder;
|
||||
canvas.drawLine(leftBorder - 5, y, leftBorder, y, gridPaint);
|
||||
}
|
||||
|
||||
Context c = getContext();
|
||||
// TODO: This should really show units for all series.
|
||||
String s = metricUnits
|
||||
? c.getString(R.string.meter)
|
||||
: c.getString(R.string.feet);
|
||||
canvas.drawText(s, leftBorder - UNIT_BORDER * .2f,
|
||||
UNIT_BORDER * .8f + TOP_BORDER, labelPaint);
|
||||
String s = metricUnits ? c.getString(R.string.meter) : c.getString(R.string.feet);
|
||||
canvas.drawText(s, leftBorder - UNIT_BORDER * .2f, UNIT_BORDER * .8f + topBorder, labelPaint);
|
||||
}
|
||||
|
||||
|
||||
private synchronized void drawGrid(Canvas c) {
|
||||
if (data.size() < 1) {
|
||||
if (data.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
for (ChartValueSeries cvs : series) {
|
||||
if (cvs.isEnabled()) {
|
||||
int interval = cvs.getInterval();
|
||||
for (int i = 1; i * interval < cvs.getSpread(); i++) {
|
||||
int y = getY(cvs, i * interval + cvs.getMin());
|
||||
c.drawLine(getX(data.get(0)[0]), y,
|
||||
getX(data.get(data.size() - 1)[0]), y,
|
||||
cvs.getPaint());
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package com.google.android.apps.mytracks;
|
||||
|
||||
import com.google.android.apps.mytracks.util.ApiFeatures;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.app.Activity;
|
||||
@@ -82,7 +83,8 @@ class CheckUnits {
|
||||
}
|
||||
|
||||
private static void recordCheckPerformed(SharedPreferences preferences) {
|
||||
preferences.edit().putBoolean(PREFERENCE_UNITS_CHECKED, true).commit();
|
||||
ApiFeatures.getInstance().getApiPlatformAdapter().applyPreferenceChanges(
|
||||
preferences.edit().putBoolean(PREFERENCE_UNITS_CHECKED, true));
|
||||
}
|
||||
|
||||
private CheckUnits() {
|
||||
|
||||
@@ -15,43 +15,43 @@
|
||||
*/
|
||||
package com.google.android.apps.mytracks;
|
||||
|
||||
import com.google.android.apps.mytracks.util.ApiFeatures;
|
||||
import com.google.android.apps.mytracks.util.ResourceUtils;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
/**
|
||||
* Displays an EULA ("End User License Agreement") that the user has to accept
|
||||
* before using the application. Your application should call
|
||||
* {@link Eula#showEula(android.app.Activity)} in the onCreate() method of the
|
||||
* first activity. If the user accepts the EULA, it will never be shown again.
|
||||
* If the user refuses, {@link android.app.Activity#finish()} is invoked on your
|
||||
* activity.
|
||||
* This class handles display of EULAs ("End User License Agreements") to the
|
||||
* user.
|
||||
*/
|
||||
class Eula {
|
||||
private static final String PREFERENCE_EULA_ACCEPTED = "eula.accepted";
|
||||
private static final String PREFERENCES_EULA = "eula";
|
||||
|
||||
private Eula() {}
|
||||
|
||||
/**
|
||||
* Displays the EULA if necessary. This method should be called from the
|
||||
* onCreate() method of your main Activity.
|
||||
* onCreate() method of your main Activity. If the user accepts, the EULA
|
||||
* will never be displayed again. If the user refuses, the activity will
|
||||
* finish (exit).
|
||||
*
|
||||
* @param activity The Activity to finish if the user rejects the EULA
|
||||
*/
|
||||
static void showEula(final Activity activity) {
|
||||
static void showEulaRequireAcceptance(final Activity activity) {
|
||||
final SharedPreferences preferences =
|
||||
activity.getSharedPreferences(PREFERENCES_EULA, Activity.MODE_PRIVATE);
|
||||
if (preferences.getBoolean(PREFERENCE_EULA_ACCEPTED, false)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final AlertDialog.Builder builder = new AlertDialog.Builder(activity);
|
||||
builder.setTitle(R.string.eula_title);
|
||||
builder.setCancelable(true);
|
||||
final AlertDialog.Builder builder = initDialog(activity);
|
||||
builder.setPositiveButton(R.string.accept,
|
||||
new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
@@ -72,12 +72,31 @@ class Eula {
|
||||
refuse(activity);
|
||||
}
|
||||
});
|
||||
builder.setMessage(ResourceUtils.readFile(activity, R.raw.eula));
|
||||
builder.show();
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the EULA to the user in an informational context. They won't be
|
||||
* given the choice of accepting or declining the EULA -- we're simply
|
||||
* displaying it for them to read.
|
||||
*/
|
||||
static void showEula(Context context) {
|
||||
AlertDialog.Builder builder = initDialog(context);
|
||||
builder.setPositiveButton(R.string.ok, null);
|
||||
builder.show();
|
||||
}
|
||||
|
||||
private static AlertDialog.Builder initDialog(Context context) {
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
||||
builder.setCancelable(true);
|
||||
builder.setTitle(R.string.eula_title);
|
||||
builder.setMessage(ResourceUtils.readFile(context, R.raw.eula));
|
||||
return builder;
|
||||
}
|
||||
|
||||
private static void accept(Activity activity, SharedPreferences preferences) {
|
||||
preferences.edit().putBoolean(PREFERENCE_EULA_ACCEPTED, true).commit();
|
||||
ApiFeatures.getInstance().getApiPlatformAdapter().applyPreferenceChanges(
|
||||
preferences.edit().putBoolean(PREFERENCE_EULA_ACCEPTED, true));
|
||||
Intent startIntent = new Intent(activity, WelcomeActivity.class);
|
||||
activity.startActivityForResult(startIntent, MyTracksConstants.WELCOME);
|
||||
}
|
||||
@@ -85,7 +104,4 @@ class Eula {
|
||||
private static void refuse(Activity activity) {
|
||||
activity.finish();
|
||||
}
|
||||
|
||||
private Eula() {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ public class ExportAllTracks {
|
||||
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;
|
||||
@@ -67,6 +68,11 @@ public class ExportAllTracks {
|
||||
case CSV_OPTION_INDEX:
|
||||
format = TrackFileFormat.CSV;
|
||||
break;
|
||||
case TCX_OPTION_INDEX:
|
||||
format = TrackFileFormat.TCX;
|
||||
break;
|
||||
default:
|
||||
Log.w(MyTracksConstants.TAG, "Unknown export format: " + which);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -23,7 +23,7 @@ import static com.google.android.apps.mytracks.DialogManager.DIALOG_WRITE_PROGRE
|
||||
import com.google.android.accounts.Account;
|
||||
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
|
||||
import com.google.android.apps.mytracks.content.Track;
|
||||
import com.google.android.apps.mytracks.content.Waypoint;
|
||||
import com.google.android.apps.mytracks.content.WaypointCreationRequest;
|
||||
import com.google.android.apps.mytracks.io.AuthManager;
|
||||
import com.google.android.apps.mytracks.io.AuthManagerFactory;
|
||||
import com.google.android.apps.mytracks.io.GpxImporter;
|
||||
@@ -39,6 +39,7 @@ import com.google.android.apps.mytracks.services.StatusAnnouncerFactory;
|
||||
import com.google.android.apps.mytracks.services.TrackRecordingService;
|
||||
import com.google.android.apps.mytracks.util.ApiFeatures;
|
||||
import com.google.android.apps.mytracks.util.FileUtils;
|
||||
import com.google.android.apps.mytracks.util.MyTracksUtils;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.app.Activity;
|
||||
@@ -60,6 +61,7 @@ import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
import android.os.IBinder;
|
||||
import android.os.RemoteException;
|
||||
import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
@@ -89,10 +91,6 @@ import org.xml.sax.SAXException;
|
||||
*/
|
||||
public class MyTracks extends TabActivity implements OnTouchListener,
|
||||
OnSharedPreferenceChangeListener, ProgressIndicator {
|
||||
|
||||
private static final String WAYPOINT_ICON_URL =
|
||||
"http://maps.google.com/mapfiles/ms/micons/blue-pushpin.png";
|
||||
|
||||
/**
|
||||
* Singleton instance
|
||||
*/
|
||||
@@ -252,6 +250,11 @@ public class MyTracks extends TabActivity implements OnTouchListener,
|
||||
Log.d(MyTracksConstants.TAG, "MyTracks.onCreate");
|
||||
super.onCreate(savedInstanceState);
|
||||
instance = this;
|
||||
ApiFeatures apiFeatures = ApiFeatures.getInstance();
|
||||
if (!MyTracksUtils.isRelease(this)) {
|
||||
apiFeatures.getApiPlatformAdapter().enableStrictMode();
|
||||
}
|
||||
|
||||
providerUtils = MyTracksProviderUtils.Factory.get(this);
|
||||
menuManager = new MenuManager(this);
|
||||
sharedPreferences = getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0);
|
||||
@@ -259,7 +262,7 @@ public class MyTracks extends TabActivity implements OnTouchListener,
|
||||
|
||||
// The volume we want to control is the Text-To-Speech volume
|
||||
int volumeStream =
|
||||
new StatusAnnouncerFactory(ApiFeatures.getInstance()).getVolumeStream();
|
||||
new StatusAnnouncerFactory(apiFeatures).getVolumeStream();
|
||||
setVolumeControlStream(volumeStream);
|
||||
|
||||
// We don't need a window title bar:
|
||||
@@ -310,7 +313,7 @@ public class MyTracks extends TabActivity implements OnTouchListener,
|
||||
}
|
||||
|
||||
// This will show the eula until the user accepts or quits the app.
|
||||
Eula.showEula(this);
|
||||
Eula.showEulaRequireAcceptance(this);
|
||||
|
||||
// Check if we got invoked via the VIEW intent:
|
||||
Intent intent = getIntent();
|
||||
@@ -425,7 +428,13 @@ public class MyTracks extends TabActivity implements OnTouchListener,
|
||||
public boolean onTrackballEvent(MotionEvent event) {
|
||||
if (isRecording()) {
|
||||
if (event.getAction() == MotionEvent.ACTION_DOWN) {
|
||||
insertStatisticsMarker();
|
||||
try {
|
||||
insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS);
|
||||
} catch (RemoteException e) {
|
||||
Log.e(MyTracksConstants.TAG, "Cannot insert statistics marker.", e);
|
||||
} catch (IllegalStateException e) {
|
||||
Log.e(MyTracksConstants.TAG, "Cannot insert statistics marker.", e);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -506,13 +515,14 @@ public class MyTracks extends TabActivity implements OnTouchListener,
|
||||
}
|
||||
break;
|
||||
}
|
||||
case MyTracksConstants.AUTHENTICATE_TO_DOCS: {
|
||||
case MyTracksConstants.AUTHENTICATE_TO_DOCLIST: {
|
||||
if (resultCode == RESULT_OK) {
|
||||
setProgressValue(0);
|
||||
setProgressMessage(
|
||||
R.string.progress_message_authenticating_docs);
|
||||
authenticate(results,
|
||||
MyTracksConstants.AUTHENTICATE_TO_TRIX, "writely");
|
||||
MyTracksConstants.AUTHENTICATE_TO_TRIX,
|
||||
SendToDocs.GDATA_SERVICE_NAME_DOCLIST);
|
||||
} else {
|
||||
dialogManager.dismissDialogSafely(DIALOG_PROGRESS);
|
||||
}
|
||||
@@ -523,7 +533,8 @@ public class MyTracks extends TabActivity implements OnTouchListener,
|
||||
setProgressValue(30);
|
||||
setProgressMessage(
|
||||
R.string.progress_message_authenticating_docs);
|
||||
authenticate(results, MyTracksConstants.SEND_TO_DOCS, "wise");
|
||||
authenticate(results, MyTracksConstants.SEND_TO_DOCS,
|
||||
SendToDocs.GDATA_SERVICE_NAME_TRIX);
|
||||
} else {
|
||||
dialogManager.dismissDialogSafely(DIALOG_PROGRESS);
|
||||
}
|
||||
@@ -535,9 +546,10 @@ public class MyTracks extends TabActivity implements OnTouchListener,
|
||||
setProgressValue(50);
|
||||
setProgressMessage(R.string.progress_message_sending_docs);
|
||||
final long trackId = results.getLongExtra("trackid", selectedTrackId);
|
||||
final SendToDocs sender = new SendToDocs(this,
|
||||
authMap.get(SendToDocs.GDATA_SERVICE_NAME_TRIX),
|
||||
authMap.get(SendToDocs.GDATA_SERVICE_NAME_DOCLIST), trackId);
|
||||
sendToTrackId = trackId;
|
||||
final SendToDocs sender = new SendToDocs(this, authMap.get("wise"),
|
||||
authMap.get("writely"), trackId);
|
||||
Runnable onCompletion = new Runnable() {
|
||||
public void run() {
|
||||
setProgressValue(100);
|
||||
@@ -591,7 +603,7 @@ public class MyTracks extends TabActivity implements OnTouchListener,
|
||||
}
|
||||
}
|
||||
if (dialogManager.getSendToGoogleDialog().getSendToDocs()) {
|
||||
onActivityResult(MyTracksConstants.AUTHENTICATE_TO_DOCS,
|
||||
onActivityResult(MyTracksConstants.AUTHENTICATE_TO_DOCLIST,
|
||||
RESULT_OK, new Intent());
|
||||
} else {
|
||||
dialogManager.dismissDialogSafely(DIALOG_PROGRESS);
|
||||
@@ -877,90 +889,25 @@ public class MyTracks extends TabActivity implements OnTouchListener,
|
||||
/**
|
||||
* Inserts a waypoint marker.
|
||||
*
|
||||
* @return the id of the inserted statistics marker, or
|
||||
* -1 unable to find location
|
||||
* -2 track recording service is not running?
|
||||
* -3 remote exception when contacting track recording service
|
||||
* -4 inserting marker into provider failed
|
||||
* @return Id of the inserted statistics marker.
|
||||
* @throws RemoteException If the call on the service failed.
|
||||
*/
|
||||
public long insertWaypointMarker() {
|
||||
Location location = getCurrentLocation();
|
||||
if (location == null) {
|
||||
public long insertWaypoint(WaypointCreationRequest request) throws RemoteException {
|
||||
if (trackRecordingService == null) {
|
||||
throw new IllegalStateException("The recording service is not bound.");
|
||||
}
|
||||
try {
|
||||
long waypointId = trackRecordingService.insertWaypoint(request);
|
||||
if (waypointId >= 0) {
|
||||
Toast.makeText(this, R.string.status_statistics_inserted,
|
||||
Toast.LENGTH_LONG).show();
|
||||
}
|
||||
return waypointId;
|
||||
} catch (RemoteException e) {
|
||||
Toast.makeText(this, R.string.error_unable_to_insert_marker,
|
||||
Toast.LENGTH_LONG).show();
|
||||
return -1;
|
||||
throw e;
|
||||
}
|
||||
if (trackRecordingService != null) {
|
||||
try {
|
||||
Waypoint wpt = new Waypoint();
|
||||
wpt.setName(getString(R.string.waypoint));
|
||||
wpt.setType(Waypoint.TYPE_WAYPOINT);
|
||||
wpt.setTrackId(recordingTrackId);
|
||||
wpt.setIcon(WAYPOINT_ICON_URL);
|
||||
wpt.setLocation(location);
|
||||
long waypointId = trackRecordingService.insertWaypointMarker(wpt);
|
||||
if (waypointId >= 0) {
|
||||
Toast.makeText(this, R.string.status_waypoint_inserted,
|
||||
Toast.LENGTH_LONG).show();
|
||||
return waypointId;
|
||||
} else {
|
||||
Toast.makeText(this, R.string.error_unable_to_insert_marker,
|
||||
Toast.LENGTH_LONG).show();
|
||||
Log.e(MyTracksConstants.TAG, "Cannot insert waypoint marker?");
|
||||
return -4;
|
||||
}
|
||||
// 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, R.string.error_unable_to_insert_marker,
|
||||
Toast.LENGTH_LONG).show();
|
||||
Log.e(MyTracksConstants.TAG, "Cannot insert waypoint marker.", e);
|
||||
}
|
||||
return -3;
|
||||
}
|
||||
return -2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a statistics marker.
|
||||
*
|
||||
* @return the id of the inserted statistics marker, or
|
||||
* -1 unable to find location
|
||||
* -2 track recording service is not running?
|
||||
* -3 remote exception when contacting track recording service
|
||||
* -4 inserting marker into provider failed
|
||||
*/
|
||||
public long insertStatisticsMarker() {
|
||||
Location location = getLastLocation();
|
||||
if (location == null) {
|
||||
Toast.makeText(this, R.string.error_unable_to_insert_marker,
|
||||
Toast.LENGTH_LONG).show();
|
||||
return -1;
|
||||
}
|
||||
if (trackRecordingService != null) {
|
||||
try {
|
||||
long waypointId =
|
||||
trackRecordingService.insertStatisticsMarker(location);
|
||||
if (waypointId >= 0) {
|
||||
Toast.makeText(this, R.string.status_statistics_inserted,
|
||||
Toast.LENGTH_LONG).show();
|
||||
return waypointId;
|
||||
} else {
|
||||
Toast.makeText(this, R.string.error_unable_to_insert_marker,
|
||||
Toast.LENGTH_LONG).show();
|
||||
Log.e(MyTracksConstants.TAG, "Cannot insert statistics marker?");
|
||||
return -4;
|
||||
}
|
||||
// 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, R.string.error_unable_to_insert_marker,
|
||||
Toast.LENGTH_LONG).show();
|
||||
Log.e(MyTracksConstants.TAG, "Cannot insert statistics marker?", e);
|
||||
}
|
||||
return -3;
|
||||
}
|
||||
return -2;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -980,7 +927,7 @@ public class MyTracks extends TabActivity implements OnTouchListener,
|
||||
service);
|
||||
authMap.put(service, auth);
|
||||
}
|
||||
Log.d(MyTracksConstants.TAG, "Loggin in to " + service + "...");
|
||||
Log.d(MyTracksConstants.TAG, "Logging in to " + service + "...");
|
||||
if (AuthManagerFactory.useModernAuthManager()) {
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
@@ -1096,7 +1043,7 @@ public class MyTracks extends TabActivity implements OnTouchListener,
|
||||
authenticate(new Intent(), MyTracksConstants.SEND_TO_GOOGLE,
|
||||
SendToFusionTables.SERVICE_ID);
|
||||
} else {
|
||||
onActivityResult(MyTracksConstants.AUTHENTICATE_TO_DOCS, RESULT_OK,
|
||||
onActivityResult(MyTracksConstants.AUTHENTICATE_TO_DOCLIST, RESULT_OK,
|
||||
new Intent());
|
||||
}
|
||||
}
|
||||
@@ -1108,10 +1055,10 @@ public class MyTracks extends TabActivity implements OnTouchListener,
|
||||
* @param trackId the id of the track
|
||||
*/
|
||||
public void setSelectedTrackId(final long trackId) {
|
||||
sharedPreferences
|
||||
.edit()
|
||||
.putLong(getString(R.string.selected_track_key), trackId)
|
||||
.commit();
|
||||
ApiFeatures.getInstance().getApiPlatformAdapter().applyPreferenceChanges(
|
||||
sharedPreferences
|
||||
.edit()
|
||||
.putLong(getString(R.string.selected_track_key), trackId));
|
||||
}
|
||||
|
||||
long getSelectedTrackId() {
|
||||
|
||||
@@ -50,7 +50,7 @@ public abstract class MyTracksConstants {
|
||||
public static final int FEATURE_DETAILS = 5;
|
||||
public static final int START_RECORDING = 6;
|
||||
public static final int STOP_RECORDING = 7;
|
||||
public static final int AUTHENTICATE_TO_DOCS = 8;
|
||||
public static final int AUTHENTICATE_TO_DOCLIST = 8;
|
||||
public static final int AUTHENTICATE_TO_TRIX = 9;
|
||||
public static final int SEND_TO_DOCS = 10;
|
||||
public static final int DELETE_TRACK = 11;
|
||||
@@ -213,6 +213,9 @@ public abstract class MyTracksConstants {
|
||||
}
|
||||
}
|
||||
|
||||
public static final String MAPSHOP_BASE_URL =
|
||||
"http://maps.google.com/maps/ms";
|
||||
|
||||
/**
|
||||
* This is an abstract utility class.
|
||||
*/
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
package com.google.android.apps.mytracks;
|
||||
|
||||
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
|
||||
import com.google.android.apps.mytracks.util.ApiFeatures;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
@@ -63,7 +64,7 @@ public class MyTracksDeleteAllTracks extends Handler {
|
||||
context.getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0);
|
||||
SharedPreferences.Editor editor = prefs.edit();
|
||||
editor.putLong(context.getString(R.string.selected_track_key), -1);
|
||||
editor.commit();
|
||||
ApiFeatures.getInstance().getApiPlatformAdapter().applyPreferenceChanges(editor);
|
||||
if (done != null) {
|
||||
Handler h = new Handler();
|
||||
h.post(done);
|
||||
|
||||
@@ -42,6 +42,8 @@ import android.widget.ImageView;
|
||||
public class MyTracksWaypointDetails extends Activity
|
||||
implements OnClickListener {
|
||||
|
||||
public static final String WAYPOINT_ID_EXTRA = "com.google.android.apps.mytracks.WAYPOINT_ID";
|
||||
|
||||
/**
|
||||
* The id of the way point being edited (taken from bundle, "waypointid")
|
||||
*/
|
||||
@@ -78,7 +80,7 @@ public class MyTracksWaypointDetails extends Activity
|
||||
}
|
||||
|
||||
// Required extra when launching this intent:
|
||||
waypointId = getIntent().getLongExtra("waypointid", -1);
|
||||
waypointId = getIntent().getLongExtra(WAYPOINT_ID_EXTRA, -1);
|
||||
if (waypointId < 0) {
|
||||
Log.d(MyTracksConstants.TAG,
|
||||
"MyTracksWaypointsDetails intent was launched w/o waypoint id.");
|
||||
|
||||
@@ -17,6 +17,7 @@ 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.WaypointCreationRequest;
|
||||
import com.google.android.apps.mytracks.content.WaypointsColumns;
|
||||
import com.google.android.apps.mytracks.util.StringUtils;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
@@ -28,6 +29,8 @@ import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.database.Cursor;
|
||||
import android.os.Bundle;
|
||||
import android.os.RemoteException;
|
||||
import android.util.Log;
|
||||
import android.view.ContextMenu;
|
||||
import android.view.ContextMenu.ContextMenuInfo;
|
||||
import android.view.MenuItem;
|
||||
@@ -87,7 +90,7 @@ public class MyTracksWaypointsList extends ListActivity
|
||||
protected void onListItemClick(ListView l, View v, int position, long id) {
|
||||
Intent result = new Intent();
|
||||
result.putExtra("trackid", trackId);
|
||||
result.putExtra("waypointid", id);
|
||||
result.putExtra(MyTracksWaypointDetails.WAYPOINT_ID_EXTRA, id);
|
||||
setResult(MyTracksConstants.EDIT_WAYPOINT, result);
|
||||
finish();
|
||||
}
|
||||
@@ -103,7 +106,7 @@ public class MyTracksWaypointsList extends ListActivity
|
||||
case MyTracksConstants.MENU_EDIT: {
|
||||
Intent intent = new Intent(this, MyTracksWaypointDetails.class);
|
||||
intent.putExtra("trackid", trackId);
|
||||
intent.putExtra("waypointid", waypointId);
|
||||
intent.putExtra(MyTracksWaypointDetails.WAYPOINT_ID_EXTRA, waypointId);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
}
|
||||
@@ -165,27 +168,34 @@ public class MyTracksWaypointsList extends ListActivity
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
WaypointCreationRequest request;
|
||||
switch (v.getId()) {
|
||||
case R.id.waypointslist_btn_insert_waypoint: {
|
||||
long id = MyTracks.getInstance().insertWaypointMarker();
|
||||
if (id >= 0) {
|
||||
Intent intent = new Intent(this, MyTracksWaypointDetails.class);
|
||||
intent.putExtra("waypointid", id);
|
||||
startActivity(intent);
|
||||
}
|
||||
case R.id.waypointslist_btn_insert_waypoint:
|
||||
request = WaypointCreationRequest.DEFAULT_MARKER;
|
||||
break;
|
||||
}
|
||||
case R.id.waypointslist_btn_insert_statistics: {
|
||||
long id = MyTracks.getInstance().insertStatisticsMarker();
|
||||
if (id >= 0) {
|
||||
Intent intent = new Intent(this, MyTracksWaypointDetails.class);
|
||||
intent.putExtra("waypointid", id);
|
||||
startActivity(intent);
|
||||
}
|
||||
case R.id.waypointslist_btn_insert_statistics:
|
||||
request = WaypointCreationRequest.DEFAULT_STATISTICS;
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
return;
|
||||
}
|
||||
long id;
|
||||
try {
|
||||
id = MyTracks.getInstance().insertWaypoint(request);
|
||||
} catch (RemoteException e) {
|
||||
Log.e(MyTracksConstants.TAG, "Cannot insert marker.", e);
|
||||
return;
|
||||
} catch (IllegalStateException e) {
|
||||
Log.e(MyTracksConstants.TAG, "Cannot insert marker.", e);
|
||||
return;
|
||||
}
|
||||
if (id < 0) {
|
||||
Log.e(MyTracksConstants.TAG, "Failed to insert marker.");
|
||||
return;
|
||||
}
|
||||
Intent intent = new Intent(this, MyTracksWaypointDetails.class);
|
||||
intent.putExtra(MyTracksWaypointDetails.WAYPOINT_ID_EXTRA, id);
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
private void setListAdapter() {
|
||||
|
||||
@@ -20,6 +20,7 @@ import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
@@ -71,6 +72,12 @@ public class WelcomeActivity extends Activity {
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||
builder.setView(view);
|
||||
builder.setPositiveButton(R.string.ok, null);
|
||||
builder.setNeutralButton(R.string.license, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
Eula.showEula(WelcomeActivity.this);
|
||||
}
|
||||
});
|
||||
builder.setIcon(R.drawable.arrow_icon);
|
||||
AlertDialog dialog = builder.create();
|
||||
dialog.show();
|
||||
|
||||
@@ -137,6 +137,7 @@ public class MyTracksProvider extends ContentProvider {
|
||||
@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);
|
||||
@@ -144,11 +145,15 @@ public class MyTracksProvider extends ContentProvider {
|
||||
db.execSQL("DROP TABLE IF EXISTS " + WAYPOINTS_TABLE);
|
||||
onCreate(db);
|
||||
} else {
|
||||
Log.w(TAG, "Upgrading database from version " + oldVersion + " to "
|
||||
+ newVersion + ", adding sensor column.");
|
||||
if (oldVersion == 17) {
|
||||
// Incremental updates go here.
|
||||
// Each time you increase the DB version, add a corresponding if clause.
|
||||
|
||||
// Sensor data.
|
||||
if (oldVersion <= 17) {
|
||||
Log.w(TAG, "Upgrading database from version " + oldVersion + " to "
|
||||
+ newVersion + ", adding sensor column.");
|
||||
db.execSQL("ALTER TABLE " + TRACKPOINTS_TABLE
|
||||
+ " ADD " + TrackPointsColumns.SENSOR + " BLOB");
|
||||
+ " ADD " + TrackPointsColumns.SENSOR + " BLOB");
|
||||
};
|
||||
if (oldVersion >= 17) {
|
||||
db.execSQL("ALTER TABLE " + TRACKS_TABLE
|
||||
|
||||
@@ -24,6 +24,7 @@ import android.location.Location;
|
||||
|
||||
import java.io.OutputStream;
|
||||
import java.io.PrintWriter;
|
||||
import java.nio.charset.Charset;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.TimeZone;
|
||||
@@ -58,8 +59,8 @@ public class GpxTrackWriter implements TrackFormatWriter {
|
||||
@Override
|
||||
public void writeHeader() {
|
||||
if (pw != null) {
|
||||
pw.println("<?xml version=\"1.0\" encoding=\"ISO-8859-1\" ");
|
||||
pw.println("standalone=\"yes\"?>");
|
||||
pw.format("<?xml version=\"1.0\" encoding=\"%s\" standalone=\"yes\"?>\n",
|
||||
Charset.defaultCharset().name());
|
||||
pw.println("<?xml-stylesheet type=\"text/xsl\" href=\"details.xsl\"?>");
|
||||
pw.println("<gpx");
|
||||
pw.println(" version=\"1.0\"");
|
||||
|
||||
@@ -20,26 +20,16 @@ import com.google.android.apps.mytracks.MyTracksConstants;
|
||||
import com.google.android.apps.mytracks.MyTracksSettings;
|
||||
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
|
||||
import com.google.android.apps.mytracks.content.Track;
|
||||
import com.google.android.apps.mytracks.io.docs.DocsHelper;
|
||||
import com.google.android.apps.mytracks.io.gdata.GDataClientFactory;
|
||||
import com.google.android.apps.mytracks.io.gdata.GDataWrapper;
|
||||
import com.google.android.apps.mytracks.io.gdata.GDataWrapper.QueryFunction;
|
||||
import com.google.android.apps.mytracks.stats.TripStatistics;
|
||||
import com.google.android.apps.mytracks.util.ResourceUtils;
|
||||
import com.google.android.apps.mytracks.util.StringUtils;
|
||||
import com.google.android.apps.mytracks.util.UnitConversions;
|
||||
import com.google.android.common.gdata.AndroidXmlParserFactory;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
import com.google.wireless.gdata.client.GDataClient;
|
||||
import com.google.wireless.gdata.client.GDataServiceClient;
|
||||
import com.google.wireless.gdata.client.HttpException;
|
||||
import com.google.wireless.gdata.data.Entry;
|
||||
import com.google.wireless.gdata.docs.DocumentsClient;
|
||||
import com.google.wireless.gdata.docs.SpreadsheetsClient;
|
||||
import com.google.wireless.gdata.docs.SpreadsheetsClient.WorksheetEntry;
|
||||
import com.google.wireless.gdata.maps.xml.XmlMapsGDataParserFactory;
|
||||
import com.google.wireless.gdata.parser.GDataParser;
|
||||
import com.google.wireless.gdata.parser.ParseException;
|
||||
import com.google.wireless.gdata2.client.AuthenticationException;
|
||||
import com.google.wireless.gdata.client.GDataServiceClient;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.SharedPreferences;
|
||||
@@ -47,16 +37,7 @@ import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
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.text.DecimalFormat;
|
||||
import java.text.NumberFormat;
|
||||
|
||||
/**
|
||||
* A helper class used to transmit tracks statistics to Google Docs/Trix.
|
||||
@@ -64,29 +45,15 @@ import java.text.NumberFormat;
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class SendToDocs {
|
||||
private static final NumberFormat LARGE_UNIT_FORMAT =
|
||||
new DecimalFormat("#,###,###.00");
|
||||
private static final NumberFormat SMALL_UNIT_FORMAT =
|
||||
new DecimalFormat("###,###");
|
||||
private static final String DOCS_FEED_URL =
|
||||
"http://docs.google.com/feeds/documents/private/full";
|
||||
private static final String DOCS_MY_SPREADSHEETS_FEED_URL =
|
||||
"http://docs.google.com/feeds/documents/private/full?"
|
||||
+ "category=mine,spreadsheet";
|
||||
private static final String DOCS_SPREADSHEET_URL =
|
||||
"http://docs.google.com/feeds/documents/private/full/spreadsheet%3A";
|
||||
private static final String DOCS_SPREADSHEET_URL_FORMAT =
|
||||
"http://spreadsheets.google.com/feeds/list/%s/%s/private/full";
|
||||
protected static final String DOCS_WORKSHEETS_URL_FORMAT =
|
||||
"http://spreadsheets.google.com/feeds/worksheets/%s/private/full";
|
||||
private static final String CONTENT_TYPE_PARAM = "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";
|
||||
|
||||
/** The GData service name for Google Spreadsheets (aka Trix) */
|
||||
public static final String GDATA_SERVICE_NAME_TRIX = "wise";
|
||||
|
||||
/** The GData service name for the Google Docs Document List */
|
||||
public static final String GDATA_SERVICE_NAME_DOCLIST = "writely";
|
||||
|
||||
private final Activity activity;
|
||||
private final AuthManager wiseAuth;
|
||||
private final AuthManager writelyAuth;
|
||||
private final AuthManager trixAuth;
|
||||
private final AuthManager docListAuth;
|
||||
private final long trackId;
|
||||
private final boolean metricUnits;
|
||||
private final HandlerThread handlerThread;
|
||||
@@ -94,18 +61,15 @@ public class SendToDocs {
|
||||
|
||||
private boolean createdNewSpreadSheet = false;
|
||||
|
||||
private String spreadSheetId = null;
|
||||
private String workSheetId = null;
|
||||
|
||||
private boolean success = true;
|
||||
private String statusMessage = "";
|
||||
private Runnable onCompletion = null;
|
||||
|
||||
public SendToDocs(Activity activity, AuthManager wiseAuth,
|
||||
AuthManager writelyAuth, long trackId) {
|
||||
public SendToDocs(Activity activity, AuthManager trixAuth,
|
||||
AuthManager docListAuth, long trackId) {
|
||||
this.activity = activity;
|
||||
this.wiseAuth = wiseAuth;
|
||||
this.writelyAuth = writelyAuth;
|
||||
this.trixAuth = trixAuth;
|
||||
this.docListAuth = docListAuth;
|
||||
this.trackId = trackId;
|
||||
|
||||
SharedPreferences preferences = activity.getSharedPreferences(
|
||||
@@ -195,22 +159,24 @@ public class SendToDocs {
|
||||
* @param track the track
|
||||
*/
|
||||
private boolean uploadToDocs(Track track) {
|
||||
GDataWrapper<GDataServiceClient> wiseWrapper = new GDataWrapper<GDataServiceClient>();
|
||||
wiseWrapper.setAuthManager(wiseAuth);
|
||||
wiseWrapper.setRetryOnAuthFailure(true);
|
||||
GDataWrapper<GDataServiceClient> docListWrapper = new GDataWrapper<GDataServiceClient>();
|
||||
docListWrapper.setAuthManager(docListAuth);
|
||||
docListWrapper.setRetryOnAuthFailure(true);
|
||||
|
||||
GDataWrapper<GDataServiceClient> trixWrapper = new GDataWrapper<GDataServiceClient>();
|
||||
trixWrapper.setAuthManager(trixAuth);
|
||||
trixWrapper.setRetryOnAuthFailure(true);
|
||||
|
||||
GDataWrapper<GDataServiceClient> writelyWrapper = new GDataWrapper<GDataServiceClient>();
|
||||
writelyWrapper.setAuthManager(writelyAuth);
|
||||
writelyWrapper.setRetryOnAuthFailure(true);
|
||||
DocsHelper docsHelper = new DocsHelper();
|
||||
|
||||
GDataClient androidClient = null;
|
||||
try {
|
||||
androidClient = GDataClientFactory.getGDataClient(activity);
|
||||
SpreadsheetsClient gdataClient = new SpreadsheetsClient(androidClient,
|
||||
new XmlMapsGDataParserFactory(new AndroidXmlParserFactory()));
|
||||
wiseWrapper.setClient(gdataClient);
|
||||
trixWrapper.setClient(gdataClient);
|
||||
Log.d(MyTracksConstants.TAG,
|
||||
"GData connection prepared: " + this.writelyAuth);
|
||||
"GData connection prepared: " + this.docListAuth);
|
||||
String sheetTitle = "My Tracks";
|
||||
|
||||
if (track.getCategory() != null && !track.getCategory().equals("")) {
|
||||
@@ -219,15 +185,19 @@ public class SendToDocs {
|
||||
|
||||
DocumentsClient docsGdataClient = new DocumentsClient(androidClient,
|
||||
new XmlMapsGDataParserFactory(new AndroidXmlParserFactory()));
|
||||
writelyWrapper.setClient(docsGdataClient);
|
||||
docListWrapper.setClient(docsGdataClient);
|
||||
|
||||
// First try to find the spreadsheet:
|
||||
if (!getSpreadsheetId(writelyWrapper, sheetTitle)) {
|
||||
Log.i(MyTracksConstants.TAG, "Spreadsheet lookup failed.");
|
||||
String spreadsheetId = null;
|
||||
try {
|
||||
spreadsheetId = docsHelper.requestSpreadsheetId(docListWrapper,
|
||||
sheetTitle);
|
||||
} catch (IOException e) {
|
||||
Log.i(MyTracksConstants.TAG, "Spreadsheet lookup failed.", e);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (spreadSheetId == null) {
|
||||
if (spreadsheetId == null) {
|
||||
MyTracks.getInstance().setProgressValue(65);
|
||||
// Waiting a few seconds and trying again. Maybe the server just had a
|
||||
// hickup (unfortunately that happens quite a lot...).
|
||||
@@ -236,22 +206,33 @@ public class SendToDocs {
|
||||
} catch (InterruptedException e) {
|
||||
Log.e(MyTracksConstants.TAG, "Sleep interrupted", e);
|
||||
}
|
||||
if (!getSpreadsheetId(writelyWrapper, sheetTitle)) {
|
||||
Log.i(MyTracksConstants.TAG, "2nd spreadsheet lookup failed.");
|
||||
|
||||
try {
|
||||
spreadsheetId = docsHelper.requestSpreadsheetId(docListWrapper,
|
||||
sheetTitle);
|
||||
} catch (IOException e) {
|
||||
Log.i(MyTracksConstants.TAG, "2nd spreadsheet lookup failed.", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// We were unable to find an existing spreadsheet, so create a new one.
|
||||
MyTracks.getInstance().setProgressValue(70);
|
||||
if (spreadSheetId == null) {
|
||||
if (spreadsheetId == null) {
|
||||
Log.i(MyTracksConstants.TAG, "Creating new spreadsheet: " + sheetTitle);
|
||||
|
||||
if (!createSpreadSheet(writelyWrapper, sheetTitle)) {
|
||||
try {
|
||||
spreadsheetId = docsHelper.createSpreadsheet(activity, docListWrapper,
|
||||
sheetTitle);
|
||||
} catch (IOException e) {
|
||||
Log.i(MyTracksConstants.TAG, "Failed to create new spreadsheet "
|
||||
+ sheetTitle, e);
|
||||
return false;
|
||||
}
|
||||
MyTracks.getInstance().setProgressValue(80);
|
||||
createdNewSpreadSheet = true;
|
||||
|
||||
if (spreadSheetId == null) {
|
||||
if (spreadsheetId == null) {
|
||||
MyTracks.getInstance().setProgressValue(85);
|
||||
// The previous creation might have succeeded even though GData
|
||||
// reported an error. Seems to be a know bug,
|
||||
@@ -264,10 +245,16 @@ public class SendToDocs {
|
||||
} catch (InterruptedException e) {
|
||||
Log.e(MyTracksConstants.TAG, "Sleep interrupted", e);
|
||||
}
|
||||
if (!getSpreadsheetId(writelyWrapper, sheetTitle)) {
|
||||
|
||||
try {
|
||||
spreadsheetId = docsHelper.requestSpreadsheetId(docListWrapper,
|
||||
sheetTitle);
|
||||
} catch (IOException e) {
|
||||
Log.i(MyTracksConstants.TAG, "Failed create-failed lookup", e);
|
||||
return false;
|
||||
}
|
||||
if (spreadSheetId == null) {
|
||||
|
||||
if (spreadsheetId == null) {
|
||||
MyTracks.getInstance().setProgressValue(87);
|
||||
// Re-try
|
||||
try {
|
||||
@@ -275,11 +262,16 @@ public class SendToDocs {
|
||||
} catch (InterruptedException e) {
|
||||
Log.e(MyTracksConstants.TAG, "Sleep interrupted", e);
|
||||
}
|
||||
if (!getSpreadsheetId(writelyWrapper, sheetTitle)) {
|
||||
|
||||
try {
|
||||
spreadsheetId = docsHelper.requestSpreadsheetId(docListWrapper,
|
||||
sheetTitle);
|
||||
} catch (IOException e) {
|
||||
Log.i(MyTracksConstants.TAG, "Failed create-failed relookup", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (spreadSheetId == null) {
|
||||
if (spreadsheetId == null) {
|
||||
Log.i(MyTracksConstants.TAG,
|
||||
"Creating new spreadsheet really failed.");
|
||||
return false;
|
||||
@@ -287,14 +279,21 @@ public class SendToDocs {
|
||||
}
|
||||
}
|
||||
|
||||
if (!getWorkSheetId(wiseWrapper)) {
|
||||
Log.i(MyTracksConstants.TAG, "Looking up worksheet id failed.");
|
||||
String worksheetId = null;
|
||||
try {
|
||||
worksheetId = docsHelper.getWorksheetId(trixWrapper, spreadsheetId);
|
||||
if (worksheetId == null) {
|
||||
throw new IOException("Worksheet ID lookup returned empty");
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.i(MyTracksConstants.TAG, "Looking up worksheet id failed.", e);
|
||||
return false;
|
||||
}
|
||||
|
||||
MyTracks.getInstance().setProgressValue(90);
|
||||
|
||||
insertRowNet(track, spreadSheetId, workSheetId);
|
||||
docsHelper.addTrackRow(activity, trixAuth, spreadsheetId, worksheetId,
|
||||
track, metricUnits);
|
||||
Log.i(MyTracksConstants.TAG, "Done uploading to docs.");
|
||||
} catch (IOException e) {
|
||||
Log.e(MyTracksConstants.TAG, "Unable to upload docs.", e);
|
||||
@@ -306,235 +305,4 @@ public class SendToDocs {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean getSpreadsheetId(GDataWrapper<GDataServiceClient> wrapper, final String title) {
|
||||
spreadSheetId = null;
|
||||
return wrapper.runQuery(new QueryFunction<GDataServiceClient>() {
|
||||
@Override
|
||||
public void query(GDataServiceClient client)
|
||||
throws IOException, GDataWrapper.ParseException, GDataWrapper.HttpException {
|
||||
GDataParser listParser;
|
||||
try {
|
||||
listParser = client.getParserForFeed(Entry.class,
|
||||
DOCS_MY_SPREADSHEETS_FEED_URL, writelyAuth.getAuthToken());
|
||||
listParser.init();
|
||||
while (listParser.hasMoreData()) {
|
||||
Entry entry = listParser.readNextEntry(null);
|
||||
String entryTitle = entry.getTitle();
|
||||
Log.i(MyTracksConstants.TAG, "Found docs entry: " + entryTitle);
|
||||
if (entryTitle.equals(title)) {
|
||||
String entryId = entry.getId();
|
||||
int lastSlash = entryId.lastIndexOf('/');
|
||||
spreadSheetId = entryId.substring(lastSlash + 15);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (ParseException e) {
|
||||
throw new GDataWrapper.ParseException(e);
|
||||
} catch (HttpException e) {
|
||||
throw new GDataWrapper.HttpException(e.getStatusCode(), e.getMessage());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private boolean getWorkSheetId(GDataWrapper<GDataServiceClient> wrapper) {
|
||||
workSheetId = null;
|
||||
return wrapper.runQuery(new QueryFunction<GDataServiceClient>() {
|
||||
@Override
|
||||
public void query(GDataServiceClient client)
|
||||
throws GDataWrapper.AuthenticationException, IOException, GDataWrapper.ParseException {
|
||||
String uri = String.format(DOCS_WORKSHEETS_URL_FORMAT, spreadSheetId);
|
||||
GDataParser sheetParser;
|
||||
try {
|
||||
sheetParser = ((SpreadsheetsClient) client).getParserForWorksheetsFeed(uri,
|
||||
wiseAuth.getAuthToken());
|
||||
sheetParser.init();
|
||||
if (!sheetParser.hasMoreData()) {
|
||||
Log.i(MyTracksConstants.TAG, "Found no worksheets");
|
||||
return; // failure
|
||||
}
|
||||
|
||||
// just grab the first.
|
||||
WorksheetEntry worksheetEntry =
|
||||
(WorksheetEntry) sheetParser.readNextEntry(new WorksheetEntry());
|
||||
|
||||
int lastSlash = worksheetEntry.getId().lastIndexOf('/');
|
||||
workSheetId = worksheetEntry.getId().substring(lastSlash + 1);
|
||||
|
||||
} catch (AuthenticationException e) {
|
||||
throw new GDataWrapper.AuthenticationException(e);
|
||||
} catch (ParseException e) {
|
||||
throw new GDataWrapper.ParseException(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new MyTracks spreadsheet with the given name.
|
||||
*/
|
||||
private boolean createSpreadSheet(GDataWrapper<GDataServiceClient> writelyWrapper,
|
||||
final String name) {
|
||||
spreadSheetId = null;
|
||||
|
||||
return writelyWrapper.runQuery(new QueryFunction<GDataServiceClient>() {
|
||||
@Override
|
||||
public void query(GDataServiceClient client) throws IOException {
|
||||
// Construct data
|
||||
// Send data
|
||||
URL url = new URL(DOCS_FEED_URL);
|
||||
URLConnection conn = url.openConnection();
|
||||
conn.addRequestProperty(CONTENT_TYPE_PARAM,
|
||||
OPENDOCUMENT_SPREADSHEET_MIME_TYPE);
|
||||
conn.addRequestProperty("Slug", name);
|
||||
conn.addRequestProperty("Authorization",
|
||||
"GoogleLogin auth=" + writelyAuth.getAuthToken());
|
||||
conn.setDoOutput(true);
|
||||
OutputStream os = conn.getOutputStream();
|
||||
ResourceUtils.readBinaryFileToOutputStream(
|
||||
activity, R.raw.mytracks_empty_spreadsheet, os);
|
||||
|
||||
// Get the response
|
||||
// TODO: The following is a horrible ugly hack.
|
||||
// Hopefully we can retire it when there is a proper gdata api.
|
||||
BufferedReader rd = null;
|
||||
String line;
|
||||
StringBuilder resultBuilder = new StringBuilder();
|
||||
try {
|
||||
rd = new BufferedReader(new InputStreamReader(conn.getInputStream()));
|
||||
while ((line = rd.readLine()) != null) {
|
||||
resultBuilder.append(line);
|
||||
}
|
||||
os.close();
|
||||
rd.close();
|
||||
} catch (FileNotFoundException e) {
|
||||
// The GData API sometimes throws an error, even though creation of
|
||||
// the document succeeded. In that case let's just return. The caller
|
||||
// then needs to check if the doc actually exists.
|
||||
return;
|
||||
} finally {
|
||||
os.close();
|
||||
if (rd != null) {
|
||||
rd.close();
|
||||
}
|
||||
}
|
||||
|
||||
String result = resultBuilder.toString();
|
||||
// Try to find the id.
|
||||
int idTagIndex = result.indexOf("<id>");
|
||||
if (idTagIndex == -1) {
|
||||
return;
|
||||
}
|
||||
int idTagCloseIndex = result.indexOf("</id>", idTagIndex);
|
||||
if (idTagCloseIndex == -1) {
|
||||
return;
|
||||
}
|
||||
int idStringStart = result.indexOf(DOCS_SPREADSHEET_URL, idTagIndex);
|
||||
if (idStringStart == -1) {
|
||||
return;
|
||||
}
|
||||
spreadSheetId = result.substring(
|
||||
idStringStart + DOCS_SPREADSHEET_URL.length(), idTagCloseIndex);
|
||||
Log.i(MyTracksConstants.TAG, "Created new spreadsheet: " + spreadSheetId);
|
||||
}});
|
||||
}
|
||||
|
||||
private void insertRowNet(Track track, String spreadsheetId,
|
||||
String worksheetId) throws IOException {
|
||||
String worksheetUri = String.format(DOCS_SPREADSHEET_URL_FORMAT,
|
||||
spreadsheetId, worksheetId);
|
||||
TripStatistics stats = track.getStatistics();
|
||||
|
||||
/* Prepare the Post-Text we are going to send. */
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("<entry xmlns='http://www.w3.org/2005/Atom' ");
|
||||
sb.append("xmlns:gsx='http://schemas.google.com/spreadsheets/"
|
||||
+ "2006/extended'>");
|
||||
appendTag("name", track.getName(), sb);
|
||||
appendTag("description", track.getDescription(), sb);
|
||||
appendTag("date", String.format("%tc", stats.getStartTime()), sb);
|
||||
appendTag("totaltime", StringUtils.formatTimeAlwaysShowingHours(
|
||||
stats.getTotalTime()), sb);
|
||||
appendTag("movingtime", StringUtils.formatTimeAlwaysShowingHours(
|
||||
stats.getMovingTime()), sb);
|
||||
appendLargeUnitsTag("distance", stats.getTotalDistance() / 1000, sb);
|
||||
appendTag("distanceunit",
|
||||
metricUnits
|
||||
? activity.getString(R.string.kilometer)
|
||||
: activity.getString(R.string.mile),
|
||||
sb);
|
||||
appendLargeUnitsTag("averagespeed", stats.getAverageSpeed() * 3.6, sb);
|
||||
appendLargeUnitsTag("averagemovingspeed",
|
||||
stats.getAverageMovingSpeed() * 3.6, sb);
|
||||
appendLargeUnitsTag("maxspeed", stats.getMaxSpeed() * 3.6, sb);
|
||||
appendTag("speedunit",
|
||||
metricUnits
|
||||
? activity.getString(R.string.kilometer_per_hour)
|
||||
: activity.getString(R.string.mile_per_hour),
|
||||
sb);
|
||||
appendSmallUnitsTag("elevationgain", stats.getTotalElevationGain(), sb);
|
||||
appendSmallUnitsTag("minelevation", stats.getMinElevation(), sb);
|
||||
appendSmallUnitsTag("maxelevation", stats.getMaxElevation(), sb);
|
||||
appendTag("elevationunit",
|
||||
metricUnits
|
||||
? activity.getString(R.string.meter)
|
||||
: activity.getString(R.string.feet),
|
||||
sb);
|
||||
if (track.getMapId().length() > 0) {
|
||||
appendTag("map", SendToFusionTables.getMapVisualizationUrl(track), sb);
|
||||
}
|
||||
sb.append("</entry>");
|
||||
Log.i(MyTracksConstants.TAG,
|
||||
"Inserting at: " + spreadsheetId + " => " + worksheetUri);
|
||||
|
||||
String postText = sb.toString();
|
||||
Log.i(MyTracksConstants.TAG, postText);
|
||||
|
||||
// Send data
|
||||
// No need for a wrapper because we know that the authorization was good
|
||||
// enough to get this far
|
||||
URL url = new URL(worksheetUri);
|
||||
URLConnection conn = url.openConnection();
|
||||
conn.addRequestProperty(CONTENT_TYPE_PARAM, ATOM_FEED_MIME_TYPE);
|
||||
conn.addRequestProperty("Authorization",
|
||||
"GoogleLogin auth=" + wiseAuth.getAuthToken());
|
||||
conn.setDoOutput(true);
|
||||
OutputStreamWriter wr = new OutputStreamWriter(conn.getOutputStream());
|
||||
wr.write(postText);
|
||||
wr.flush();
|
||||
|
||||
// Get the response
|
||||
BufferedReader rd =
|
||||
new BufferedReader(new InputStreamReader(conn.getInputStream()));
|
||||
String line;
|
||||
while ((line = rd.readLine()) != null) {
|
||||
// Process line...
|
||||
Log.i(MyTracksConstants.TAG, "r: " + line);
|
||||
}
|
||||
wr.close();
|
||||
rd.close();
|
||||
|
||||
Log.i(MyTracksConstants.TAG, "Post finished.");
|
||||
}
|
||||
|
||||
private void appendTag(String name, String value, StringBuilder sb) {
|
||||
sb.append("<gsx:");
|
||||
sb.append(name);
|
||||
sb.append('>');
|
||||
sb.append(StringUtils.stringAsCData(value));
|
||||
sb.append("</gsx:");
|
||||
sb.append(name);
|
||||
sb.append('>');
|
||||
}
|
||||
|
||||
private void appendLargeUnitsTag(String name, double d, StringBuilder sb) {
|
||||
double value = metricUnits ? d : (d * UnitConversions.KM_TO_MI);
|
||||
appendTag(name, LARGE_UNIT_FORMAT.format(value), sb);
|
||||
}
|
||||
|
||||
private void appendSmallUnitsTag(String name, double d, StringBuilder sb) {
|
||||
double value = metricUnits ? d : (d * UnitConversions.M_TO_FT);
|
||||
appendTag(name, SMALL_UNIT_FORMAT.format(value), sb);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ import android.os.Build;
|
||||
|
||||
import java.io.OutputStream;
|
||||
import java.io.PrintWriter;
|
||||
import java.nio.charset.Charset;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.TimeZone;
|
||||
@@ -77,7 +78,8 @@ public class TcxTrackWriter implements TrackFormatWriter {
|
||||
if (pw == null) {
|
||||
return;
|
||||
}
|
||||
pw.println("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\" ?>");
|
||||
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\" ");
|
||||
|
||||
@@ -16,9 +16,11 @@
|
||||
package com.google.android.apps.mytracks.io;
|
||||
|
||||
import com.google.android.apps.mytracks.MyTracksConstants;
|
||||
import com.google.android.apps.mytracks.content.MyTracksLocation;
|
||||
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
|
||||
import com.google.android.apps.mytracks.content.Track;
|
||||
import com.google.android.apps.mytracks.content.Waypoint;
|
||||
import com.google.android.apps.mytracks.content.MyTracksProviderUtils.LocationIterator;
|
||||
import com.google.android.apps.mytracks.util.FileUtils;
|
||||
import com.google.android.apps.mytracks.util.MyTracksUtils;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
@@ -266,62 +268,80 @@ public class TrackWriter {
|
||||
private void writeLocations() {
|
||||
boolean wroteFirst = false;
|
||||
boolean segmentOpen = false;
|
||||
Location lastLoc = null, loc = null;
|
||||
boolean isLastValid = false;
|
||||
Cursor locationsCursor =
|
||||
providerUtils.getLocationsCursor(track.getId(), 0, -1, false);
|
||||
|
||||
if (locationsCursor == null || !locationsCursor.moveToFirst()) {
|
||||
Log.w(MyTracksConstants.TAG, "Unable to get any points to write");
|
||||
return;
|
||||
}
|
||||
|
||||
do {
|
||||
if (loc == null) loc = new Location("");
|
||||
providerUtils.fillLocation(locationsCursor, loc);
|
||||
|
||||
boolean isValid = MyTracksUtils.isValidLocation(loc);
|
||||
boolean validSegment = isValid && isLastValid;
|
||||
if (!wroteFirst && validSegment) {
|
||||
// Found the first two consecutive points which are valid
|
||||
writer.writeBeginTrack(lastLoc);
|
||||
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(lastLoc);
|
||||
class TrackWriterLocationFactory implements MyTracksProviderUtils.LocationFactory {
|
||||
Location currentLocation;
|
||||
Location lastLocation;
|
||||
|
||||
@Override
|
||||
public Location createLocation() {
|
||||
if (currentLocation == null) {
|
||||
currentLocation = new MyTracksLocation("");
|
||||
}
|
||||
|
||||
// Write the current point
|
||||
writer.writeLocation(loc);
|
||||
} else {
|
||||
if (segmentOpen) {
|
||||
writer.writeCloseSegment();
|
||||
segmentOpen = false;
|
||||
return currentLocation;
|
||||
}
|
||||
|
||||
public void swapLocations() {
|
||||
Location tmpLoc = lastLocation;
|
||||
lastLocation = currentLocation;
|
||||
currentLocation = tmpLoc;
|
||||
if (currentLocation != null) {
|
||||
currentLocation.reset();
|
||||
}
|
||||
}
|
||||
|
||||
// Swap loc and lastLoc (so lastLoc is reused)
|
||||
Location tmp = lastLoc;
|
||||
lastLoc = loc;
|
||||
loc = tmp;
|
||||
if (loc != null) loc.reset();
|
||||
|
||||
isLastValid = isValid;
|
||||
} while (locationsCursor.moveToNext());
|
||||
|
||||
if (segmentOpen) {
|
||||
writer.writeCloseSegment();
|
||||
segmentOpen = false;
|
||||
}
|
||||
if (wroteFirst) {
|
||||
writer.writeEndTrack(lastLoc);
|
||||
};
|
||||
|
||||
TrackWriterLocationFactory locationFactory = new TrackWriterLocationFactory();
|
||||
LocationIterator it = providerUtils.getLocationIterator(track.getId(), 0, false,
|
||||
locationFactory);
|
||||
try {
|
||||
if (!it.hasNext()) {
|
||||
Log.w(MyTracksConstants.TAG, "Unable to get any points to write");
|
||||
return;
|
||||
}
|
||||
while (it.hasNext()) {
|
||||
Location loc = it.next();
|
||||
|
||||
boolean isValid = MyTracksUtils.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);
|
||||
} 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,370 @@
|
||||
/*
|
||||
* Copyright 2010 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package com.google.android.apps.mytracks.io.docs;
|
||||
|
||||
import com.google.android.apps.mytracks.MyTracksConstants;
|
||||
import com.google.android.apps.mytracks.content.Track;
|
||||
import com.google.android.apps.mytracks.io.AuthManager;
|
||||
import com.google.android.apps.mytracks.io.gdata.GDataWrapper;
|
||||
import com.google.android.apps.mytracks.io.gdata.GDataWrapper.QueryFunction;
|
||||
import com.google.android.apps.mytracks.stats.TripStatistics;
|
||||
import com.google.android.apps.mytracks.util.ResourceUtils;
|
||||
import com.google.android.apps.mytracks.util.StringUtils;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
import com.google.wireless.gdata.client.GDataServiceClient;
|
||||
import com.google.wireless.gdata.client.HttpException;
|
||||
import com.google.wireless.gdata.data.Entry;
|
||||
import com.google.wireless.gdata.docs.SpreadsheetsClient;
|
||||
import com.google.wireless.gdata.docs.SpreadsheetsClient.WorksheetEntry;
|
||||
import com.google.wireless.gdata.parser.GDataParser;
|
||||
import com.google.wireless.gdata.parser.ParseException;
|
||||
import com.google.wireless.gdata2.client.AuthenticationException;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
/**
|
||||
* This class contains helper methods for interacting with Google Docs and
|
||||
* Google Spreadsheets.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
* @author Matthew Simmons
|
||||
*/
|
||||
public class DocsHelper {
|
||||
private static final String DOCS_FEED_URL =
|
||||
"http://docs.google.com/feeds/documents/private/full";
|
||||
private static final String DOCS_SPREADSHEET_URL =
|
||||
"http://docs.google.com/feeds/documents/private/full/spreadsheet%3A";
|
||||
private static final String DOCS_WORKSHEETS_URL_FORMAT =
|
||||
"http://spreadsheets.google.com/feeds/worksheets/%s/private/full";
|
||||
private static final String DOCS_MY_SPREADSHEETS_FEED_URL =
|
||||
"http://docs.google.com/feeds/documents/private/full?category=mine,spreadsheet";
|
||||
private static final String DOCS_SPREADSHEET_URL_FORMAT =
|
||||
"http://spreadsheets.google.com/feeds/list/%s/%s/private/full";
|
||||
|
||||
private static final String CONTENT_TYPE_PARAM = "Content-Type";
|
||||
private static final String OPENDOCUMENT_SPREADSHEET_MIME_TYPE =
|
||||
"application/x-vnd.oasis.opendocument.spreadsheet";
|
||||
private static final String ATOM_FEED_MIME_TYPE = "application/atom+xml";
|
||||
|
||||
/**
|
||||
* Creates a new MyTracks spreadsheet with the given name.
|
||||
*
|
||||
* @param context The context associated with this request.
|
||||
* @param docListWrapper The GData handle for the Document List service.
|
||||
* @param name The name for the newly-created spreadsheet.
|
||||
* @return The spreadsheet ID, if one is created. {@code null} will be
|
||||
* returned if a GData error didn't occur, but no spreadsheet ID was
|
||||
* returned.
|
||||
*/
|
||||
public String createSpreadsheet(final Context context,
|
||||
final GDataWrapper<GDataServiceClient> docListWrapper, final String name) throws IOException {
|
||||
final AtomicReference<String> idSaver = new AtomicReference<String>();
|
||||
|
||||
boolean result = docListWrapper.runQuery(new QueryFunction<GDataServiceClient>() {
|
||||
@Override
|
||||
public void query(GDataServiceClient client) throws IOException {
|
||||
// Construct and send request
|
||||
URL url = new URL(DOCS_FEED_URL);
|
||||
URLConnection conn = url.openConnection();
|
||||
conn.addRequestProperty(CONTENT_TYPE_PARAM,
|
||||
OPENDOCUMENT_SPREADSHEET_MIME_TYPE);
|
||||
conn.addRequestProperty("Slug", name);
|
||||
conn.addRequestProperty("Authorization",
|
||||
"GoogleLogin auth=" +
|
||||
docListWrapper.getAuthManager().getAuthToken());
|
||||
conn.setDoOutput(true);
|
||||
OutputStream os = conn.getOutputStream();
|
||||
ResourceUtils.readBinaryFileToOutputStream(
|
||||
context, R.raw.mytracks_empty_spreadsheet, os);
|
||||
|
||||
// Get the response
|
||||
// TODO: The following is a horrible ugly hack.
|
||||
// Hopefully we can retire it when there is a proper gdata api.
|
||||
BufferedReader rd = null;
|
||||
String line;
|
||||
StringBuilder resultBuilder = new StringBuilder();
|
||||
try {
|
||||
rd = new BufferedReader(new InputStreamReader(conn.getInputStream()));
|
||||
while ((line = rd.readLine()) != null) {
|
||||
resultBuilder.append(line);
|
||||
}
|
||||
os.close();
|
||||
rd.close();
|
||||
} catch (FileNotFoundException e) {
|
||||
// The GData API sometimes throws an error, even though creation of
|
||||
// the document succeeded. In that case let's just return. The caller
|
||||
// then needs to check if the doc actually exists.
|
||||
return;
|
||||
} finally {
|
||||
os.close();
|
||||
if (rd != null) {
|
||||
rd.close();
|
||||
}
|
||||
}
|
||||
|
||||
String result = resultBuilder.toString();
|
||||
// Try to find the id.
|
||||
int idTagIndex = result.indexOf("<id>");
|
||||
if (idTagIndex == -1) {
|
||||
return;
|
||||
}
|
||||
int idTagCloseIndex = result.indexOf("</id>", idTagIndex);
|
||||
if (idTagCloseIndex == -1) {
|
||||
return;
|
||||
}
|
||||
int idStringStart = result.indexOf(DOCS_SPREADSHEET_URL, idTagIndex);
|
||||
if (idStringStart == -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
String id = result.substring(
|
||||
idStringStart + DOCS_SPREADSHEET_URL.length(), idTagCloseIndex);
|
||||
Log.i(MyTracksConstants.TAG, "Created new spreadsheet: " + id);
|
||||
idSaver.set(id);
|
||||
}});
|
||||
|
||||
if (!result) {
|
||||
throw newIOException(docListWrapper,
|
||||
"Failed to create new spreadsheet.");
|
||||
}
|
||||
|
||||
return idSaver.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the ID of a spreadsheet with the given name.
|
||||
*
|
||||
* @param docListWrapper The GData handle for the Document List service.
|
||||
* @param title The name of the spreadsheet whose ID is to be retrieved.
|
||||
* @return The spreadsheet ID, if it can be retrieved. {@code null} will
|
||||
* be returned if no spreadsheet exists by the given name.
|
||||
* @throws IOException If an error occurs during the GData request.
|
||||
*/
|
||||
public String requestSpreadsheetId(final GDataWrapper<GDataServiceClient> docListWrapper,
|
||||
final String title) throws IOException {
|
||||
final AtomicReference<String> idSaver = new AtomicReference<String>();
|
||||
|
||||
boolean result = docListWrapper.runQuery(new QueryFunction<GDataServiceClient>() {
|
||||
@Override
|
||||
public void query(GDataServiceClient client)
|
||||
throws IOException, GDataWrapper.ParseException, GDataWrapper.HttpException {
|
||||
GDataParser listParser;
|
||||
try {
|
||||
listParser = client.getParserForFeed(Entry.class,
|
||||
DOCS_MY_SPREADSHEETS_FEED_URL,
|
||||
docListWrapper.getAuthManager().getAuthToken());
|
||||
listParser.init();
|
||||
|
||||
while (listParser.hasMoreData()) {
|
||||
Entry entry = listParser.readNextEntry(null);
|
||||
String entryTitle = entry.getTitle();
|
||||
Log.i(MyTracksConstants.TAG, "Found docs entry: " + entryTitle);
|
||||
if (entryTitle.equals(title)) {
|
||||
String entryId = entry.getId();
|
||||
int lastSlash = entryId.lastIndexOf('/');
|
||||
idSaver.set(entryId.substring(lastSlash + 15));
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (ParseException e) {
|
||||
throw new GDataWrapper.ParseException(e);
|
||||
} catch (HttpException e) {
|
||||
throw new GDataWrapper.HttpException(e.getStatusCode(), e.getMessage());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
throw newIOException(docListWrapper,
|
||||
"Failed to retrieve spreadsheet list.");
|
||||
}
|
||||
|
||||
return idSaver.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the ID of the first worksheet in the named spreadsheet.
|
||||
*
|
||||
* @param trixWrapper The GData handle for the spreadsheet service.
|
||||
* @param spreadsheetId The GData ID for the given spreadsheet.
|
||||
* @return The worksheet ID, if it can be retrieved. {@code null} will be
|
||||
* returned if the GData request returns without error, but without an
|
||||
* ID.
|
||||
* @throws IOException If an error occurs during the GData request.
|
||||
*/
|
||||
public String getWorksheetId(final GDataWrapper<GDataServiceClient> trixWrapper,
|
||||
final String spreadsheetId) throws IOException {
|
||||
final AtomicReference<String> idSaver = new AtomicReference<String>();
|
||||
|
||||
boolean result = trixWrapper.runQuery(new QueryFunction<GDataServiceClient>() {
|
||||
@Override
|
||||
public void query(GDataServiceClient client)
|
||||
throws GDataWrapper.AuthenticationException, IOException, GDataWrapper.ParseException, GDataWrapper.HttpException {
|
||||
String uri = String.format(DOCS_WORKSHEETS_URL_FORMAT, spreadsheetId);
|
||||
GDataParser sheetParser;
|
||||
try {
|
||||
sheetParser = ((SpreadsheetsClient) client).getParserForWorksheetsFeed(uri,
|
||||
trixWrapper.getAuthManager().getAuthToken());
|
||||
sheetParser.init();
|
||||
if (!sheetParser.hasMoreData()) {
|
||||
Log.i(MyTracksConstants.TAG, "Found no worksheets");
|
||||
return;
|
||||
}
|
||||
|
||||
// Grab the first worksheet.
|
||||
WorksheetEntry worksheetEntry =
|
||||
(WorksheetEntry) sheetParser.readNextEntry(new WorksheetEntry());
|
||||
|
||||
int lastSlash = worksheetEntry.getId().lastIndexOf('/');
|
||||
idSaver.set(worksheetEntry.getId().substring(lastSlash + 1));
|
||||
} catch (ParseException e) {
|
||||
throw new GDataWrapper.ParseException(e);
|
||||
} catch (AuthenticationException e) {
|
||||
throw new GDataWrapper.AuthenticationException(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
throw newIOException(trixWrapper, "Failed to retrieve worksheet ID.");
|
||||
}
|
||||
|
||||
return idSaver.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a row to a worksheet containing the stats for a given track.
|
||||
*
|
||||
* @param context The context associated with this request.
|
||||
* @param trixAuth The GData authorization for the spreadsheet service.
|
||||
* @param spreadsheetId The spreadsheet to be modified.
|
||||
* @param worksheetId The worksheet to be modified.
|
||||
* @param track The track whose stats are to be written.
|
||||
* @param metricUnits True if metric units are to be used. If false,
|
||||
* imperial units will be used.
|
||||
* @throws IOException If an error occurs while updating the worksheet.
|
||||
*/
|
||||
public void addTrackRow(Context context, AuthManager trixAuth,
|
||||
String spreadsheetId, String worksheetId, Track track,
|
||||
boolean metricUnits) throws IOException {
|
||||
|
||||
String worksheetUri = String.format(DOCS_SPREADSHEET_URL_FORMAT,
|
||||
spreadsheetId, worksheetId);
|
||||
TripStatistics stats = track.getStatistics();
|
||||
|
||||
String distanceUnit = context.getString(metricUnits ?
|
||||
R.string.kilometer : R.string.mile);
|
||||
String speedUnit = context.getString(metricUnits ?
|
||||
R.string.kilometer_per_hour : R.string.mile_per_hour);
|
||||
String elevationUnit = context.getString(metricUnits ?
|
||||
R.string.meter : R.string.feet);
|
||||
|
||||
// Prepare the Post-Text we are going to send.
|
||||
DocsTagBuilder tagBuilder = new DocsTagBuilder(metricUnits)
|
||||
.append("name", track.getName())
|
||||
.append("description", track.getDescription())
|
||||
.append("date", String.format("%tc", stats.getStartTime()))
|
||||
.append("totaltime", StringUtils.formatTimeAlwaysShowingHours(
|
||||
stats.getTotalTime()))
|
||||
.append("movingtime", StringUtils.formatTimeAlwaysShowingHours(
|
||||
stats.getMovingTime()))
|
||||
.appendLargeUnits("distance", stats.getTotalDistance() / 1000)
|
||||
.append("distanceunit", distanceUnit)
|
||||
.appendLargeUnits("averagespeed", stats.getAverageSpeed() * 3.6)
|
||||
.appendLargeUnits("averagemovingspeed",
|
||||
stats.getAverageMovingSpeed() * 3.6)
|
||||
.appendLargeUnits("maxspeed", stats.getMaxSpeed() * 3.6)
|
||||
.append("speedunit", speedUnit)
|
||||
.appendSmallUnits("elevationgain", stats.getTotalElevationGain())
|
||||
.appendSmallUnits("minelevation", stats.getMinElevation())
|
||||
.appendSmallUnits("maxelevation", stats.getMaxElevation())
|
||||
.append("elevationunit", elevationUnit);
|
||||
|
||||
if (track.getMapId().length() > 0) {
|
||||
tagBuilder.append("map", String.format("%s?msa=0&msid=%s",
|
||||
MyTracksConstants.MAPSHOP_BASE_URL, track.getMapId()));
|
||||
}
|
||||
|
||||
String postText = new StringBuilder()
|
||||
.append("<entry xmlns='http://www.w3.org/2005/Atom' "
|
||||
+ "xmlns:gsx='http://schemas.google.com/spreadsheets/"
|
||||
+ "2006/extended'>")
|
||||
.append(tagBuilder.build())
|
||||
.append("</entry>")
|
||||
.toString();
|
||||
|
||||
Log.i(MyTracksConstants.TAG,
|
||||
"Inserting at: " + spreadsheetId + " => " + worksheetUri);
|
||||
|
||||
Log.i(MyTracksConstants.TAG, postText);
|
||||
|
||||
writeRowData(trixAuth, worksheetUri, postText);
|
||||
|
||||
Log.i(MyTracksConstants.TAG, "Post finished.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes spreadsheet row data to the indicated worksheet.
|
||||
*
|
||||
* @param trixAuth The GData authorization for the spreadsheet service.
|
||||
* @param worksheetUri The URI of the worksheet to be altered.
|
||||
* @param postText The XML tags describing the change to be made.
|
||||
* @throws IOException Thrown if an error occurs during the write.
|
||||
*/
|
||||
protected void writeRowData(AuthManager trixAuth, String worksheetUri,
|
||||
String postText) throws IOException {
|
||||
// No need for a wrapper because we know that the authorization was good
|
||||
// enough to get this far.
|
||||
URL url = new URL(worksheetUri);
|
||||
URLConnection conn = url.openConnection();
|
||||
conn.addRequestProperty(CONTENT_TYPE_PARAM, ATOM_FEED_MIME_TYPE);
|
||||
conn.addRequestProperty("Authorization",
|
||||
"GoogleLogin auth=" + trixAuth.getAuthToken());
|
||||
conn.setDoOutput(true);
|
||||
OutputStreamWriter wr = new OutputStreamWriter(conn.getOutputStream());
|
||||
wr.write(postText);
|
||||
wr.flush();
|
||||
|
||||
// Get the response.
|
||||
// TODO: Should we parse the response, rather than simply throwing it away?
|
||||
BufferedReader rd =
|
||||
new BufferedReader(new InputStreamReader(conn.getInputStream()));
|
||||
String line;
|
||||
while ((line = rd.readLine()) != null) {
|
||||
// Process line.
|
||||
Log.i(MyTracksConstants.TAG, "r: " + line);
|
||||
}
|
||||
wr.close();
|
||||
rd.close();
|
||||
}
|
||||
|
||||
private static IOException newIOException(GDataWrapper wrapper,
|
||||
String message) {
|
||||
return new IOException(String.format("%s: %d: %s", message,
|
||||
wrapper.getErrorType(), wrapper.getErrorMessage()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* Copyright 2010 Google Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package com.google.android.apps.mytracks.io.docs;
|
||||
|
||||
import com.google.android.apps.mytracks.util.StringUtils;
|
||||
import com.google.android.apps.mytracks.util.UnitConversions;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.NumberFormat;
|
||||
|
||||
/**
|
||||
* <p>This class builds a string of XML tags used to talk to Docs using GData.
|
||||
*
|
||||
* <p>Sample Usage:
|
||||
*
|
||||
* <code>
|
||||
* String tags = new DocsTagBuilder(false)
|
||||
* .append("tagName", "tagValue")
|
||||
* .appendLargeUnits("bigTagName", 1.0)
|
||||
* .build();
|
||||
* </code>
|
||||
*
|
||||
* <p>results in:
|
||||
*
|
||||
* <code>
|
||||
* <gsx:tagName><![CDATA[tagValue]]></gsx:tagName>
|
||||
* <gsx:bigTagName><![CDATA[1.00]]></gsx:bigTagName>
|
||||
* </code>
|
||||
*
|
||||
* @author Matthew Simmons
|
||||
*/
|
||||
class DocsTagBuilder {
|
||||
// TODO(simmonmt): These formats aren't I18N-compatible. Not everyone uses
|
||||
// commas for thousands and dots for the decimal point.
|
||||
private static final NumberFormat LARGE_UNIT_FORMAT =
|
||||
new DecimalFormat("#,###,###.00");
|
||||
private static final NumberFormat SMALL_UNIT_FORMAT =
|
||||
new DecimalFormat("###,###");
|
||||
|
||||
protected final boolean metricUnits;
|
||||
protected final StringBuilder stringBuilder;
|
||||
|
||||
/**
|
||||
* @param metricUnits True if metric units are to be used. If false,
|
||||
* imperial units will be used.
|
||||
*/
|
||||
DocsTagBuilder(boolean metricUnits) {
|
||||
this.metricUnits = metricUnits;
|
||||
stringBuilder = new StringBuilder();
|
||||
}
|
||||
|
||||
/** Appends a tag containing a string value */
|
||||
DocsTagBuilder append(String name, String value) {
|
||||
appendTag(name, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends a tag containing a numeric value. The value will be formatted
|
||||
* according to the large distance of the specified measurement system (i.e.
|
||||
* kilometers or miles).
|
||||
*
|
||||
* @param name The tag name.
|
||||
* @param d The value to be formatted, in kilometers.
|
||||
*/
|
||||
DocsTagBuilder appendLargeUnits(String name, double d) {
|
||||
double value = metricUnits ? d : (d * UnitConversions.KM_TO_MI);
|
||||
appendTag(name, LARGE_UNIT_FORMAT.format(value));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends a tag containing a numeric value. The value will be formatted
|
||||
* according to the small distance of the specified measurement system (i.e.
|
||||
* meters or feet).
|
||||
*
|
||||
* @param name The tag name.
|
||||
* @param d The value to be formatted, in meters.
|
||||
*/
|
||||
DocsTagBuilder appendSmallUnits(String name, double d) {
|
||||
double value = metricUnits ? d : (d * UnitConversions.M_TO_FT);
|
||||
appendTag(name, SMALL_UNIT_FORMAT.format(value));
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Returns a string containing all tags which have been added. */
|
||||
String build() {
|
||||
return stringBuilder.toString();
|
||||
}
|
||||
|
||||
private void appendTag(String name, String value) {
|
||||
stringBuilder.append("<gsx:")
|
||||
.append(name)
|
||||
.append(">")
|
||||
.append(StringUtils.stringAsCData(value))
|
||||
.append("</gsx:")
|
||||
.append(name)
|
||||
.append(">");
|
||||
}
|
||||
}
|
||||
@@ -289,6 +289,10 @@ public class GDataWrapper<C> {
|
||||
this.auth = auth;
|
||||
}
|
||||
|
||||
public AuthManager getAuthManager() {
|
||||
return auth;
|
||||
}
|
||||
|
||||
public void setRetryOnAuthFailure(boolean retry) {
|
||||
retryOnAuthFailure = retry;
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
package com.google.android.apps.mytracks.services;
|
||||
|
||||
import com.google.android.apps.mytracks.MyTracksConstants;
|
||||
import com.google.android.apps.mytracks.content.WaypointCreationRequest;
|
||||
import com.google.android.apps.mytracks.util.UnitConversions;
|
||||
|
||||
import android.util.Log;
|
||||
@@ -111,7 +112,7 @@ public class SplitManager {
|
||||
}
|
||||
|
||||
if (distance > this.nextSplitDistance) {
|
||||
service.insertStatisticsMarker(service.getLastLocation());
|
||||
service.insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS);
|
||||
calculateNextSplit();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
|
||||
package com.google.android.apps.mytracks.services;
|
||||
|
||||
import com.google.android.apps.mytracks.content.WaypointCreationRequest;
|
||||
|
||||
|
||||
/**
|
||||
* A simple task to insert statistics markers every n minutes.
|
||||
* @author Sandor Dornbush
|
||||
@@ -24,7 +27,7 @@ public class TimeSplitTask implements PeriodicTask {
|
||||
|
||||
@Override
|
||||
public void run(TrackRecordingService service) {
|
||||
service.insertStatisticsMarker(service.getLastLocation());
|
||||
service.insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -26,6 +26,7 @@ import com.google.android.apps.mytracks.content.Sensor;
|
||||
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.WaypointCreationRequest;
|
||||
import com.google.android.apps.mytracks.content.WaypointsColumns;
|
||||
import com.google.android.apps.mytracks.content.Sensor.SensorDataSet;
|
||||
import com.google.android.apps.mytracks.services.sensors.SensorManager;
|
||||
@@ -70,9 +71,6 @@ import java.util.TimerTask;
|
||||
*/
|
||||
public class TrackRecordingService extends Service implements LocationListener {
|
||||
|
||||
private static final String STATISTICS_ICON_URL =
|
||||
"http://maps.google.com/mapfiles/ms/micons/ylw-pushpin.png";
|
||||
|
||||
static final int MAX_AUTO_RESUME_TRACK_RETRY_ATTEMPTS = 3;
|
||||
|
||||
private NotificationManager notificationManager;
|
||||
@@ -429,7 +427,7 @@ public class TrackRecordingService extends Service implements LocationListener {
|
||||
lastValidLocation = null;
|
||||
|
||||
Waypoint waypoint = providerUtils.getFirstWaypoint(recordingTrackId);
|
||||
if (waypoint != null) {
|
||||
if (waypoint != null && waypoint.getStatistics() != null) {
|
||||
currentWaypointId = waypoint.getId();
|
||||
waypointStatsBuilder = new TripStatisticsBuilder(
|
||||
waypoint.getStatistics());
|
||||
@@ -646,7 +644,6 @@ public class TrackRecordingService extends Service implements LocationListener {
|
||||
locationManager = (LocationManager) getSystemService(LOCATION_SERVICE);
|
||||
splitManager = new SplitManager(this);
|
||||
|
||||
sensorManager = SensorManagerFactory.getSensorManager(this);
|
||||
prefManager = new PreferenceManager(this);
|
||||
registerLocationListener();
|
||||
|
||||
@@ -733,7 +730,7 @@ public class TrackRecordingService extends Service implements LocationListener {
|
||||
splitManager.shutdown();
|
||||
splitManager = null;
|
||||
if (sensorManager != null) {
|
||||
sensorManager.onDestroy();
|
||||
sensorManager.shutdown();
|
||||
sensorManager = null;
|
||||
}
|
||||
|
||||
@@ -860,50 +857,62 @@ public class TrackRecordingService extends Service implements LocationListener {
|
||||
return isRecording;
|
||||
}
|
||||
|
||||
public long insertWaypointMarker(Waypoint waypoint) {
|
||||
public long insertWaypoint(WaypointCreationRequest request) {
|
||||
if (!isRecording()) {
|
||||
throw new IllegalStateException(
|
||||
"Unable to insert waypoint marker while not recording!");
|
||||
}
|
||||
|
||||
if (waypoint.getLocation() == null) {
|
||||
if (lastValidLocation == null) {
|
||||
Log.w(MyTracksConstants.TAG, "Cannot insert waypoint with no location");
|
||||
return -1;
|
||||
}
|
||||
|
||||
waypoint.setLocation(lastValidLocation);
|
||||
if (request == null) {
|
||||
request = WaypointCreationRequest.DEFAULT_MARKER;
|
||||
}
|
||||
|
||||
if (waypoint.getTrackId() < 0) {
|
||||
waypoint.setTrackId(recordingTrackId);
|
||||
Waypoint wpt = new Waypoint();
|
||||
switch (request.getType()) {
|
||||
case MARKER:
|
||||
buildMarker(wpt, request);
|
||||
break;
|
||||
case STATISTICS:
|
||||
buildStatisticsMarker(wpt);
|
||||
break;
|
||||
}
|
||||
|
||||
waypoint.setLength(length);
|
||||
waypoint.setDuration(waypoint.getLocation().getTime()
|
||||
- statsBuilder.getStatistics().getStartTime());
|
||||
Uri uri = providerUtils.insertWaypoint(waypoint);
|
||||
wpt.setTrackId(recordingTrackId);
|
||||
wpt.setLength(length);
|
||||
if (lastValidLocation != null) {
|
||||
// A null location is ok, and expected on track start.
|
||||
wpt.setLocation(lastLocation);
|
||||
wpt.setDuration(lastLocation.getTime()
|
||||
- statsBuilder.getStatistics().getStartTime());
|
||||
}
|
||||
Uri uri = providerUtils.insertWaypoint(wpt);
|
||||
return Long.parseLong(uri.getLastPathSegment());
|
||||
}
|
||||
|
||||
private void buildMarker(Waypoint wpt, WaypointCreationRequest request) {
|
||||
wpt.setType(Waypoint.TYPE_WAYPOINT);
|
||||
if (request.getIconUrl() == null) {
|
||||
wpt.setIcon(getString(R.string.waypoint_icon_url));
|
||||
} else {
|
||||
wpt.setIcon(request.getIconUrl());
|
||||
}
|
||||
if (request.getName() == null) {
|
||||
wpt.setName(getString(R.string.waypoint));
|
||||
} else {
|
||||
wpt.setName(request.getName());
|
||||
}
|
||||
if (request.getDescription() != null) {
|
||||
wpt.setDescription(request.getDescription());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a statistics marker. A statistics marker holds the stats for the
|
||||
* last segment up to this marker.
|
||||
* Build a statistics marker.
|
||||
* A statistics marker holds the stats for the* last segment up to this marker.
|
||||
*
|
||||
* @param location the location where to insert
|
||||
* @param Waypoint The waypoint which will be populated with stats data.
|
||||
* @return the unique id of the inserted marker
|
||||
*/
|
||||
public long insertStatisticsMarker(Location location) {
|
||||
if (!isRecording()) {
|
||||
throw new IllegalStateException(
|
||||
"Unable to insert statistics marker while not recording!");
|
||||
}
|
||||
|
||||
private void buildStatisticsMarker(Waypoint waypoint) {
|
||||
StringUtils utils = new StringUtils(TrackRecordingService.this);
|
||||
|
||||
// Create a new waypoint to save
|
||||
Waypoint waypoint = new Waypoint();
|
||||
|
||||
// Set stop and total time in the stats data
|
||||
final long time = System.currentTimeMillis();
|
||||
waypointStatsBuilder.pauseAt(time);
|
||||
@@ -913,24 +922,18 @@ public class TrackRecordingService extends Service implements LocationListener {
|
||||
waypoint.setDuration(time - statsBuilder.getStatistics().getStartTime());
|
||||
|
||||
// Set the rest of the waypoint data
|
||||
waypoint.setTrackId(recordingTrackId);
|
||||
waypoint.setType(Waypoint.TYPE_STATISTICS);
|
||||
waypoint.setName(TrackRecordingService.this.getString(R.string.statistics));
|
||||
waypoint.setName(getString(R.string.statistics));
|
||||
waypoint.setStatistics(waypointStatsBuilder.getStatistics());
|
||||
waypoint.setDescription(utils.generateWaypointDescription(waypoint));
|
||||
waypoint.setLocation(location);
|
||||
waypoint.setIcon(STATISTICS_ICON_URL);
|
||||
waypoint.setLength(length);
|
||||
waypoint.setIcon(getString(R.string.stats_icon_url));
|
||||
|
||||
waypoint.setStartId(providerUtils.getLastLocationId(recordingTrackId));
|
||||
Uri uri = providerUtils.insertWaypoint(waypoint);
|
||||
|
||||
// Create a new stats keeper for the next marker
|
||||
// Create a new stats keeper for the next marker.
|
||||
waypointStatsBuilder = new TripStatisticsBuilder(time);
|
||||
updateCurrentWaypoint();
|
||||
return Long.parseLong(uri.getLastPathSegment());
|
||||
}
|
||||
|
||||
|
||||
private ServiceBinder binder = new ServiceBinder(this);
|
||||
|
||||
/**
|
||||
@@ -961,79 +964,54 @@ public class TrackRecordingService extends Service implements LocationListener {
|
||||
|
||||
@Override
|
||||
public boolean isRecording() {
|
||||
checkService();
|
||||
return service.isRecording();
|
||||
}
|
||||
|
||||
private void checkService() {
|
||||
if (service == null) {
|
||||
throw new IllegalStateException("The service has been already detached!");
|
||||
}
|
||||
return service.isRecording();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getRecordingTrackId() {
|
||||
if (service == null) {
|
||||
throw new IllegalStateException("The service has been already detached!");
|
||||
}
|
||||
checkService();
|
||||
return service.recordingTrackId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasRecorded() {
|
||||
if (service == null) {
|
||||
throw new IllegalStateException("The service has been already detached!");
|
||||
}
|
||||
checkService();
|
||||
return service.providerUtils.getLastTrackId() >= 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long startNewTrack() {
|
||||
if (service == null) {
|
||||
throw new IllegalStateException("The service has been already detached!");
|
||||
}
|
||||
checkService();
|
||||
return service.startNewTrack();
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert the given waypoint marker. Users can insert waypoint markers
|
||||
* to tag locations with a name, description, category etc.
|
||||
* Inserts a waypoint marker in the track being recorded.
|
||||
*
|
||||
* @param waypoint a waypoint
|
||||
* @return the unique id of the inserted marker
|
||||
* @param request Details of the waypoint to insert
|
||||
* @return the unique ID of the inserted marker
|
||||
*/
|
||||
@Override
|
||||
public long insertWaypointMarker(Waypoint waypoint) {
|
||||
if (service == null) {
|
||||
throw new IllegalStateException("The service has been already detached!");
|
||||
}
|
||||
return service.insertWaypointMarker(waypoint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert a statistics marker. A statistics marker holds the stats for
|
||||
* the last segment up to this marker.
|
||||
*
|
||||
* @param location the location where to insert
|
||||
* @return the unique id of the inserted marker
|
||||
*/
|
||||
@Override
|
||||
public long insertStatisticsMarker(Location location) {
|
||||
if (service == null) {
|
||||
throw new IllegalStateException("The service has been already detached!");
|
||||
}
|
||||
return service.insertStatisticsMarker(location);
|
||||
public long insertWaypoint(WaypointCreationRequest request) {
|
||||
checkService();
|
||||
return service.insertWaypoint(request);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void endCurrentTrack() {
|
||||
if (service == null) {
|
||||
throw new IllegalStateException("The service has been already detached!");
|
||||
}
|
||||
checkService();
|
||||
service.endCurrentTrack();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteAllTracks() {
|
||||
if (service == null) {
|
||||
throw new IllegalStateException("The service has been already detached!");
|
||||
}
|
||||
checkService();
|
||||
if (isRecording()) {
|
||||
throw new IllegalStateException("Cannot delete all tracks while recording!");
|
||||
}
|
||||
@@ -1042,14 +1020,13 @@ public class TrackRecordingService extends Service implements LocationListener {
|
||||
|
||||
@Override
|
||||
public void recordLocation(Location loc) {
|
||||
if (service == null) {
|
||||
throw new IllegalStateException("The service has been already detached!");
|
||||
}
|
||||
checkService();
|
||||
service.onLocationChanged(loc);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] getSensorData() {
|
||||
checkService();
|
||||
if (service.sensorManager == null) {
|
||||
Log.d(MyTracksConstants.TAG, "No sensor manager for data.");
|
||||
return null;
|
||||
@@ -1063,6 +1040,7 @@ public class TrackRecordingService extends Service implements LocationListener {
|
||||
|
||||
@Override
|
||||
public int getSensorState() {
|
||||
checkService();
|
||||
if (service.sensorManager == null) {
|
||||
Log.d(MyTracksConstants.TAG, "No sensor manager for data.");
|
||||
return Sensor.SensorState.NONE.getNumber();
|
||||
@@ -1095,12 +1073,13 @@ public class TrackRecordingService extends Service implements LocationListener {
|
||||
providerUtils.updateTrack(track);
|
||||
statsBuilder = new TripStatisticsBuilder(startTime);
|
||||
waypointStatsBuilder = new TripStatisticsBuilder(startTime);
|
||||
currentWaypointId = insertStatisticsMarker(null);
|
||||
currentWaypointId = insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS);
|
||||
setUpAnnouncer();
|
||||
length = 0;
|
||||
showNotification();
|
||||
registerLocationListener();
|
||||
splitManager.restore();
|
||||
sensorManager = SensorManagerFactory.getSensorManager(this);
|
||||
if (sensorManager != null) {
|
||||
sensorManager.onStartTrack();
|
||||
}
|
||||
@@ -1149,6 +1128,7 @@ public class TrackRecordingService extends Service implements LocationListener {
|
||||
|
||||
if (sensorManager != null) {
|
||||
sensorManager.shutdown();
|
||||
sensorManager = null;
|
||||
}
|
||||
|
||||
releaseWakeLock();
|
||||
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
/*
|
||||
* 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.services.sensors;
|
||||
|
||||
import static com.google.android.apps.mytracks.MyTracksConstants.TAG;
|
||||
import com.google.android.apps.mytracks.MyTracksSettings;
|
||||
import com.google.android.apps.mytracks.content.Sensor;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import com.dsi.ant.AntDefine;
|
||||
import com.dsi.ant.AntMesg;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* A sensor manager to connect ANT+ sensors.
|
||||
* This can include heart rate monitors.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class AntDirectSensorManager extends AntSensorManager {
|
||||
|
||||
/*
|
||||
* These constants are defined by the ANT+ heart rate monitor spec.
|
||||
*/
|
||||
public static final byte HRM_CHANNEL = 0;
|
||||
public static final byte NETWORK_NUMBER = 1;
|
||||
public static final byte HEART_RATE_DEVICE_TYPE = 120;
|
||||
public static final byte POWER_DEVICE_TYPE = 11;
|
||||
public static final byte MANUFACTURER_ID = 1;
|
||||
public static final short CHANNEL_PERIOD = 8070;
|
||||
public static final byte RF_FREQUENCY = 57;
|
||||
|
||||
private short deviceNumberHRM;
|
||||
|
||||
public AntDirectSensorManager(Context context) {
|
||||
super(context);
|
||||
|
||||
deviceNumberHRM = WILDCARD;
|
||||
|
||||
// First read the the device id that we will be pairing with.
|
||||
SharedPreferences prefs = context.getSharedPreferences(
|
||||
MyTracksSettings.SETTINGS_NAME, 0);
|
||||
if (prefs != null) {
|
||||
deviceNumberHRM =
|
||||
(short) prefs.getInt(context.getString(R.string.ant_heart_rate_sensor_id_key), 0);
|
||||
}
|
||||
Log.i(TAG, "Pairing with heart rate monitor: " + deviceNumberHRM);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(byte[] antMessage) {
|
||||
int channel =
|
||||
antMessage[AntMesg.MESG_DATA_OFFSET] & AntDefine.CHANNEL_NUMBER_MASK;
|
||||
switch (channel) {
|
||||
case HRM_CHANNEL:
|
||||
antDecodeHRM(antMessage);
|
||||
break;
|
||||
default:
|
||||
Log.d(TAG, "Unhandled message: " + channel);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode an ant heart rate monitor message.
|
||||
* @param antMessage The byte array received from the heart rate monitor.
|
||||
*/
|
||||
private void antDecodeHRM(byte[] antMessage) {
|
||||
// message ID == broadcast
|
||||
switch (antMessage[AntMesg.MESG_ID_OFFSET]) {
|
||||
case AntMesg.MESG_BROADCAST_DATA_ID:
|
||||
handleBroadcastData(antMessage);
|
||||
break;
|
||||
case AntMesg.MESG_RESPONSE_EVENT_ID:
|
||||
handleMessageResponse(antMessage);
|
||||
break;
|
||||
case AntMesg.MESG_CHANNEL_ID_ID:
|
||||
handleChannelId(antMessage);
|
||||
break;
|
||||
default:
|
||||
Log.e(TAG, "Unexpected message id: " + antMessage[3]);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleBroadcastData(byte[] antMessage) {
|
||||
if (deviceNumberHRM == WILDCARD) {
|
||||
getAntReceiver().ANTRequestMessage(HRM_CHANNEL,
|
||||
AntMesg.MESG_CHANNEL_ID_ID);
|
||||
Log.d(TAG, "Requesting channel id id.");
|
||||
}
|
||||
|
||||
setSensorState(Sensor.SensorState.CONNECTED);
|
||||
int bpm = (int) antMessage[10] & 0xFF;
|
||||
Sensor.SensorData.Builder b = Sensor.SensorData.newBuilder()
|
||||
.setValue(bpm)
|
||||
.setState(Sensor.SensorState.SENDING);
|
||||
sensorData =
|
||||
Sensor.SensorDataSet.newBuilder()
|
||||
.setCreationTime(System.currentTimeMillis())
|
||||
.setHeartRate(b)
|
||||
.build();
|
||||
}
|
||||
|
||||
void handleChannelId(byte[] antMessage) {
|
||||
// Store the device id.
|
||||
deviceNumberHRM =
|
||||
(short) (((int) antMessage[3] & 0xFF |
|
||||
((int) (antMessage[4] & 0xFF) << 8)) & 0xFFFF);
|
||||
Log.i(TAG, "Found device id: " + deviceNumberHRM);
|
||||
|
||||
SharedPreferences prefs = context.getSharedPreferences(
|
||||
MyTracksSettings.SETTINGS_NAME, Context.MODE_PRIVATE);
|
||||
SharedPreferences.Editor editor = prefs.edit();
|
||||
editor.putInt(context.getString(R.string.ant_heart_rate_sensor_id_key), deviceNumberHRM);
|
||||
editor.commit();
|
||||
}
|
||||
|
||||
private void handleMessageResponse(byte[] antMessage) {
|
||||
if (antMessage[3] == AntMesg.MESG_EVENT_ID &&
|
||||
antMessage[4] == AntDefine.EVENT_RX_SEARCH_TIMEOUT) {
|
||||
// Search timeout
|
||||
Log.w(TAG, "Search timed out. Unassigning channel.");
|
||||
getAntReceiver().ANTUnassignChannel((byte) 0);
|
||||
setSensorState(Sensor.SensorState.DISCONNECTED);
|
||||
} else if (antMessage[3] == AntMesg.MESG_UNASSIGN_CHANNEL_ID) {
|
||||
setSensorState(Sensor.SensorState.DISCONNECTED);
|
||||
Log.i(TAG, "Disconnected from the sensor: " + getSensorState());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a channel to the SRM head unit.
|
||||
*/
|
||||
@Override
|
||||
public void setupChannel() {
|
||||
super.setupChannel();
|
||||
antChannelSetup(NETWORK_NUMBER,
|
||||
HRM_CHANNEL,
|
||||
deviceNumberHRM,
|
||||
HEART_RATE_DEVICE_TYPE,
|
||||
(byte) 0x01,
|
||||
CHANNEL_PERIOD,
|
||||
RF_FREQUENCY,
|
||||
(byte) 0);
|
||||
setSensorState(Sensor.SensorState.CONNECTING);
|
||||
}
|
||||
|
||||
public short getDeviceNumberHRM() {
|
||||
return deviceNumberHRM;
|
||||
}
|
||||
|
||||
void setDeviceNumberHRM(short deviceNumberHRM) {
|
||||
this.deviceNumberHRM = deviceNumberHRM;
|
||||
}
|
||||
}
|
||||
+226
@@ -0,0 +1,226 @@
|
||||
/*
|
||||
* 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.services.sensors;
|
||||
|
||||
import com.google.android.apps.mytracks.MyTracksConstants;
|
||||
import com.google.android.apps.mytracks.MyTracksSettings;
|
||||
import com.google.android.apps.mytracks.content.Sensor;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import com.dsi.ant.AntDefine;
|
||||
import com.dsi.ant.AntMesg;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* A sensor manager that can connect to an SRM Power Control 7 head unit.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class AntSRMSensorManager extends AntSensorManager {
|
||||
|
||||
/*
|
||||
* These constants are defined by the SRM Headunit spec.
|
||||
*/
|
||||
public static final byte NETWORK_NUMBER = (byte) 0;
|
||||
public static final byte DEVICE_TYPE = (byte) 12;
|
||||
public static final byte MANUFACTURER_ID = (byte) 1;
|
||||
public static final short CHANNEL_PERIOD = (short) 8192;
|
||||
public static final byte RF_FREQUENCY = (byte) 50;
|
||||
|
||||
public static final int MESSAGE_TYPE_INDEX = 3;
|
||||
public static final int INITIALS_MESSAGE_TYPE = 5;
|
||||
public static final int SENSOR_DATA_MESSAGE_TYPE = 6;
|
||||
|
||||
public static final int MESSAGE_ID_INDEX = 4;
|
||||
|
||||
/**
|
||||
* This can be any value between 0..8
|
||||
*/
|
||||
private byte channel = (byte) 5;
|
||||
|
||||
/**
|
||||
* This is the id of this device.
|
||||
* The head unit must be programmed to expect this number.
|
||||
*/
|
||||
private byte deviceId;
|
||||
|
||||
/**
|
||||
* The id of the last message received from the SRM head unit.
|
||||
*/
|
||||
private int lastMessageId;
|
||||
|
||||
public AntSRMSensorManager(Context context) {
|
||||
super(context);
|
||||
|
||||
// First read the the device id that we will be announcing.
|
||||
SharedPreferences prefs = context.getSharedPreferences(
|
||||
MyTracksSettings.SETTINGS_NAME, 0);
|
||||
if (prefs != null) {
|
||||
deviceId = (byte) prefs.getInt(
|
||||
context.getString(R.string.ant_srm_bridge_sensor_id_key),
|
||||
WILDCARD);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(byte[] antMessage) {
|
||||
// Parse channel number
|
||||
byte recievedChannel = (byte) (antMessage[AntMesg.MESG_DATA_OFFSET]
|
||||
& AntDefine.CHANNEL_NUMBER_MASK);
|
||||
if (recievedChannel != channel) {
|
||||
Log.d(MyTracksConstants.TAG, "Unexpected channel: " + recievedChannel);
|
||||
return;
|
||||
}
|
||||
switch (antMessage[AntMesg.MESG_ID_OFFSET]) {
|
||||
case AntMesg.MESG_BROADCAST_DATA_ID:
|
||||
handleBroadcastData(antMessage);
|
||||
break;
|
||||
case AntMesg.MESG_RESPONSE_EVENT_ID:
|
||||
handleMessageResponse(antMessage);
|
||||
break;
|
||||
case AntMesg.MESG_CHANNEL_ID_ID:
|
||||
handleChannelId(antMessage);
|
||||
break;
|
||||
default:
|
||||
Log.e(MyTracksConstants.TAG,
|
||||
"Unexpected message id: " + antMessage[3]);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleBroadcastData(byte[] antMessage) {
|
||||
if (deviceId == WILDCARD) {
|
||||
getAntReceiver().ANTRequestMessage(channel, AntMesg.MESG_CHANNEL_ID_ID);
|
||||
Log.d(MyTracksConstants.TAG, "Requesting channel id id.");
|
||||
}
|
||||
setSensorState(Sensor.SensorState.CONNECTED);
|
||||
switch(antMessage[MESSAGE_TYPE_INDEX]) {
|
||||
case INITIALS_MESSAGE_TYPE:
|
||||
// TODO handle initials message.
|
||||
break;
|
||||
case SENSOR_DATA_MESSAGE_TYPE:
|
||||
parseSensorData(antMessage);
|
||||
break;
|
||||
default:
|
||||
Log.e(MyTracksConstants.TAG,
|
||||
"Unexpected message type: " + antMessage[MESSAGE_TYPE_INDEX]);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleChannelId(byte[] antMessage) {
|
||||
// Store the device id.
|
||||
deviceId = antMessage[3];
|
||||
Log.i(MyTracksConstants.TAG, "Found device id: " + deviceId);
|
||||
|
||||
SharedPreferences prefs = context.getSharedPreferences(
|
||||
MyTracksSettings.SETTINGS_NAME, Context.MODE_PRIVATE);
|
||||
SharedPreferences.Editor editor = prefs.edit();
|
||||
editor.putInt(context.getString(R.string.ant_srm_bridge_sensor_id_key), deviceId);
|
||||
editor.commit();
|
||||
}
|
||||
|
||||
private void handleMessageResponse(byte[] antMessage) {
|
||||
if (antMessage[3] == AntMesg.MESG_EVENT_ID
|
||||
&& antMessage[4] == AntDefine.EVENT_RX_SEARCH_TIMEOUT) {
|
||||
// Search timeout
|
||||
Log.w(MyTracksConstants.TAG, "Search timed out. Unassigning channel.");
|
||||
getAntReceiver().ANTUnassignChannel(channel);
|
||||
setSensorState(Sensor.SensorState.DISCONNECTED);
|
||||
} else if (antMessage[3] == AntMesg.MESG_UNASSIGN_CHANNEL_ID) {
|
||||
setSensorState(Sensor.SensorState.DISCONNECTED);
|
||||
Log.i(MyTracksConstants.TAG,
|
||||
"Disconnected from the sensor: " + getSensorState());
|
||||
}
|
||||
}
|
||||
|
||||
private void parseSensorData(byte[] antMessage) {
|
||||
if (antMessage.length != 11) {
|
||||
Log.e(MyTracksConstants.TAG,
|
||||
"Unexpected ant message length: " + antMessage.length);
|
||||
return;
|
||||
}
|
||||
|
||||
int newMessageId = antMessage[MESSAGE_ID_INDEX] & 0xFF;
|
||||
if (lastMessageId == newMessageId) {
|
||||
// Repeated message.
|
||||
Log.i(MyTracksConstants.TAG,
|
||||
String.format("SRM ignoring repeat: 0x%X", newMessageId));
|
||||
return;
|
||||
}
|
||||
if (newMessageId < lastMessageId) {
|
||||
if (!(newMessageId < 20 && lastMessageId > 200)) {
|
||||
Log.i(MyTracksConstants.TAG,
|
||||
String.format("SRM ignoring repeat: 0x%X", newMessageId));
|
||||
return;
|
||||
} // else assume the byte overflowed to 0.
|
||||
}
|
||||
lastMessageId = newMessageId;
|
||||
|
||||
Sensor.SensorDataSet.Builder builder = Sensor.SensorDataSet.newBuilder()
|
||||
.setCreationTime(System.currentTimeMillis());
|
||||
|
||||
int power = SensorUtils.unsignedShortToInt(antMessage, 5);
|
||||
if (power >= 0) {
|
||||
Sensor.SensorData.Builder b = Sensor.SensorData.newBuilder()
|
||||
.setValue(power)
|
||||
.setState(Sensor.SensorState.SENDING);
|
||||
builder.setPower(b);
|
||||
}
|
||||
|
||||
int speed = SensorUtils.unsignedShortToInt(antMessage, 7);
|
||||
if (speed >= 0) {
|
||||
// The speed from srm is in 1/10 km/hr.
|
||||
// Sensor data expects the speed as m/s.
|
||||
// TODO finish this.
|
||||
//sensorData.setWheelSpeed(UnitConversions.TENTH_KMH_TO_MPS * speed);
|
||||
}
|
||||
|
||||
int cadence = antMessage[9] & 0xFF;
|
||||
if (cadence >= 0) {
|
||||
Sensor.SensorData.Builder b = Sensor.SensorData.newBuilder()
|
||||
.setValue(cadence)
|
||||
.setState(Sensor.SensorState.SENDING);
|
||||
builder.setCadence(b);
|
||||
}
|
||||
|
||||
int bpm = antMessage[10] & 0xFF;
|
||||
if (bpm > 0) {
|
||||
Sensor.SensorData.Builder b = Sensor.SensorData.newBuilder()
|
||||
.setValue(bpm)
|
||||
.setState(Sensor.SensorState.SENDING);
|
||||
builder.setHeartRate(b);
|
||||
}
|
||||
sensorData = builder.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a channel to the SRM head unit.
|
||||
*/
|
||||
@Override
|
||||
public void setupChannel() {
|
||||
antChannelSetup(NETWORK_NUMBER,
|
||||
channel,
|
||||
deviceId,
|
||||
DEVICE_TYPE,
|
||||
MANUFACTURER_ID,
|
||||
CHANNEL_PERIOD,
|
||||
RF_FREQUENCY,
|
||||
(byte) 0);
|
||||
lastMessageId = 0;
|
||||
setSensorState(Sensor.SensorState.CONNECTING);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
/*
|
||||
* 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.services.sensors;
|
||||
|
||||
import com.google.android.apps.mytracks.MyTracksConstants;
|
||||
import com.google.android.apps.mytracks.content.Sensor.SensorDataSet;
|
||||
|
||||
import com.dsi.ant.AntDefine;
|
||||
import com.dsi.ant.AntInterface;
|
||||
import com.dsi.ant.AntInterfaceIntent;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* An abstract class for ANT+ based sensors.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public abstract class AntSensorManager extends SensorManager {
|
||||
|
||||
// ANT Service
|
||||
public static final int EVENT_ANT_ENABLED = 1;
|
||||
public static final int EVENT_ANT_DISABLED = 2;
|
||||
|
||||
// Pairing
|
||||
protected static final short WILDCARD = 0;
|
||||
|
||||
private AntInterface antReceiver;
|
||||
|
||||
private boolean status;
|
||||
|
||||
// Flag to know if the ANT App was interrupted
|
||||
// TODO: This code path is not used but probably should be.
|
||||
private boolean antInterrupted;
|
||||
|
||||
/**
|
||||
* The data from the sensors.
|
||||
*/
|
||||
protected SensorDataSet sensorData;
|
||||
|
||||
protected Context context;
|
||||
|
||||
private static final boolean DEBUGGING = false;
|
||||
|
||||
/**
|
||||
* Receives all of the ANT intents and dispatches to the proper handler.
|
||||
*/
|
||||
private final BroadcastReceiver receiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
String antAction = intent.getAction();
|
||||
|
||||
Log.i(MyTracksConstants.TAG, "enter onReceive" + antAction);
|
||||
if (antAction.equals(AntInterfaceIntent.ANT_ENABLED_ACTION)) {
|
||||
Log.i(MyTracksConstants.TAG, "enter onReceive ANT_ENABLED_ACTION "
|
||||
+ antAction);
|
||||
handler.sendMessage(handler.obtainMessage(EVENT_ANT_ENABLED, 0));
|
||||
}
|
||||
if (antAction.equals(AntInterfaceIntent.ANT_DISABLED_ACTION)) {
|
||||
Log.i(MyTracksConstants.TAG, "enter onReceive ANT_DISABLED_ACTION "
|
||||
+ antAction);
|
||||
handler.sendMessage(handler.obtainMessage(EVENT_ANT_DISABLED, 0));
|
||||
}
|
||||
|
||||
if (antAction.equals(AntInterfaceIntent.ANT_RX_MESSAGE_ACTION)) {
|
||||
byte[] antMessage =
|
||||
intent.getByteArrayExtra(AntInterfaceIntent.ANT_MESSAGE);
|
||||
if (DEBUGGING) {
|
||||
StringBuilder sb =
|
||||
new StringBuilder("ANT_RX_MESSAGE_ACTION Rx:");
|
||||
|
||||
for (int i = 0; i < antMessage.length; i++)
|
||||
sb.append(String.format("[%X]]", (antMessage[i] & 0xFF)));
|
||||
|
||||
Log.d(MyTracksConstants.TAG, sb.toString());
|
||||
}
|
||||
|
||||
handleMessage(antMessage);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handler for all the ANT related events. The events will be handled only
|
||||
* when we are in the ANT application main menu
|
||||
**/
|
||||
private Handler handler = new Handler() {
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
switch (msg.what) {
|
||||
case EVENT_ANT_ENABLED:
|
||||
Log.i(MyTracksConstants.TAG,
|
||||
"enter handleMessage ----EVENT_ANT_ENABLED");
|
||||
break;
|
||||
case EVENT_ANT_DISABLED:
|
||||
Log.i(MyTracksConstants.TAG,
|
||||
"enter handleMessage ----EVENT_ANT_DISABLED");
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public AntSensorManager(Context context) {
|
||||
this.context = context;
|
||||
|
||||
// Need to enable the ANT if it is not enabled earlier
|
||||
antReceiver = AntInterface.getInstance(context, null);
|
||||
if (antReceiver == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
status = antReceiver.isEnabled();
|
||||
if (!status) {
|
||||
// Make sure not to call AntInterface.enable() again, if it has been
|
||||
// already called before
|
||||
if (antInterrupted == false) {
|
||||
status = antReceiver.enable();
|
||||
if (!status) {
|
||||
Log.e(MyTracksConstants.TAG, "Can not enable ANT interface");
|
||||
} else {
|
||||
Log.i(MyTracksConstants.TAG, "Powering on Radio");
|
||||
}
|
||||
} else {
|
||||
Log.i(MyTracksConstants.TAG, "Radio already enabled");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setupChannel() {
|
||||
Log.i(MyTracksConstants.TAG, "Registering for ant intents.");
|
||||
// Register for ANT intent broadcasts.
|
||||
IntentFilter intentFilter = new IntentFilter();
|
||||
intentFilter.addAction(AntInterfaceIntent.ANT_ENABLED_ACTION);
|
||||
intentFilter.addAction(AntInterfaceIntent.ANT_DISABLED_ACTION);
|
||||
intentFilter.addAction(AntInterfaceIntent.ANT_RX_MESSAGE_ACTION);
|
||||
context.registerReceiver(receiver, intentFilter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SensorDataSet getSensorDataSet() {
|
||||
return sensorData;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public abstract void handleMessage(byte[] antMessage);
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
Log.i(MyTracksConstants.TAG, "Unregistering for ant intents.");
|
||||
context.unregisterReceiver(receiver);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up an ANT+ channel.
|
||||
*
|
||||
* @return True on success.
|
||||
*/
|
||||
protected boolean antChannelSetup(byte networkNumber, byte channelNumber,
|
||||
short deviceNumber, byte deviceType, byte txType, short channelPeriod,
|
||||
byte radioFreq, byte proxSearch) {
|
||||
|
||||
// Assign as slave channel on selected network (0 = public, 1 = ANT+, 2 =
|
||||
// ANTFS)
|
||||
if (!antReceiver.ANTAssignChannel(channelNumber,
|
||||
AntDefine.PARAMETER_RX_NOT_TX, networkNumber)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!antReceiver.ANTSetChannelId(channelNumber, deviceNumber, deviceType,
|
||||
txType)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!antReceiver.ANTSetChannelPeriod(channelNumber, channelPeriod)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!antReceiver.ANTSetChannelRFFreq(channelNumber, radioFreq)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Disable high priority search
|
||||
if (!antReceiver.ANTSetChannelSearchTimeout(channelNumber, (byte) 0)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Set search timeout to 30 seconds (low priority search))
|
||||
if (!antReceiver.ANTSetLowPriorityChannelSearchTimeout(channelNumber, (byte) 12)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (deviceNumber == WILDCARD) {
|
||||
// Configure proximity search, if using wild card search
|
||||
if (!antReceiver.ANTSetProximitySearch(channelNumber, proxSearch)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return !antReceiver.ANTOpenChannel(channelNumber);
|
||||
}
|
||||
|
||||
public AntInterface getAntReceiver() {
|
||||
return antReceiver;
|
||||
}
|
||||
}
|
||||
+6
-3
@@ -13,7 +13,6 @@
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
|
||||
package com.google.android.apps.mytracks.services.sensors;
|
||||
|
||||
import com.google.android.apps.mytracks.MyTracksConstants;
|
||||
@@ -46,15 +45,19 @@ public class SensorManagerFactory {
|
||||
return null;
|
||||
}
|
||||
|
||||
String sensor = prefs.getString(context.getString(R.string.sensor_type_key),
|
||||
null);
|
||||
String sensor = prefs.getString(context.getString(R.string.sensor_type_key), null);
|
||||
Log.i(MyTracksConstants.TAG, "Creating sensor of type: " + sensor);
|
||||
|
||||
if (sensor == null) {
|
||||
return null;
|
||||
} else if (sensor.equals(context.getString(R.string.ant_sensor_type))) {
|
||||
return new AntDirectSensorManager(context);
|
||||
} else if (sensor.equals(context.getString(R.string.srm_ant_bridge_sensor_type))) {
|
||||
return new AntSRMSensorManager(context);
|
||||
} else if (sensor.equals(context.getString(R.string.zephyr_sensor_type))) {
|
||||
return new ZephyrSensorManager(context);
|
||||
} else {
|
||||
Log.w(MyTracksConstants.TAG, "Unable to find sensor type: " + sensor);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
+3
-2
@@ -35,8 +35,9 @@ public class ZephyrMessageParser implements MessageParser {
|
||||
}
|
||||
Log.w(MyTracksConstants.TAG, "Got zephyr data: " + sb);
|
||||
// The provided units are 1/16 strides per minute.
|
||||
int cadence = SensorUtils.unsignedShortToInt(buffer, 56);
|
||||
// Heart Rate
|
||||
// TODO: Fix the cadence calculation.
|
||||
// int cadence = SensorUtils.unsignedShortToInt(buffer, 56);
|
||||
// Heart Rate
|
||||
Sensor.SensorData.Builder b = Sensor.SensorData.newBuilder()
|
||||
.setValue(buffer[12] & 0xFF)
|
||||
.setState(Sensor.SensorState.SENDING);
|
||||
|
||||
@@ -62,24 +62,18 @@ public class ApiFeatures {
|
||||
* Allow subclasses for mocking, but no direct instantiation.
|
||||
*/
|
||||
protected ApiFeatures() {
|
||||
if (getApiLevel() >= 5) {
|
||||
try {
|
||||
Class<?> clazz = Class.forName(
|
||||
"com.google.android.apps.mytracks.util.EclairPlatformAdapter");
|
||||
apiPlatformAdapter = (ApiPlatformAdapter) clazz.newInstance();
|
||||
} catch (Exception e) {
|
||||
Log.i(MyTracksConstants.TAG, "ApiFeatures: Unable to instantiate Eclair"
|
||||
+ " platform adapter", e);
|
||||
}
|
||||
}
|
||||
if (apiPlatformAdapter == null) {
|
||||
if (getApiLevel() >= 9) {
|
||||
apiPlatformAdapter = new GingerbreadPlatformAdapter();
|
||||
} else if (getApiLevel() >= 5) {
|
||||
apiPlatformAdapter = new EclairPlatformAdapter();
|
||||
} else {
|
||||
Log.i(MyTracksConstants.TAG,
|
||||
"ApiFeatures: Using default platform adapter");
|
||||
// Cupcake adapter is always supported, so it's safe to do static linkage.
|
||||
apiPlatformAdapter = new CupcakePlatformAdapter();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public ApiPlatformAdapter getApiPlatformAdapter() {
|
||||
return apiPlatformAdapter;
|
||||
}
|
||||
@@ -107,7 +101,11 @@ public class ApiFeatures {
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
public boolean hasStrictMode() {
|
||||
return getApiLevel() >= 9;
|
||||
}
|
||||
|
||||
// Visible for testing.
|
||||
protected int getApiLevel() {
|
||||
return ANDROID_API_LEVEL;
|
||||
|
||||
@@ -18,6 +18,7 @@ package com.google.android.apps.mytracks.util;
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.Service;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.HandlerThread;
|
||||
|
||||
/**
|
||||
@@ -57,4 +58,15 @@ public interface ApiPlatformAdapter {
|
||||
* @return true whether the handler has been stopped or not.
|
||||
*/
|
||||
boolean stopHandlerThread(HandlerThread handlerThread);
|
||||
|
||||
/**
|
||||
* Applies all changes done to the given preferences editor.
|
||||
* Changes may or may not be applied immediately.
|
||||
*/
|
||||
void applyPreferenceChanges(SharedPreferences.Editor editor);
|
||||
|
||||
/**
|
||||
* Enables strict mode where supported, only if this is a development build.
|
||||
*/
|
||||
void enableStrictMode();
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ package com.google.android.apps.mytracks.util;
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.Service;
|
||||
import android.content.SharedPreferences.Editor;
|
||||
import android.os.HandlerThread;
|
||||
|
||||
/**
|
||||
@@ -50,4 +51,14 @@ public class CupcakePlatformAdapter implements ApiPlatformAdapter {
|
||||
// Do nothing, as Cupcake doesn't provide quit().
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void applyPreferenceChanges(Editor editor) {
|
||||
editor.commit();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enableStrictMode() {
|
||||
// Not supported
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ package com.google.android.apps.mytracks.util;
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.Service;
|
||||
import android.content.SharedPreferences.Editor;
|
||||
import android.os.HandlerThread;
|
||||
|
||||
/**
|
||||
@@ -45,4 +46,14 @@ public class EclairPlatformAdapter implements ApiPlatformAdapter {
|
||||
public boolean stopHandlerThread(HandlerThread handlerThread) {
|
||||
return handlerThread.quit();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void applyPreferenceChanges(Editor editor) {
|
||||
editor.commit();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enableStrictMode() {
|
||||
// Not supported
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.google.android.apps.mytracks.util;
|
||||
|
||||
import com.google.android.apps.mytracks.MyTracksConstants;
|
||||
|
||||
import android.content.SharedPreferences.Editor;
|
||||
import android.os.StrictMode;
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* The Gingerbread (API level 9) specific implememntation of the
|
||||
* {@link ApiPlatformAdapter}.
|
||||
*
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
public class GingerbreadPlatformAdapter extends EclairPlatformAdapter {
|
||||
@Override
|
||||
public void applyPreferenceChanges(Editor editor) {
|
||||
// Apply asynchronously
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enableStrictMode() {
|
||||
Log.d(MyTracksConstants.TAG, "Enabling strict mode");
|
||||
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
|
||||
.detectDiskWrites()
|
||||
.detectNetwork()
|
||||
.penaltyLog()
|
||||
.build());
|
||||
StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
|
||||
.detectAll()
|
||||
.penaltyLog()
|
||||
.penaltyDeath()
|
||||
.build());
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,7 @@ import android.content.Context;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.PackageManager.NameNotFoundException;
|
||||
import android.content.pm.Signature;
|
||||
import android.location.Location;
|
||||
import android.util.Log;
|
||||
|
||||
@@ -38,6 +39,7 @@ import java.util.Stack;
|
||||
* @author Leif Hendrik Wilden
|
||||
*/
|
||||
public class MyTracksUtils {
|
||||
private static final int RELEASE_SIGNATURE_HASHCODE = -1855564782;
|
||||
|
||||
/**
|
||||
* Computes the distance on the two sphere between the point c0 and the line
|
||||
@@ -250,6 +252,24 @@ public class MyTracksUtils {
|
||||
(int) (location.getLongitude() * 1E6));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not this is a release build.
|
||||
*/
|
||||
public static boolean isRelease(Context context) {
|
||||
try {
|
||||
Signature [] sigs = context.getPackageManager().getPackageInfo(
|
||||
context.getPackageName(), PackageManager.GET_SIGNATURES).signatures;
|
||||
for (Signature sig : sigs) {
|
||||
if (sig.hashCode() == RELEASE_SIGNATURE_HASHCODE) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch (NameNotFoundException e) {
|
||||
Log.e(MyTracksConstants.TAG, "Unable to get signatures", e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the My Tracks version from the manifest.
|
||||
*
|
||||
@@ -258,17 +278,17 @@ public class MyTracksUtils {
|
||||
public static String getMyTracksVersion(Context context) {
|
||||
try {
|
||||
PackageInfo pi = context.getPackageManager().getPackageInfo(
|
||||
"com.google.android.maps.mytracks",
|
||||
"com.google.android.maps.mytracks",
|
||||
PackageManager.GET_META_DATA);
|
||||
return pi.versionName;
|
||||
} catch (NameNotFoundException e) {
|
||||
} catch (NameNotFoundException e) {
|
||||
Log.w(MyTracksConstants.TAG, "Failed to get version info.", e);
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a utility class w/ only static memebers.
|
||||
* This is a utility class w/ only static members.
|
||||
*/
|
||||
protected MyTracksUtils() {
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user