forked from upstream-mirrors/OpenTracks
merge heads
This commit is contained in:
@@ -112,12 +112,19 @@ limitations under the License.
|
|||||||
<meta-data android:name="com.google.android.backup.api_key"
|
<meta-data android:name="com.google.android.backup.api_key"
|
||||||
android:value="AEdPqrEAAAAIi-_QiwoRSc9_bAC9cmuNXTQyU8ajJmGtKdhskQ" />
|
android:value="AEdPqrEAAAAIi-_QiwoRSc9_bAC9cmuNXTQyU8ajJmGtKdhskQ" />
|
||||||
|
|
||||||
|
<!-- Main track data provider -->
|
||||||
<provider android:name="com.google.android.apps.mytracks.content.MyTracksProvider"
|
<provider android:name="com.google.android.apps.mytracks.content.MyTracksProvider"
|
||||||
android:authorities="com.google.android.maps.mytracks"
|
android:authorities="com.google.android.maps.mytracks"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:readPermission="com.google.android.apps.mytracks.READ_TRACK_DATA"
|
android:readPermission="com.google.android.apps.mytracks.READ_TRACK_DATA"
|
||||||
android:writePermission="com.google.android.apps.mytracks.WRITE_TRACK_DATA" />
|
android:writePermission="com.google.android.apps.mytracks.WRITE_TRACK_DATA" />
|
||||||
|
|
||||||
|
<!-- Provider for search suggestions -->
|
||||||
|
<provider android:name="com.google.android.apps.mytracks.content.SearchEngineProvider"
|
||||||
|
android:authorities="com.google.android.maps.mytracks.search"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
|
<!-- Main activity -->
|
||||||
<activity android:name="com.google.android.apps.mytracks.MyTracks"
|
<activity android:name="com.google.android.apps.mytracks.MyTracks"
|
||||||
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|orientation|fontScale">
|
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|orientation|fontScale">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
@@ -129,7 +136,12 @@ limitations under the License.
|
|||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
<data android:mimeType="vnd.android.cursor.item/vnd.google.track" />
|
<data android:mimeType="vnd.android.cursor.item/vnd.google.track" />
|
||||||
|
<data android:mimeType="vnd.android.cursor.item/vnd.google.waypoint" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
|
<!-- Enable searches via SearchActivity. -->
|
||||||
|
<meta-data android:name="android.app.default_searchable"
|
||||||
|
android:value="com.google.android.apps.mytracks.SearchActivity" />
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity android:name="com.google.android.apps.mytracks.MapActivity"
|
<activity android:name="com.google.android.apps.mytracks.MapActivity"
|
||||||
@@ -137,18 +149,26 @@ limitations under the License.
|
|||||||
<intent-filter>
|
<intent-filter>
|
||||||
<category android:name="android.intent.category.TAB" />
|
<category android:name="android.intent.category.TAB" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
|
<!-- Enable searches via SearchActivity. -->
|
||||||
|
<meta-data android:name="android.app.default_searchable"
|
||||||
|
android:value="com.google.android.apps.mytracks.SearchActivity" />
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity android:name="com.google.android.apps.mytracks.MapsList"
|
|
||||||
android:label="@string/maps_list_title"
|
|
||||||
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|orientation|fontScale"
|
|
||||||
android:exported="false" />
|
|
||||||
|
|
||||||
<!-- TODO: VIEW and PICK for track cursor dir -->
|
<!-- TODO: Merge TrackList, WaypointsLIst and SearchActivity, then implement VIEW and PICK for track and waypoint cursor dir -->
|
||||||
<activity android:name="com.google.android.apps.mytracks.TrackList" />
|
<activity android:name="com.google.android.apps.mytracks.TrackList">
|
||||||
|
<!-- Enable searches via SearchActivity. -->
|
||||||
|
<meta-data android:name="android.app.default_searchable"
|
||||||
|
android:value="com.google.android.apps.mytracks.SearchActivity" />
|
||||||
|
</activity>
|
||||||
|
|
||||||
<!-- TODO: VIEW and PICK for waypoint cursor dir -->
|
<!-- TODO: VIEW and PICK for waypoint cursor dir -->
|
||||||
<activity android:name="com.google.android.apps.mytracks.WaypointsList" />
|
<activity android:name="com.google.android.apps.mytracks.WaypointsList">
|
||||||
|
<!-- Enable searches via SearchActivity. -->
|
||||||
|
<meta-data android:name="android.app.default_searchable"
|
||||||
|
android:value="com.google.android.apps.mytracks.SearchActivity" />
|
||||||
|
</activity>
|
||||||
|
|
||||||
<activity android:name="com.google.android.apps.mytracks.TrackDetail"
|
<activity android:name="com.google.android.apps.mytracks.TrackDetail"
|
||||||
android:label="@string/track_detail_title">
|
android:label="@string/track_detail_title">
|
||||||
@@ -173,7 +193,6 @@ limitations under the License.
|
|||||||
<activity android:name="com.google.android.apps.mytracks.WaypointDetails"
|
<activity android:name="com.google.android.apps.mytracks.WaypointDetails"
|
||||||
android:label="@string/marker_detail_title">
|
android:label="@string/marker_detail_title">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
|
||||||
<action android:name="android.intent.action.EDIT" />
|
<action android:name="android.intent.action.EDIT" />
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
@@ -196,6 +215,10 @@ limitations under the License.
|
|||||||
<intent-filter>
|
<intent-filter>
|
||||||
<category android:name="android.intent.category.TAB" />
|
<category android:name="android.intent.category.TAB" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
|
<!-- Enable searches via SearchActivity. -->
|
||||||
|
<meta-data android:name="android.app.default_searchable"
|
||||||
|
android:value="com.google.android.apps.mytracks.SearchActivity" />
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity android:name="com.google.android.apps.mytracks.SensorStateActivity"
|
<activity android:name="com.google.android.apps.mytracks.SensorStateActivity"
|
||||||
@@ -206,14 +229,10 @@ limitations under the License.
|
|||||||
<intent-filter>
|
<intent-filter>
|
||||||
<category android:name="android.intent.category.TAB" />
|
<category android:name="android.intent.category.TAB" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
|
||||||
|
|
||||||
<activity android:name="com.google.android.apps.mytracks.io.sendtogoogle.SendActivity"
|
<!-- Enable searches via SearchActivity. -->
|
||||||
android:theme="@android:style/Theme.Translucent.NoTitleBar">
|
<meta-data android:name="android.app.default_searchable"
|
||||||
<intent-filter>
|
android:value="com.google.android.apps.mytracks.SearchActivity" />
|
||||||
<action android:name="android.intent.action.SEND" />
|
|
||||||
<data android:mimeType="vnd.android.cursor.item/vnd.google.track" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity android:name="com.google.android.apps.mytracks.io.file.SaveActivity"
|
<activity android:name="com.google.android.apps.mytracks.io.file.SaveActivity"
|
||||||
@@ -246,9 +265,24 @@ limitations under the License.
|
|||||||
android:label="@string/welcome_title"
|
android:label="@string/welcome_title"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
<activity android:name="com.google.android.apps.mytracks.io.sendtogoogle.UploadServiceChooserActivity"/>
|
<activity android:name="com.google.android.apps.mytracks.SearchActivity"
|
||||||
|
android:launchMode="singleTop">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.SEARCH" />
|
||||||
|
</intent-filter>
|
||||||
|
<meta-data android:name="android.app.searchable"
|
||||||
|
android:resource="@xml/search"/>
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
<activity android:name="com.google.android.apps.mytracks.io.docs.SendDocsActivity"/>
|
||||||
|
<activity android:name="com.google.android.apps.mytracks.io.fusiontables.SendFusionTablesActivity"/>
|
||||||
|
<activity android:name="com.google.android.apps.mytracks.io.maps.ChooseMapActivity"
|
||||||
|
android:label="@string/maps_list_title"/>
|
||||||
|
<activity android:name="com.google.android.apps.mytracks.io.maps.SendMapsActivity"/>
|
||||||
<activity android:name="com.google.android.apps.mytracks.io.sendtogoogle.AccountChooserActivity"/>
|
<activity android:name="com.google.android.apps.mytracks.io.sendtogoogle.AccountChooserActivity"/>
|
||||||
|
<activity android:name="com.google.android.apps.mytracks.io.sendtogoogle.UploadResultActivity"/>
|
||||||
|
<activity android:name="com.google.android.apps.mytracks.io.sendtogoogle.UploadServiceChooserActivity"/>
|
||||||
|
|
||||||
<service android:name="com.google.android.apps.mytracks.services.TrackRecordingService"
|
<service android:name="com.google.android.apps.mytracks.services.TrackRecordingService"
|
||||||
android:label="@string/my_tracks_recording_service"
|
android:label="@string/my_tracks_recording_service"
|
||||||
android:icon="@drawable/arrow_320"
|
android:icon="@drawable/arrow_320"
|
||||||
@@ -258,7 +292,7 @@ limitations under the License.
|
|||||||
|
|
||||||
<service android:name="com.google.android.apps.mytracks.services.ControlRecordingService"
|
<service android:name="com.google.android.apps.mytracks.services.ControlRecordingService"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
<receiver android:name="com.google.android.apps.mytracks.BootReceiver">
|
<receiver android:name="com.google.android.apps.mytracks.BootReceiver">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ limitations under the License.
|
|||||||
android:text="@string/about_support"
|
android:text="@string/about_support"
|
||||||
style="@style/AboutLabel"/>
|
style="@style/AboutLabel"/>
|
||||||
<TextView
|
<TextView
|
||||||
android:text="@string/my_tracks_web_url"
|
android:text="@string/my_tracks_support_url"
|
||||||
style="@style/AboutUrl"/>
|
style="@style/AboutUrl"/>
|
||||||
|
|
||||||
<!-- Privacy -->
|
<!-- Privacy -->
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
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.
|
||||||
|
-->
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_height="fill_parent"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:orientation="vertical">
|
||||||
|
<TextView
|
||||||
|
android:background="@drawable/list_bar"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:padding="5dp"
|
||||||
|
android:text="@string/maps_list_pick_map"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:textSize="16sp"/>
|
||||||
|
<ListView
|
||||||
|
android:id="@+id/choose_map_list_view"
|
||||||
|
android:layout_height="fill_parent"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:padding="5dp"/>
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/choose_map_empty_view"
|
||||||
|
android:gravity="center"
|
||||||
|
android:layout_height="fill_parent"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:padding="5dp"
|
||||||
|
android:textColor="@color/red"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textStyle="bold"/>
|
||||||
|
</LinearLayout>
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
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.
|
||||||
|
-->
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:paddingBottom="2dp"
|
||||||
|
android:paddingTop="2dp">
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/choose_map_list_item_title"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginLeft="6dp"
|
||||||
|
android:layout_marginTop="6dp"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:singleLine="true"/>
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/choose_map_list_item_description"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginLeft="6dp"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:singleLine="true"/>
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/choose_map_list_item_search_status"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginLeft="6dp"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:singleLine="true"/>
|
||||||
|
</LinearLayout>
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!--
|
|
||||||
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.
|
|
||||||
-->
|
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apl/res/com.google.android.maps"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:layout_width="fill_parent"
|
|
||||||
android:layout_height="fill_parent">
|
|
||||||
<LinearLayout
|
|
||||||
android:orientation="horizontal"
|
|
||||||
android:layout_width="fill_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:background="@drawable/list_bar"
|
|
||||||
android:layout_weight="0"
|
|
||||||
android:padding="3dip">
|
|
||||||
<TextView android:id="@+id/message"
|
|
||||||
android:layout_width="fill_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:textSize="16dip"
|
|
||||||
android:textColor="#fff"
|
|
||||||
android:text="@string/maps_list_pick_map"
|
|
||||||
android:layout_weight="0"/>
|
|
||||||
</LinearLayout>
|
|
||||||
<ListView android:id="@+id/maplist"
|
|
||||||
android:layout_width="fill_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_weight="1"/>
|
|
||||||
<TextView android:id="@+id/mapslist_empty"
|
|
||||||
android:layout_width="fill_parent"
|
|
||||||
android:layout_height="fill_parent"
|
|
||||||
android:layout_gravity="center"
|
|
||||||
android:gravity="center"
|
|
||||||
android:padding="3dip"
|
|
||||||
android:textSize="16dip"
|
|
||||||
android:textColor="#c00"
|
|
||||||
android:textStyle="bold"
|
|
||||||
android:text="@string/maps_list_no_maps"
|
|
||||||
android:visibility="gone"/>
|
|
||||||
<TextView android:id="@+id/failed"
|
|
||||||
android:layout_width="fill_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="center"
|
|
||||||
android:gravity="center"
|
|
||||||
android:padding="3dip"
|
|
||||||
android:textSize="16dip"
|
|
||||||
android:textColor="#c00"
|
|
||||||
android:textStyle="bold"
|
|
||||||
android:text="@string/maps_list_error"
|
|
||||||
android:visibility="gone"/>
|
|
||||||
<LinearLayout
|
|
||||||
android:orientation="horizontal"
|
|
||||||
android:layout_width="fill_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:background="@drawable/list_bar"
|
|
||||||
android:layout_weight="0"
|
|
||||||
android:padding="3dip">
|
|
||||||
<LinearLayout android:id="@+id/loading"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="horizontal"
|
|
||||||
android:padding="3dip"
|
|
||||||
android:layout_gravity="center">
|
|
||||||
<TextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_weight="1"/>
|
|
||||||
<ProgressBar
|
|
||||||
android:layout_height="20dip"
|
|
||||||
android:layout_width="20dip"
|
|
||||||
android:layout_gravity="center"/>
|
|
||||||
<TextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="center"
|
|
||||||
android:paddingLeft="3dip"
|
|
||||||
android:textSize="16dip"
|
|
||||||
android:textColor="#fff"
|
|
||||||
android:textStyle="bold"
|
|
||||||
android:text="@string/maps_list_loading"/>
|
|
||||||
<TextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_weight="1"/>
|
|
||||||
</LinearLayout>
|
|
||||||
<TextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_weight="1"/>
|
|
||||||
</LinearLayout>
|
|
||||||
</LinearLayout>
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!--
|
|
||||||
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.
|
|
||||||
-->
|
|
||||||
<LinearLayout
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:layout_width="fill_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:padding="2dip"
|
|
||||||
android:orientation="vertical">
|
|
||||||
<TextView android:id="@+id/maplistitem"
|
|
||||||
android:layout_width="fill_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:textSize="9pt"
|
|
||||||
android:textStyle="bold"
|
|
||||||
android:textColor="#fff"
|
|
||||||
android:singleLine="true"/>
|
|
||||||
<TextView android:id="@+id/maplistdesc"
|
|
||||||
android:layout_width="fill_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:textSize="8pt"
|
|
||||||
android:singleLine="true"/>
|
|
||||||
<TextView android:id="@+id/maplistpublic"
|
|
||||||
android:layout_width="fill_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:textSize="7pt"
|
|
||||||
android:singleLine="true"/>
|
|
||||||
</LinearLayout>
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!--
|
|
||||||
Copyright 2011 Google Inc.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
-->
|
|
||||||
<!--
|
|
||||||
This layout is used as the content view for the send-to-Google result dialog.
|
|
||||||
It is used to summarize the results of uploading track data to one or more
|
|
||||||
services.
|
|
||||||
-->
|
|
||||||
<LinearLayout
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:layout_width="fill_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:paddingTop="5dip"
|
|
||||||
android:paddingBottom="5dip"
|
|
||||||
android:paddingLeft="14dip"
|
|
||||||
android:paddingRight="14dip"
|
|
||||||
android:orientation="vertical">
|
|
||||||
<TextView
|
|
||||||
android:layout_width="fill_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/send_google_result_header"
|
|
||||||
style="?android:attr/textAppearanceLarge"/>
|
|
||||||
<TableLayout
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:gravity="center_horizontal">
|
|
||||||
<!-- We don't want a divider, so we set its color to transparent. -->
|
|
||||||
<!-- We do this explicitly, rather than with a color resource, because -->
|
|
||||||
<!-- an apparent bug causes the divider to be wildly misrendered on -->
|
|
||||||
<!-- a Mini X10 running 2.1. -->
|
|
||||||
<ListView
|
|
||||||
android:id="@+id/send_to_google_result_list"
|
|
||||||
android:clickable="false"
|
|
||||||
android:paddingTop="10dip"
|
|
||||||
android:paddingBottom="10dip"
|
|
||||||
android:divider="#00000000"/>
|
|
||||||
</TableLayout>
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/send_to_google_result_comment"
|
|
||||||
android:layout_width="fill_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/send_google_success_footer"
|
|
||||||
android:visibility="gone"
|
|
||||||
style="?android:attr/textAppearanceSmall"/>
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/send_to_google_result_error"
|
|
||||||
android:layout_width="fill_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/send_google_error"
|
|
||||||
android:visibility="gone"
|
|
||||||
style="?android:attr/textAppearanceLarge"/>
|
|
||||||
</LinearLayout>
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!--
|
|
||||||
Copyright 2011 Google Inc.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
-->
|
|
||||||
<!--
|
|
||||||
This layout represents the result of sending track data to a single service.
|
|
||||||
It will be instantiated by a custom ListAdapter - once for each service to
|
|
||||||
which track data was uploaded.
|
|
||||||
-->
|
|
||||||
<TableRow
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content">
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/send_to_google_result_icon"
|
|
||||||
android:src="@drawable/success"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="fill_parent"
|
|
||||||
android:layout_gravity="top"/>
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="vertical">
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/send_to_google_result_name"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="Google Docs"
|
|
||||||
style="?android:attr/textAppearanceMedium"/>
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/send_to_google_result_url"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="http://google.com"
|
|
||||||
style="?android:attr/textAppearanceSmall"/>
|
|
||||||
</LinearLayout>
|
|
||||||
</TableRow>
|
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
Copyright 2012 Google Inc.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
-->
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:orientation="vertical">
|
||||||
|
<ScrollView
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_margin="5dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_width="fill_parent">
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<TextView
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/send_google_result_header"
|
||||||
|
style="?android:attr/textAppearanceLarge"/>
|
||||||
|
|
||||||
|
<!-- Google Maps result -->
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/upload_result_maps_result"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/upload_result_maps_result_icon"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:src="@drawable/success"/>
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:orientation="vertical">
|
||||||
|
<TextView
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:text="@string/send_google_maps"
|
||||||
|
style="?android:attr/textAppearanceMedium"/>
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/upload_result_list_item_url"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:text="@string/send_google_maps_url"
|
||||||
|
style="?android:attr/textAppearanceSmall"/>
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- Google Fusion Tables result -->
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/upload_result_fusion_tables_result"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/upload_result_fusion_tables_result_icon"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:src="@drawable/success"/>
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:orientation="vertical">
|
||||||
|
<TextView
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:text="@string/send_google_fusion_tables"
|
||||||
|
style="?android:attr/textAppearanceMedium"/>
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/upload_result_list_item_url"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:text="@string/send_google_fusion_tables_url"
|
||||||
|
style="?android:attr/textAppearanceSmall"/>
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- Google Docs result -->
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/upload_result_docs_result"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/upload_result_docs_result_icon"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:src="@drawable/success"/>
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:orientation="vertical">
|
||||||
|
<TextView
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:text="@string/send_google_docs"
|
||||||
|
style="?android:attr/textAppearanceMedium"/>
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/upload_result_list_item_url"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:text="@string/send_google_docs_url"
|
||||||
|
style="?android:attr/textAppearanceSmall"/>
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/upload_result_success_footer"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:text="@string/send_google_success_footer"
|
||||||
|
style="?android:attr/textAppearanceSmall"/>
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/upload_result_error_footer"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:text="@string/send_google_error"
|
||||||
|
style="?android:attr/textAppearanceLarge"/>
|
||||||
|
</LinearLayout>
|
||||||
|
</ScrollView>
|
||||||
|
</LinearLayout>
|
||||||
@@ -1,4 +1,19 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
Copyright 2011 Google Inc.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
-->
|
||||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<item android:id="@+id/menu_record_track"
|
<item android:id="@+id/menu_record_track"
|
||||||
android:icon="@drawable/ic_menu_recordtrack"
|
android:icon="@drawable/ic_menu_recordtrack"
|
||||||
@@ -16,20 +31,24 @@
|
|||||||
android:icon="@drawable/ic_menu_waypoints"
|
android:icon="@drawable/ic_menu_waypoints"
|
||||||
android:title="@string/menu_markers"/>
|
android:title="@string/menu_markers"/>
|
||||||
|
|
||||||
|
<item android:id="@+id/menu_search"
|
||||||
|
android:icon="@android:drawable/ic_menu_search"
|
||||||
|
android:title="@string/menu_search"/>
|
||||||
|
|
||||||
<item android:id="@+id/menu_sensor_state"
|
<item android:id="@+id/menu_sensor_state"
|
||||||
android:icon="@drawable/ic_menu_sensorstatus"
|
android:icon="@drawable/ic_menu_sensorstatus"
|
||||||
android:orderInCategory="1000"
|
android:orderInCategory="1000"
|
||||||
android:title="@string/menu_sensor_state"/>
|
android:title="@string/menu_sensor_state"/>
|
||||||
|
|
||||||
<item android:id="@+id/menu_settings"
|
<item android:id="@+id/menu_settings"
|
||||||
android:icon="@android:drawable/ic_menu_preferences"
|
android:icon="@android:drawable/ic_menu_preferences"
|
||||||
android:orderInCategory="1001"
|
android:orderInCategory="1001"
|
||||||
android:title="@string/menu_settings"/>
|
android:title="@string/menu_settings"/>
|
||||||
|
|
||||||
<item android:id="@+id/menu_aggregated_statistics"
|
<item android:id="@+id/menu_aggregated_statistics"
|
||||||
android:orderInCategory="1002"
|
android:orderInCategory="1002"
|
||||||
android:title="@string/menu_aggregated_statistics"/>
|
android:title="@string/menu_aggregated_statistics"/>
|
||||||
|
|
||||||
<item android:id="@+id/menu_help"
|
<item android:id="@+id/menu_help"
|
||||||
android:icon="@android:drawable/ic_menu_info_details"
|
android:icon="@android:drawable/ic_menu_info_details"
|
||||||
android:orderInCategory="1003"
|
android:orderInCategory="1003"
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
Copyright 2012 Google Inc.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
-->
|
||||||
|
<menu xmlns:android="http://schemas.android.com/apk/res/android" >
|
||||||
|
<item android:id="@+id/menu_search"
|
||||||
|
android:icon="@android:drawable/ic_menu_search"
|
||||||
|
android:title="@string/menu_search"
|
||||||
|
android:onClick="onSearch"
|
||||||
|
android:showAsAction="always" />
|
||||||
|
</menu>
|
||||||
@@ -37,6 +37,9 @@ limitations under the License.
|
|||||||
<string name="my_tracks_analytics_id">UA-7222692-2</string>
|
<string name="my_tracks_analytics_id">UA-7222692-2</string>
|
||||||
<string name="my_tracks_privacy_url">m.google.com/privacy</string>
|
<string name="my_tracks_privacy_url">m.google.com/privacy</string>
|
||||||
<string name="my_tracks_recording_service_name">My Tracks Recording Service</string>
|
<string name="my_tracks_recording_service_name">My Tracks Recording Service</string>
|
||||||
|
<string name="my_tracks_support_url">
|
||||||
|
groups.google.com/a/googleproductforums.com/forum/#!categories/maps/mytracks
|
||||||
|
</string>
|
||||||
<string name="my_tracks_web_url">mytracks.appspot.com</string>
|
<string name="my_tracks_web_url">mytracks.appspot.com</string>
|
||||||
<string name="permission_notification_value">
|
<string name="permission_notification_value">
|
||||||
com.google.android.apps.mytracks.MYTRACKS_NOTIFICATIONS
|
com.google.android.apps.mytracks.MYTRACKS_NOTIFICATIONS
|
||||||
|
|||||||
@@ -693,6 +693,43 @@ limitations under the License.
|
|||||||
My Tracks: Imported %1$d of %2$s from %3$s.
|
My Tracks: Imported %1$d of %2$s from %3$s.
|
||||||
</string>
|
</string>
|
||||||
|
|
||||||
|
<!-- Maps List -->
|
||||||
|
<string name="maps_list_error"
|
||||||
|
translation_description="When sending a track to an existing Google Map, the text on the
|
||||||
|
'Maps' list page informing the user that his maps cannot be loaded at this time.">
|
||||||
|
Your maps could not be loaded at this time.
|
||||||
|
</string>
|
||||||
|
<string name="maps_list_loading"
|
||||||
|
translation_description="When sending a track to an existing Google Map, the progress dialog
|
||||||
|
message informing the user that his maps are being loaded.">
|
||||||
|
Loading your Google Maps.
|
||||||
|
</string>
|
||||||
|
<string name="maps_list_no_maps"
|
||||||
|
translation_description="When sending a track to an existing Google Map, the text on the
|
||||||
|
'Maps' list page informing the user that he doesn't have any maps.">
|
||||||
|
You don\'t have any maps.
|
||||||
|
</string>
|
||||||
|
<string name="maps_list_pick_map"
|
||||||
|
translation_description="When sending a track to an existing Google Map, the text on the
|
||||||
|
'Maps' list page asking the user to pick one of his maps.">
|
||||||
|
Pick a map:
|
||||||
|
</string>
|
||||||
|
<string name="maps_list_public_label"
|
||||||
|
translation_description="When sending a track to an existing Google Map, in the 'Maps'
|
||||||
|
list page, the label next to the maps that are public, as opposed to being unlisted.">
|
||||||
|
Public
|
||||||
|
</string>
|
||||||
|
<string name="maps_list_title"
|
||||||
|
translation_description="When sending a track to an existing Google Map, the title of the
|
||||||
|
page listing the user's maps in Google Maps.">
|
||||||
|
Your Google Maps
|
||||||
|
</string>
|
||||||
|
<string name="maps_list_unlisted_label"
|
||||||
|
translation_description="When sending a track to an existing Google Map, in the 'Maps'
|
||||||
|
list page, the label next to the maps that are unlisted, as opposed to being public.">
|
||||||
|
Unlisted
|
||||||
|
</string>
|
||||||
|
|
||||||
<!-- Marker -->
|
<!-- Marker -->
|
||||||
<string name="marker_detail_marker_description"
|
<string name="marker_detail_marker_description"
|
||||||
translation_description="In the detail page for a waypoint marker, the text field for the marker
|
translation_description="In the detail page for a waypoint marker, the text field for the marker
|
||||||
@@ -741,14 +778,18 @@ limitations under the License.
|
|||||||
A marker was inserted.
|
A marker was inserted.
|
||||||
</string>
|
</string>
|
||||||
<string name="marker_label_end"
|
<string name="marker_label_end"
|
||||||
translation_description="When inserting a marker to indicate the end of a track, the label at
|
translation_description="When inserting a marker to indicate the end of a track, the marker's
|
||||||
the end of the marker name. E.g., the marker is named 'track_name (End)'.">
|
name. E.g., 'track_name (End)'.
|
||||||
(End)
|
|
||||||
|
%1$s track name. E.g., 'track_name'.">
|
||||||
|
%1$s (End)
|
||||||
</string>
|
</string>
|
||||||
<string name="marker_label_start"
|
<string name="marker_label_start"
|
||||||
translation_description="When inserting a marker to indicate the beginning of a track, the label
|
translation_description="When inserting a marker to indicate the start of a track, the marker's
|
||||||
at the end of the marker name. E.g., the marker is named 'track_name (Start)'.">
|
name. E.g., 'track_name (Start)'.
|
||||||
(Start)
|
|
||||||
|
%1$s track name. E.g., 'track_name'.">
|
||||||
|
%1$s (Start)
|
||||||
</string>
|
</string>
|
||||||
<string name="marker_list_context_menu_title"
|
<string name="marker_list_context_menu_title"
|
||||||
translation_description="In the context menu for a marker, the title indicating that all the
|
translation_description="In the context menu for a marker, the title indicating that all the
|
||||||
@@ -820,43 +861,6 @@ limitations under the License.
|
|||||||
Waypoint
|
Waypoint
|
||||||
</string>
|
</string>
|
||||||
|
|
||||||
<!-- Maps List -->
|
|
||||||
<string name="maps_list_error"
|
|
||||||
translation_description="When sending a track to an existing Google Map, the text on the
|
|
||||||
'Maps' list page informing the user that his maps cannot be loaded at this time.">
|
|
||||||
Your maps could not be loaded at this time.
|
|
||||||
</string>
|
|
||||||
<string name="maps_list_loading"
|
|
||||||
translation_description="When sending a track to an existing Google Map, the progress dialog
|
|
||||||
message informing the user that his maps are being loaded.">
|
|
||||||
Loading maps...
|
|
||||||
</string>
|
|
||||||
<string name="maps_list_no_maps"
|
|
||||||
translation_description="When sending a track to an existing Google Map, the text on the
|
|
||||||
'Maps' list page informing the user that he doesn't have any maps.">
|
|
||||||
You don\'t have any maps.
|
|
||||||
</string>
|
|
||||||
<string name="maps_list_pick_map"
|
|
||||||
translation_description="When sending a track to an existing Google Map, the text on the
|
|
||||||
'Maps' list page asking the user to pick one of his maps.">
|
|
||||||
Pick a map:
|
|
||||||
</string>
|
|
||||||
<string name="maps_list_public_label"
|
|
||||||
translation_description="When sending a track to an existing Google Map, in the 'Maps'
|
|
||||||
list page, the label next to the maps that are public, as opposed to being unlisted.">
|
|
||||||
Public
|
|
||||||
</string>
|
|
||||||
<string name="maps_list_title"
|
|
||||||
translation_description="When sending a track to an existing Google Map, the title of the
|
|
||||||
page listing the user's maps in Google Maps.">
|
|
||||||
Your Google Maps
|
|
||||||
</string>
|
|
||||||
<string name="maps_list_unlisted_label"
|
|
||||||
translation_description="When sending a track to an existing Google Map, in the 'Maps'
|
|
||||||
list page, the label next to the maps that are unlisted, as opposed to being public.">
|
|
||||||
Unlisted
|
|
||||||
</string>
|
|
||||||
|
|
||||||
<!-- Menu Items -->
|
<!-- Menu Items -->
|
||||||
<string name="menu_aggregated_statistics"
|
<string name="menu_aggregated_statistics"
|
||||||
translation_description="In the menu, to go to the aggregated statistics page.
|
translation_description="In the menu, to go to the aggregated statistics page.
|
||||||
@@ -931,6 +935,10 @@ limitations under the License.
|
|||||||
Screenshot: menu_record_track.png">
|
Screenshot: menu_record_track.png">
|
||||||
Tracks
|
Tracks
|
||||||
</string>
|
</string>
|
||||||
|
<string name="menu_search"
|
||||||
|
translation_description="In the menu, to start a textual search for tracks or waypoints.">
|
||||||
|
Search
|
||||||
|
</string>
|
||||||
|
|
||||||
<!-- My Location -->
|
<!-- My Location -->
|
||||||
<string name="my_location_approximate_location"
|
<string name="my_location_approximate_location"
|
||||||
@@ -1088,6 +1096,24 @@ limitations under the License.
|
|||||||
Finished writing to the SD card.
|
Finished writing to the SD card.
|
||||||
</string>
|
</string>
|
||||||
|
|
||||||
|
<!-- Search interface -->
|
||||||
|
<string name="search_hint"
|
||||||
|
translation_description="The hint shown on the search box until the user types a search query.
|
||||||
|
See suggestion at http://developer.android.com/guide/topics/search/search-dialog.html about
|
||||||
|
keeping this consistent with other Android apps.">
|
||||||
|
Search My Tracks tracks and waypoints
|
||||||
|
</string>
|
||||||
|
<string name="search_global_description"
|
||||||
|
translation_description="Description of what is searched in My Tracks, to be displayed in the
|
||||||
|
global system search settings.">
|
||||||
|
My Tracks tracks and waypoints
|
||||||
|
</string>
|
||||||
|
<string name="search_voice_prompt"
|
||||||
|
translation_description="Prompt shown when the user activates voice search, asking him to
|
||||||
|
speak what will be searched for.">
|
||||||
|
Speak the name of a track or waypoint.
|
||||||
|
</string>
|
||||||
|
|
||||||
<!-- Send Google -->
|
<!-- Send Google -->
|
||||||
<string name="send_google_by_my_tracks"
|
<string name="send_google_by_my_tracks"
|
||||||
translation_description="When sending a track to Google, a note indicating it is created by
|
translation_description="When sending a track to Google, a note indicating it is created by
|
||||||
@@ -1153,36 +1179,12 @@ limitations under the License.
|
|||||||
found.">
|
found.">
|
||||||
No account found
|
No account found
|
||||||
</string>
|
</string>
|
||||||
<string name="send_google_progress_authenticating"
|
<string name="send_google_progress_title"
|
||||||
translation_description="When sending a track to a Google service, the message on the progress
|
translation_description="When sending a track to a Google service, the title on the progress
|
||||||
dialog indicating authenticating to the service.
|
dialog.
|
||||||
|
|
||||||
%1$s: service name. 'Google Maps', 'Google Fusion Tables', or 'Google Docs'.">
|
%1$s: service name. 'Google Maps', 'Google Fusion Tables', or 'Google Docs'.">
|
||||||
Authenticating to %1$s...
|
Sending to %1$s. Please wait...
|
||||||
</string>
|
|
||||||
<string name="send_google_progress_creating"
|
|
||||||
translation_description="When sending a track to a Google service, the message on the progress
|
|
||||||
dialog indicating creating a new service item.
|
|
||||||
|
|
||||||
%1$s: service name. 'Google Maps', 'Google Fusion Tables', or 'Google Docs'.">
|
|
||||||
Creating a new %1$s item...
|
|
||||||
</string>
|
|
||||||
<string name="send_google_progress_preparing_track"
|
|
||||||
translation_description="When sending a track to Google Maps, the message on the progress
|
|
||||||
dialog indicating My Tracks is preparing the track's data for upload.">
|
|
||||||
Preparing track...
|
|
||||||
</string>
|
|
||||||
<string name="send_google_progress_reading_track"
|
|
||||||
translation_description="When sending a track to Google, the message on the progress dialog
|
|
||||||
indicating My Tracks is reading the track from its database.">
|
|
||||||
Reading track...
|
|
||||||
</string>
|
|
||||||
<string name="send_google_progress_sending"
|
|
||||||
translation_description="When sending a track to Google service, the message on the progress
|
|
||||||
dialog indicating My Tracks is sending the track data.
|
|
||||||
|
|
||||||
%1$s: service name. 'Google Maps', 'Google Fusion Tables', or 'Google Docs'.">
|
|
||||||
Sending a track to %1$s...
|
|
||||||
</string>
|
</string>
|
||||||
<string name="send_google_recorded"
|
<string name="send_google_recorded"
|
||||||
translation_description="When sending a track to Google, in the track description, a tag
|
translation_description="When sending a track to Google, in the track description, a tag
|
||||||
@@ -1226,11 +1228,12 @@ limitations under the License.
|
|||||||
Send to...
|
Send to...
|
||||||
</string>
|
</string>
|
||||||
<string name="send_google_track_part_label"
|
<string name="send_google_track_part_label"
|
||||||
translation_description="When sending a track to Google, the label to name segments when
|
translation_description="When sending a track to Google, the label to name segments of a large
|
||||||
splitting a large track.
|
track. E.g., 'track_name (Part 1)'
|
||||||
|
|
||||||
%1$d: part number. E.g., '1'.">
|
%1$s: track name. E.g., 'track_name'
|
||||||
(Part %1$d)
|
%2$d: part number. E.g., '1'.">
|
||||||
|
%1$s (Part %2$d)
|
||||||
</string>
|
</string>
|
||||||
|
|
||||||
<!-- Sensor State -->
|
<!-- Sensor State -->
|
||||||
@@ -1735,13 +1738,6 @@ limitations under the License.
|
|||||||
Screenshot: settings_recording_top.png">
|
Screenshot: settings_recording_top.png">
|
||||||
Voice
|
Voice
|
||||||
</string>
|
</string>
|
||||||
<string name="settings_recording_voice_not_available"
|
|
||||||
translation_description="In the 'Recording' settings, the summary for the 'Voice'
|
|
||||||
explaining that this option is not available on your Android phone.
|
|
||||||
|
|
||||||
Screenshot: settings_recording_voice_not_available.png">
|
|
||||||
Voice announcement is not available on your Android phone.
|
|
||||||
</string>
|
|
||||||
<string name="settings_recording_voice_summary"
|
<string name="settings_recording_voice_summary"
|
||||||
translation_description="In the 'Recording' settings, the summary for the 'Voice'
|
translation_description="In the 'Recording' settings, the summary for the 'Voice'
|
||||||
dropdown option explaining that it configures the time or distance between two voice
|
dropdown option explaining that it configures the time or distance between two voice
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<searchable xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:hint="@string/search_hint"
|
||||||
|
android:includeInGlobalSearch="true"
|
||||||
|
android:label="@string/my_tracks_app_name"
|
||||||
|
android:searchSettingsDescription="@string/search_global_description"
|
||||||
|
android:voicePromptText="@string/search_voice_prompt"
|
||||||
|
android:voiceSearchMode="showVoiceSearchButton|launchRecognizer"
|
||||||
|
android:searchSuggestAuthority="com.google.android.maps.mytracks.search"
|
||||||
|
android:searchSuggestThreshold="2"
|
||||||
|
android:searchSuggestSelection=" ? " />
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2010 Google Inc.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
|
||||||
* use this file except in compliance with the License. You may obtain a copy of
|
|
||||||
* the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
* License for the specific language governing permissions and limitations under
|
|
||||||
* the License.
|
|
||||||
*/
|
|
||||||
package com.google.android.apps.mytracks;
|
|
||||||
|
|
||||||
import com.google.android.maps.mytracks.R;
|
|
||||||
|
|
||||||
import android.accounts.Account;
|
|
||||||
import android.accounts.AccountManager;
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.app.AlertDialog;
|
|
||||||
import android.content.DialogInterface;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Choose which account to upload track information to.
|
|
||||||
*
|
|
||||||
* @author Sandor Dornbush
|
|
||||||
*
|
|
||||||
* @deprecated To be removed.
|
|
||||||
*/
|
|
||||||
public class AccountChooser {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The last selected account.
|
|
||||||
*/
|
|
||||||
private int selectedAccountIndex = -1;
|
|
||||||
private Account selectedAccount = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An interface for receiving updates once the user has selected the account.
|
|
||||||
*/
|
|
||||||
public interface AccountHandler {
|
|
||||||
/**
|
|
||||||
* Handle the account being selected.
|
|
||||||
* @param account The selected account or null if none could be found
|
|
||||||
*/
|
|
||||||
public void onAccountSelected(Account account);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Chooses the best account to upload to.
|
|
||||||
* If no account is found the user will be alerted.
|
|
||||||
* If only one account is found that will be used.
|
|
||||||
* If multiple accounts are found the user will be allowed to choose.
|
|
||||||
*
|
|
||||||
* @param activity The parent activity
|
|
||||||
* @param handler The handler to be notified when an account has been selected
|
|
||||||
*/
|
|
||||||
public void chooseAccount(final Activity activity,
|
|
||||||
final AccountHandler handler) {
|
|
||||||
final Account[] accounts = AccountManager.get(activity)
|
|
||||||
.getAccountsByType(Constants.ACCOUNT_TYPE);
|
|
||||||
if (accounts.length < 1) {
|
|
||||||
alertNoAccounts(activity, handler);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (accounts.length == 1) {
|
|
||||||
handler.onAccountSelected(accounts[0]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO This should be read out of a preference.
|
|
||||||
if (selectedAccount != null) {
|
|
||||||
handler.onAccountSelected(selectedAccount);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Let the user choose.
|
|
||||||
Log.e(Constants.TAG, "Multiple matching accounts found.");
|
|
||||||
final AlertDialog.Builder builder = new AlertDialog.Builder(activity);
|
|
||||||
builder.setTitle(R.string.send_google_choose_account_title);
|
|
||||||
builder.setCancelable(false);
|
|
||||||
builder.setPositiveButton(R.string.generic_ok,
|
|
||||||
new DialogInterface.OnClickListener() {
|
|
||||||
public void onClick(DialogInterface dialog, int which) {
|
|
||||||
dialog.dismiss();
|
|
||||||
|
|
||||||
if (selectedAccountIndex >= 0) {
|
|
||||||
selectedAccount = accounts[selectedAccountIndex];
|
|
||||||
}
|
|
||||||
|
|
||||||
handler.onAccountSelected(selectedAccount);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
builder.setNegativeButton(R.string.generic_cancel,
|
|
||||||
new DialogInterface.OnClickListener() {
|
|
||||||
public void onClick(DialogInterface dialog, int which) {
|
|
||||||
dialog.dismiss();
|
|
||||||
handler.onAccountSelected(null);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
String[] choices = new String[accounts.length];
|
|
||||||
for (int i = 0; i < accounts.length; i++) {
|
|
||||||
choices[i] = accounts[i].name;
|
|
||||||
}
|
|
||||||
builder.setSingleChoiceItems(choices, selectedAccountIndex,
|
|
||||||
new DialogInterface.OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(DialogInterface dialog, int which) {
|
|
||||||
selectedAccountIndex = which;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
builder.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setChosenAccount(String accountName, String accountType) {
|
|
||||||
selectedAccount = new Account(accountName, accountType);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Puts up a dialog alerting the user that no suitable account was found.
|
|
||||||
*/
|
|
||||||
private void alertNoAccounts(final Activity activity,
|
|
||||||
final AccountHandler handler) {
|
|
||||||
Log.e(Constants.TAG, "No matching accounts found.");
|
|
||||||
final AlertDialog.Builder builder = new AlertDialog.Builder(activity);
|
|
||||||
builder.setTitle(R.string.send_google_no_account_title);
|
|
||||||
builder.setMessage(R.string.send_google_no_account_message);
|
|
||||||
builder.setCancelable(true);
|
|
||||||
builder.setPositiveButton(R.string.generic_ok,
|
|
||||||
new DialogInterface.OnClickListener() {
|
|
||||||
public void onClick(DialogInterface dialog, int which) {
|
|
||||||
dialog.dismiss();
|
|
||||||
handler.onAccountSelected(null);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
builder.show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -26,10 +26,8 @@ import com.google.android.apps.mytracks.content.TrackDataHub;
|
|||||||
import com.google.android.apps.mytracks.content.TrackDataHub.ListenerDataType;
|
import com.google.android.apps.mytracks.content.TrackDataHub.ListenerDataType;
|
||||||
import com.google.android.apps.mytracks.content.TrackDataListener;
|
import com.google.android.apps.mytracks.content.TrackDataListener;
|
||||||
import com.google.android.apps.mytracks.content.Waypoint;
|
import com.google.android.apps.mytracks.content.Waypoint;
|
||||||
import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerFactory;
|
|
||||||
import com.google.android.apps.mytracks.stats.DoubleBuffer;
|
import com.google.android.apps.mytracks.stats.DoubleBuffer;
|
||||||
import com.google.android.apps.mytracks.stats.TripStatisticsBuilder;
|
import com.google.android.apps.mytracks.stats.TripStatisticsBuilder;
|
||||||
import com.google.android.apps.mytracks.util.ApiFeatures;
|
|
||||||
import com.google.android.apps.mytracks.util.LocationUtils;
|
import com.google.android.apps.mytracks.util.LocationUtils;
|
||||||
import com.google.android.apps.mytracks.util.UnitConversions;
|
import com.google.android.apps.mytracks.util.UnitConversions;
|
||||||
import com.google.android.maps.mytracks.R;
|
import com.google.android.maps.mytracks.R;
|
||||||
@@ -40,6 +38,7 @@ import android.content.DialogInterface;
|
|||||||
import android.content.DialogInterface.OnClickListener;
|
import android.content.DialogInterface.OnClickListener;
|
||||||
import android.location.Location;
|
import android.location.Location;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.speech.tts.TextToSpeech;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
@@ -116,9 +115,7 @@ public class ChartActivity extends Activity implements TrackDataListener {
|
|||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
// The volume we want to control is the Text-To-Speech volume
|
// The volume we want to control is the Text-To-Speech volume
|
||||||
int volumeStream =
|
setVolumeControlStream(TextToSpeech.Engine.DEFAULT_STREAM);
|
||||||
new StatusAnnouncerFactory(ApiFeatures.getInstance()).getVolumeStream();
|
|
||||||
setVolumeControlStream(volumeStream);
|
|
||||||
|
|
||||||
requestWindowFeature(Window.FEATURE_NO_TITLE);
|
requestWindowFeature(Window.FEATURE_NO_TITLE);
|
||||||
setContentView(R.layout.mytracks_charts);
|
setContentView(R.layout.mytracks_charts);
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
package com.google.android.apps.mytracks;
|
package com.google.android.apps.mytracks;
|
||||||
|
|
||||||
import com.google.android.apps.mytracks.util.ApiFeatures;
|
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
|
||||||
import com.google.android.maps.mytracks.R;
|
import com.google.android.maps.mytracks.R;
|
||||||
|
|
||||||
import android.app.AlertDialog;
|
import android.app.AlertDialog;
|
||||||
@@ -63,8 +63,7 @@ class CheckUnits {
|
|||||||
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
|
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
|
||||||
SharedPreferences.Editor editor = useMetricPreferences.edit();
|
SharedPreferences.Editor editor = useMetricPreferences.edit();
|
||||||
String key = context.getString(R.string.metric_units_key);
|
String key = context.getString(R.string.metric_units_key);
|
||||||
ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges(
|
ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor.putBoolean(key, metric));
|
||||||
editor.putBoolean(key, metric));
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
builder.setOnCancelListener(new DialogInterface.OnCancelListener() {
|
builder.setOnCancelListener(new DialogInterface.OnCancelListener() {
|
||||||
@@ -76,7 +75,7 @@ class CheckUnits {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static void recordCheckPerformed(SharedPreferences preferences) {
|
private static void recordCheckPerformed(SharedPreferences preferences) {
|
||||||
ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges(
|
ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(
|
||||||
preferences.edit().putBoolean(CHECK_UNITS_PREFERENCE_KEY, true));
|
preferences.edit().putBoolean(CHECK_UNITS_PREFERENCE_KEY, true));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,29 +35,18 @@ public abstract class Constants {
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
* onActivityResult request codes:
|
* onActivityResult request codes:
|
||||||
*
|
|
||||||
* TODO: These should be moved to the
|
|
||||||
* activities that call startActivityForResult. Thus do not need to keep a
|
|
||||||
* global order across all activities.
|
|
||||||
*/
|
*/
|
||||||
public static final int GET_LOGIN = 0;
|
public static final int SHOW_TRACK = 0;
|
||||||
public static final int GET_MAP = 1;
|
public static final int SHARE_GPX_FILE = 1;
|
||||||
public static final int SHOW_TRACK = 2;
|
public static final int SHARE_KML_FILE = 2;
|
||||||
public static final int AUTHENTICATE_TO_MAPS = 3;
|
public static final int SHARE_CSV_FILE = 3;
|
||||||
public static final int AUTHENTICATE_TO_FUSION_TABLES = 4;
|
public static final int SHARE_TCX_FILE = 4;
|
||||||
public static final int AUTHENTICATE_TO_DOCLIST = 5;
|
public static final int SAVE_GPX_FILE = 5;
|
||||||
public static final int AUTHENTICATE_TO_TRIX = 6;
|
public static final int SAVE_KML_FILE = 6;
|
||||||
public static final int SHARE_GPX_FILE = 7;
|
public static final int SAVE_CSV_FILE = 7;
|
||||||
public static final int SHARE_KML_FILE = 8;
|
public static final int SAVE_TCX_FILE = 8;
|
||||||
public static final int SHARE_CSV_FILE = 9;
|
public static final int SHOW_WAYPOINT = 9;
|
||||||
public static final int SHARE_TCX_FILE = 10;
|
public static final int WELCOME = 10;
|
||||||
public static final int SAVE_GPX_FILE = 11;
|
|
||||||
public static final int SAVE_KML_FILE = 12;
|
|
||||||
public static final int SAVE_CSV_FILE = 13;
|
|
||||||
public static final int SAVE_TCX_FILE = 14;
|
|
||||||
public static final int SHOW_WAYPOINT = 15;
|
|
||||||
public static final int WELCOME = 16;
|
|
||||||
public static final int CHOOSE_ACCOUNT = 17;
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Menu ids:
|
* Menu ids:
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
package com.google.android.apps.mytracks;
|
package com.google.android.apps.mytracks;
|
||||||
|
|
||||||
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
|
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
|
||||||
import com.google.android.apps.mytracks.util.ApiFeatures;
|
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
|
||||||
import com.google.android.maps.mytracks.R;
|
import com.google.android.maps.mytracks.R;
|
||||||
|
|
||||||
import android.app.AlertDialog;
|
import android.app.AlertDialog;
|
||||||
@@ -66,7 +66,7 @@ public class DeleteAllTracks extends Handler {
|
|||||||
Editor editor = prefs.edit();
|
Editor editor = prefs.edit();
|
||||||
// TODO: Go through data manager
|
// TODO: Go through data manager
|
||||||
editor.putLong(context.getString(R.string.selected_track_key), -1);
|
editor.putLong(context.getString(R.string.selected_track_key), -1);
|
||||||
ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges(editor);
|
ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor);
|
||||||
if (done != null) {
|
if (done != null) {
|
||||||
Handler h = new Handler();
|
Handler h = new Handler();
|
||||||
h.post(done);
|
h.post(done);
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import static com.google.android.apps.mytracks.Constants.TAG;
|
|||||||
|
|
||||||
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
|
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
|
||||||
import com.google.android.apps.mytracks.content.TracksColumns;
|
import com.google.android.apps.mytracks.content.TracksColumns;
|
||||||
import com.google.android.apps.mytracks.util.ApiFeatures;
|
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
|
||||||
import com.google.android.apps.mytracks.util.UriUtils;
|
import com.google.android.apps.mytracks.util.UriUtils;
|
||||||
import com.google.android.maps.mytracks.R;
|
import com.google.android.maps.mytracks.R;
|
||||||
|
|
||||||
@@ -110,7 +110,7 @@ public class DeleteTrack extends Activity
|
|||||||
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
|
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
|
||||||
if (preferences.getLong(selectedKey, -1) == deleteTrackId) {
|
if (preferences.getLong(selectedKey, -1) == deleteTrackId) {
|
||||||
Editor editor = preferences.edit().putLong(selectedKey, -1);
|
Editor editor = preferences.edit().putLong(selectedKey, -1);
|
||||||
ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges(editor);
|
ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,77 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2010 Google Inc.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
|
||||||
* use this file except in compliance with the License. You may obtain a copy of
|
|
||||||
* the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
* License for the specific language governing permissions and limitations under
|
|
||||||
* the License.
|
|
||||||
*/
|
|
||||||
package com.google.android.apps.mytracks;
|
|
||||||
|
|
||||||
import static com.google.android.apps.mytracks.Constants.TAG;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.app.Dialog;
|
|
||||||
import android.util.Log;
|
|
||||||
import android.view.WindowManager.BadTokenException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A class to handle all dialog related events for My Tracks.
|
|
||||||
*
|
|
||||||
* @author Sandor Dornbush
|
|
||||||
*/
|
|
||||||
public class DialogManager {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The equivalent of {@link Dialog#show()}, but for a specific dialog
|
|
||||||
* instance.
|
|
||||||
*/
|
|
||||||
public static void showDialogSafely(Activity activity, final Dialog dialog) {
|
|
||||||
if (activity.isFinishing()) {
|
|
||||||
Log.w(TAG, "Activity finishing - not showing dialog");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
activity.runOnUiThread(new Runnable() {
|
|
||||||
public void run() {
|
|
||||||
try {
|
|
||||||
dialog.show();
|
|
||||||
} catch (BadTokenException e) {
|
|
||||||
Log.w(TAG, "Could not display dialog", e);
|
|
||||||
} catch (IllegalStateException e) {
|
|
||||||
Log.w(TAG, "Could not display dialog", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The equivalent of {@link Dialog#dismiss()}, but for a specific dialog
|
|
||||||
* instance.
|
|
||||||
*/
|
|
||||||
public static void dismissDialogSafely(Activity activity, final Dialog dialog) {
|
|
||||||
if (activity.isFinishing()) {
|
|
||||||
Log.w(TAG, "Activity finishing - not dismissing dialog");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
activity.runOnUiThread(new Runnable() {
|
|
||||||
public void run() {
|
|
||||||
try {
|
|
||||||
dialog.dismiss();
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
// This will be thrown if this dialog was not shown before.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private DialogManager() {}
|
|
||||||
}
|
|
||||||
@@ -81,12 +81,12 @@ public class ExportAllTracks {
|
|||||||
|
|
||||||
String exportFileFormat = activity.getString(R.string.track_list_export_file);
|
String exportFileFormat = activity.getString(R.string.track_list_export_file);
|
||||||
String fileTypes[] = activity.getResources().getStringArray(R.array.file_types);
|
String fileTypes[] = activity.getResources().getStringArray(R.array.file_types);
|
||||||
|
|
||||||
String[] choices = new String[fileTypes.length];
|
String[] choices = new String[fileTypes.length];
|
||||||
for (int i = 0; i < fileTypes.length; i++) {
|
for (int i = 0; i < fileTypes.length; i++) {
|
||||||
choices[i] = String.format(exportFileFormat, fileTypes[i]);
|
choices[i] = String.format(exportFileFormat, fileTypes[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
|
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
|
||||||
builder.setTitle(R.string.track_list_export_all);
|
builder.setTitle(R.string.track_list_export_all);
|
||||||
builder.setSingleChoiceItems(choices, 0, itemClick);
|
builder.setSingleChoiceItems(choices, 0, itemClick);
|
||||||
@@ -160,7 +160,7 @@ public class ExportAllTracks {
|
|||||||
try {
|
try {
|
||||||
MyTracksProviderUtils providerUtils =
|
MyTracksProviderUtils providerUtils =
|
||||||
MyTracksProviderUtils.Factory.get(activity);
|
MyTracksProviderUtils.Factory.get(activity);
|
||||||
cursor = providerUtils.getTracksCursor("");
|
cursor = providerUtils.getTracksCursor(null, null, TracksColumns._ID);
|
||||||
if (cursor == null) {
|
if (cursor == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,11 +25,9 @@ import com.google.android.apps.mytracks.content.TrackDataListener;
|
|||||||
import com.google.android.apps.mytracks.content.TracksColumns;
|
import com.google.android.apps.mytracks.content.TracksColumns;
|
||||||
import com.google.android.apps.mytracks.content.Waypoint;
|
import com.google.android.apps.mytracks.content.Waypoint;
|
||||||
import com.google.android.apps.mytracks.io.file.SaveActivity;
|
import com.google.android.apps.mytracks.io.file.SaveActivity;
|
||||||
import com.google.android.apps.mytracks.io.sendtogoogle.SendType;
|
import com.google.android.apps.mytracks.io.sendtogoogle.SendRequest;
|
||||||
import com.google.android.apps.mytracks.io.sendtogoogle.UploadServiceChooserActivity;
|
import com.google.android.apps.mytracks.io.sendtogoogle.UploadServiceChooserActivity;
|
||||||
import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerFactory;
|
|
||||||
import com.google.android.apps.mytracks.stats.TripStatistics;
|
import com.google.android.apps.mytracks.stats.TripStatistics;
|
||||||
import com.google.android.apps.mytracks.util.ApiFeatures;
|
|
||||||
import com.google.android.apps.mytracks.util.GeoRect;
|
import com.google.android.apps.mytracks.util.GeoRect;
|
||||||
import com.google.android.apps.mytracks.util.LocationUtils;
|
import com.google.android.apps.mytracks.util.LocationUtils;
|
||||||
import com.google.android.maps.GeoPoint;
|
import com.google.android.maps.GeoPoint;
|
||||||
@@ -42,8 +40,8 @@ import android.content.Intent;
|
|||||||
import android.location.Location;
|
import android.location.Location;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Parcelable;
|
|
||||||
import android.provider.Settings;
|
import android.provider.Settings;
|
||||||
|
import android.speech.tts.TextToSpeech;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.ContextMenu;
|
import android.view.ContextMenu;
|
||||||
import android.view.ContextMenu.ContextMenuInfo;
|
import android.view.ContextMenu.ContextMenuInfo;
|
||||||
@@ -86,6 +84,25 @@ public class MapActivity extends com.google.android.maps.MapActivity
|
|||||||
*/
|
*/
|
||||||
private boolean keepMyLocationVisible;
|
private boolean keepMyLocationVisible;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The ID of a track on which we want to show a waypoint.
|
||||||
|
* The waypoint will be shown as soon as the track is loaded.
|
||||||
|
*/
|
||||||
|
private long showWaypointTrackId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The ID of a waypoint which we want to show.
|
||||||
|
* The waypoint will be shown as soon as its track is loaded.
|
||||||
|
*/
|
||||||
|
private long showWaypointId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The track that's currently selected.
|
||||||
|
* This differs from {@link TrackDataHub#getSelectedTrackId} in that this one is only set after
|
||||||
|
* actual track data has been received.
|
||||||
|
*/
|
||||||
|
private long selectedTrackId;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The current pointer location.
|
* The current pointer location.
|
||||||
* This is kept to quickly center on it when the user requests.
|
* This is kept to quickly center on it when the user requests.
|
||||||
@@ -134,9 +151,7 @@ public class MapActivity extends com.google.android.maps.MapActivity
|
|||||||
super.onCreate(bundle);
|
super.onCreate(bundle);
|
||||||
|
|
||||||
// The volume we want to control is the Text-To-Speech volume
|
// The volume we want to control is the Text-To-Speech volume
|
||||||
int volumeStream =
|
setVolumeControlStream(TextToSpeech.Engine.DEFAULT_STREAM);
|
||||||
new StatusAnnouncerFactory(ApiFeatures.getInstance()).getVolumeStream();
|
|
||||||
setVolumeControlStream(volumeStream);
|
|
||||||
|
|
||||||
// We don't need a window title bar:
|
// We don't need a window title bar:
|
||||||
requestWindowFeature(Window.FEATURE_NO_TITLE);
|
requestWindowFeature(Window.FEATURE_NO_TITLE);
|
||||||
@@ -332,6 +347,47 @@ public class MapActivity extends com.google.android.maps.MapActivity
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zooms and pans the map so that the given waypoint is visible, when the given track is loaded.
|
||||||
|
* If the track is already loaded, it does that immediately.
|
||||||
|
*
|
||||||
|
* @param trackId the ID of the track on which to show the waypoint
|
||||||
|
* @param waypointId the ID of the waypoint to show
|
||||||
|
*/
|
||||||
|
public void showWaypoint(long trackId, long waypointId) {
|
||||||
|
synchronized (this) {
|
||||||
|
if (trackId == selectedTrackId) {
|
||||||
|
showWaypoint(waypointId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showWaypointTrackId = trackId;
|
||||||
|
showWaypointId = waypointId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Does the proper zooming/panning for a just-loaded track.
|
||||||
|
* This may be either zooming to a waypoint that has been previously selected, or
|
||||||
|
* zooming to the whole track.
|
||||||
|
*
|
||||||
|
* @param track the loaded track
|
||||||
|
*/
|
||||||
|
private void zoomLoadedTrack(Track track) {
|
||||||
|
synchronized (this) {
|
||||||
|
if (track.getId() == showWaypointTrackId) {
|
||||||
|
// There's a waypoint to show in this track.
|
||||||
|
showWaypoint(showWaypointId);
|
||||||
|
|
||||||
|
showWaypointId = 0L;
|
||||||
|
showWaypointTrackId = 0L;
|
||||||
|
} else {
|
||||||
|
// Zoom out to show the whole track.
|
||||||
|
zoomMapToBoundaries(track);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSelectedTrackChanged(final Track track, final boolean isRecording) {
|
public void onSelectedTrackChanged(final Track track, final boolean isRecording) {
|
||||||
runOnUiThread(new Runnable() {
|
runOnUiThread(new Runnable() {
|
||||||
@@ -345,7 +401,12 @@ public class MapActivity extends com.google.android.maps.MapActivity
|
|||||||
if (trackSelected) {
|
if (trackSelected) {
|
||||||
busyPane.setVisibility(View.VISIBLE);
|
busyPane.setVisibility(View.VISIBLE);
|
||||||
|
|
||||||
zoomMapToBoundaries(track);
|
synchronized (this) {
|
||||||
|
// Need to get the track ID only at this point, to prevent a race condition
|
||||||
|
// among showWaypoint, zoomLoadedTrack and dataHub.loadTrack.
|
||||||
|
selectedTrackId = track.getId();
|
||||||
|
zoomLoadedTrack(track);
|
||||||
|
}
|
||||||
|
|
||||||
mapOverlay.setShowEndMarker(!isRecording);
|
mapOverlay.setShowEndMarker(!isRecording);
|
||||||
busyPane.setVisibility(View.GONE);
|
busyPane.setVisibility(View.GONE);
|
||||||
@@ -404,21 +465,18 @@ public class MapActivity extends com.google.android.maps.MapActivity
|
|||||||
long trackId = dataHub.getSelectedTrackId();
|
long trackId = dataHub.getSelectedTrackId();
|
||||||
switch (item.getItemId()) {
|
switch (item.getItemId()) {
|
||||||
case Constants.MENU_SEND_TO_GOOGLE:
|
case Constants.MENU_SEND_TO_GOOGLE:
|
||||||
intent = new Intent(this, UploadServiceChooserActivity.class);
|
intent = new Intent(this, UploadServiceChooserActivity.class)
|
||||||
intent.putExtra(UploadServiceChooserActivity.TRACK_ID, trackId);
|
.putExtra(SendRequest.SEND_REQUEST_KEY, new SendRequest(trackId, true, true, true));
|
||||||
startActivity(intent);
|
startActivity(intent);
|
||||||
return true;
|
return true;
|
||||||
case Constants.MENU_SHARE_MAP:
|
case Constants.MENU_SHARE_MAP:
|
||||||
intent = new Intent(this, UploadServiceChooserActivity.class);
|
intent = new Intent(this, UploadServiceChooserActivity.class)
|
||||||
intent.putExtra(UploadServiceChooserActivity.TRACK_ID, trackId);
|
.putExtra(SendRequest.SEND_REQUEST_KEY, new SendRequest(trackId, true, false, false));
|
||||||
intent.putExtra(UploadServiceChooserActivity.SEND_TYPE, (Parcelable) SendType.MAPS);
|
|
||||||
startActivity(intent);
|
startActivity(intent);
|
||||||
return true;
|
return true;
|
||||||
case Constants.MENU_SHARE_FUSION_TABLE:
|
case Constants.MENU_SHARE_FUSION_TABLE:
|
||||||
intent = new Intent(this, UploadServiceChooserActivity.class);
|
intent = new Intent(this, UploadServiceChooserActivity.class)
|
||||||
intent.putExtra(UploadServiceChooserActivity.TRACK_ID, trackId);
|
.putExtra(SendRequest.SEND_REQUEST_KEY, new SendRequest(trackId, false, true, false));
|
||||||
intent.putExtra(
|
|
||||||
UploadServiceChooserActivity.SEND_TYPE, (Parcelable) SendType.FUSION_TABLES);
|
|
||||||
startActivity(intent);
|
startActivity(intent);
|
||||||
return true;
|
return true;
|
||||||
case Constants.MENU_SAVE_GPX_FILE:
|
case Constants.MENU_SAVE_GPX_FILE:
|
||||||
|
|||||||
@@ -1,175 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2011 Google Inc.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
|
||||||
* use this file except in compliance with the License. You may obtain a copy of
|
|
||||||
* the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
* License for the specific language governing permissions and limitations under
|
|
||||||
* the License.
|
|
||||||
*/
|
|
||||||
package com.google.android.apps.mytracks;
|
|
||||||
|
|
||||||
import static com.google.android.apps.mytracks.Constants.TAG;
|
|
||||||
|
|
||||||
import com.google.android.apps.mytracks.io.AuthManager;
|
|
||||||
import com.google.android.apps.mytracks.io.AuthManager.AuthCallback;
|
|
||||||
import com.google.android.apps.mytracks.io.AuthManagerFactory;
|
|
||||||
import com.google.android.apps.mytracks.io.maps.MapsConstants;
|
|
||||||
import com.google.android.apps.mytracks.io.maps.MapsFacade;
|
|
||||||
import com.google.android.maps.mytracks.R;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.util.Log;
|
|
||||||
import android.view.View;
|
|
||||||
import android.widget.AdapterView;
|
|
||||||
import android.widget.AdapterView.OnItemClickListener;
|
|
||||||
import android.widget.ListView;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Activity which displays the list of current Google Maps for the user.
|
|
||||||
* Returns RESULT_OK if the user picked a map, and returns "mapid" as an extra.
|
|
||||||
*
|
|
||||||
* @author Rodrigo Damazio
|
|
||||||
*/
|
|
||||||
public class MapsList extends Activity implements MapsFacade.MapsListCallback {
|
|
||||||
private static final int GET_LOGIN = 1;
|
|
||||||
|
|
||||||
public static final String EXTRA_ACCOUNT_NAME = "accountName";
|
|
||||||
public static final String EXTRA_ACCOUNT_TYPE = "accountType";
|
|
||||||
|
|
||||||
private MapsFacade mapsClient;
|
|
||||||
private AuthManager auth;
|
|
||||||
private MapsListAdapter listAdapter;
|
|
||||||
|
|
||||||
private final OnItemClickListener clickListener =
|
|
||||||
new OnItemClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onItemClick(AdapterView<?> parent, View v, int position,
|
|
||||||
long id) {
|
|
||||||
Intent result = new Intent();
|
|
||||||
result.putExtra("mapid", (String) listAdapter.getItem(position));
|
|
||||||
setResult(RESULT_OK, result);
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Called when the activity is first created. */
|
|
||||||
@Override
|
|
||||||
public void onCreate(Bundle icicle) {
|
|
||||||
super.onCreate(icicle);
|
|
||||||
|
|
||||||
auth = AuthManagerFactory.getAuthManager(this, GET_LOGIN, null, true,
|
|
||||||
MapsConstants.SERVICE_NAME);
|
|
||||||
|
|
||||||
setContentView(R.layout.list);
|
|
||||||
|
|
||||||
listAdapter = new MapsListAdapter(this);
|
|
||||||
|
|
||||||
ListView list = (ListView) findViewById(R.id.maplist);
|
|
||||||
list.setOnItemClickListener(clickListener);
|
|
||||||
list.setAdapter(listAdapter);
|
|
||||||
|
|
||||||
startLogin();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void startLogin() {
|
|
||||||
// Starts in the UI thread.
|
|
||||||
// TODO fix this for non-froyo devices.
|
|
||||||
if (AuthManagerFactory.useModernAuthManager()) {
|
|
||||||
Intent intent = getIntent();
|
|
||||||
String accountName = intent.getStringExtra(EXTRA_ACCOUNT_NAME);
|
|
||||||
String accountType = intent.getStringExtra(EXTRA_ACCOUNT_TYPE);
|
|
||||||
if (accountName == null || accountType == null) {
|
|
||||||
Log.e(TAG, "Didn't receive account name or type");
|
|
||||||
setResult(RESULT_CANCELED);
|
|
||||||
finish();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
doLogin(auth.getAccountObject(accountName, accountType));
|
|
||||||
} else {
|
|
||||||
doLogin(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void doLogin(final Object account) {
|
|
||||||
// Starts in the UI thread.
|
|
||||||
auth.doLogin(new AuthCallback() {
|
|
||||||
@Override
|
|
||||||
public void onAuthResult(boolean success) {
|
|
||||||
if (!success) {
|
|
||||||
setResult(RESULT_CANCELED);
|
|
||||||
finish();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Runs in UI thread.
|
|
||||||
mapsClient = new MapsFacade(MapsList.this, auth);
|
|
||||||
|
|
||||||
startLookup();
|
|
||||||
}
|
|
||||||
}, account);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void startLookup() {
|
|
||||||
// Starts in the UI thread.
|
|
||||||
new Thread() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
// Communication with Maps happens in its own thread.
|
|
||||||
// This will call onReceivedMapListing below.
|
|
||||||
final boolean success = mapsClient.getMapsList(MapsList.this);
|
|
||||||
|
|
||||||
runOnUiThread(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
// Updating the UI when done happens in the UI thread.
|
|
||||||
onLookupDone(success);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onReceivedMapListing(final String mapId, final String title,
|
|
||||||
final String description, final boolean isPublic) {
|
|
||||||
// Starts in the communication thread.
|
|
||||||
runOnUiThread(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
// Updating the list with new contents happens in the UI thread.
|
|
||||||
listAdapter.addMapListing(mapId, title, description, isPublic);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onLookupDone(boolean success) {
|
|
||||||
// Starts in the UI thread.
|
|
||||||
findViewById(R.id.loading).setVisibility(View.GONE);
|
|
||||||
if (!success) {
|
|
||||||
findViewById(R.id.failed).setVisibility(View.VISIBLE);
|
|
||||||
}
|
|
||||||
TextView emptyView = (TextView) findViewById(R.id.mapslist_empty);
|
|
||||||
ListView list = (ListView) findViewById(R.id.maplist);
|
|
||||||
list.setEmptyView(emptyView);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onActivityResult(int requestCode, int resultCode,
|
|
||||||
Intent data) {
|
|
||||||
if (requestCode == GET_LOGIN) {
|
|
||||||
auth.authResult(resultCode, data);
|
|
||||||
}
|
|
||||||
super.onActivityResult(requestCode, resultCode, data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,135 +0,0 @@
|
|||||||
package com.google.android.apps.mytracks;
|
|
||||||
|
|
||||||
import com.google.android.maps.mytracks.R;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.database.DataSetObserver;
|
|
||||||
import android.graphics.Color;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.ListAdapter;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.Iterator;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.Vector;
|
|
||||||
|
|
||||||
public class MapsListAdapter implements ListAdapter {
|
|
||||||
private Vector<String[]> mapsList;
|
|
||||||
private Vector<Boolean> publicList;
|
|
||||||
private Set<DataSetObserver> observerSet;
|
|
||||||
private final Activity activity;
|
|
||||||
|
|
||||||
public MapsListAdapter(Activity activity) {
|
|
||||||
this.activity = activity;
|
|
||||||
|
|
||||||
mapsList = new Vector<String[]>();
|
|
||||||
publicList = new Vector<Boolean>();
|
|
||||||
observerSet = new HashSet<DataSetObserver>();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void addMapListing(String mapId, String title,
|
|
||||||
String description, boolean isPublic) {
|
|
||||||
synchronized (mapsList) {
|
|
||||||
// Search through the maps list to see if it has the mapid and
|
|
||||||
// remove it if so, so that we can replace it with updated info
|
|
||||||
for (int i = 0; i < mapsList.size(); ++i) {
|
|
||||||
if (mapId.equals(mapsList.get(i)[0])) {
|
|
||||||
mapsList.remove(i);
|
|
||||||
publicList.remove(i);
|
|
||||||
--i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mapsList.add(new String[] { mapId, title, description });
|
|
||||||
publicList.add(isPublic);
|
|
||||||
}
|
|
||||||
|
|
||||||
Iterator<DataSetObserver> iter = observerSet.iterator();
|
|
||||||
while (iter.hasNext()) {
|
|
||||||
iter.next().onChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public String[] getMapListingArray(int position) {
|
|
||||||
return mapsList.get(position);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean areAllItemsEnabled() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isEnabled(int position) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getCount() {
|
|
||||||
return mapsList.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Object getItem(int position) {
|
|
||||||
return mapsList.get(position)[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public long getItemId(int position) {
|
|
||||||
return position;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getItemViewType(int position) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public View getView(int position, View convertView, ViewGroup parent) {
|
|
||||||
if (convertView == null) {
|
|
||||||
convertView =
|
|
||||||
activity.getLayoutInflater().inflate(R.layout.listitem, parent, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
String[] map = mapsList.get(position);
|
|
||||||
((TextView) convertView.findViewById(R.id.maplistitem)).setText(map[1]);
|
|
||||||
((TextView) convertView.findViewById(R.id.maplistdesc)).setText(map[2]);
|
|
||||||
TextView publicUnlisted =
|
|
||||||
(TextView) convertView.findViewById(R.id.maplistpublic);
|
|
||||||
if (publicList.get(position)) {
|
|
||||||
publicUnlisted.setTextColor(Color.RED);
|
|
||||||
publicUnlisted.setText(R.string.maps_list_public_label);
|
|
||||||
} else {
|
|
||||||
publicUnlisted.setTextColor(Color.GREEN);
|
|
||||||
publicUnlisted.setText(R.string.maps_list_unlisted_label);
|
|
||||||
}
|
|
||||||
|
|
||||||
return convertView;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getViewTypeCount() {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean hasStableIds() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isEmpty() {
|
|
||||||
return mapsList.isEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void registerDataSetObserver(DataSetObserver observer) {
|
|
||||||
observerSet.add(observer);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void unregisterDataSetObserver(DataSetObserver observer) {
|
|
||||||
observerSet.remove(observer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -37,6 +37,8 @@ class MenuManager {
|
|||||||
|
|
||||||
public boolean onCreateOptionsMenu(Menu menu) {
|
public boolean onCreateOptionsMenu(Menu menu) {
|
||||||
activity.getMenuInflater().inflate(R.menu.main, menu);
|
activity.getMenuInflater().inflate(R.menu.main, menu);
|
||||||
|
|
||||||
|
// TODO: Replace search button with search widget if API level >= 11
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,6 +87,10 @@ class MenuManager {
|
|||||||
case R.id.menu_help: {
|
case R.id.menu_help: {
|
||||||
return startActivity(WelcomeActivity.class);
|
return startActivity(WelcomeActivity.class);
|
||||||
}
|
}
|
||||||
|
case R.id.menu_search: {
|
||||||
|
// TODO: Pass the current track ID and current location to do some fancier ranking.
|
||||||
|
activity.onSearchRequested();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,13 +21,14 @@ import com.google.android.apps.analytics.GoogleAnalyticsTracker;
|
|||||||
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
|
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
|
||||||
import com.google.android.apps.mytracks.content.TrackDataHub;
|
import com.google.android.apps.mytracks.content.TrackDataHub;
|
||||||
import com.google.android.apps.mytracks.content.TracksColumns;
|
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.WaypointCreationRequest;
|
||||||
|
import com.google.android.apps.mytracks.content.WaypointsColumns;
|
||||||
import com.google.android.apps.mytracks.io.file.TempFileCleaner;
|
import com.google.android.apps.mytracks.io.file.TempFileCleaner;
|
||||||
import com.google.android.apps.mytracks.services.ITrackRecordingService;
|
import com.google.android.apps.mytracks.services.ITrackRecordingService;
|
||||||
import com.google.android.apps.mytracks.services.ServiceUtils;
|
import com.google.android.apps.mytracks.services.ServiceUtils;
|
||||||
import com.google.android.apps.mytracks.services.TrackRecordingServiceConnection;
|
import com.google.android.apps.mytracks.services.TrackRecordingServiceConnection;
|
||||||
import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerFactory;
|
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
|
||||||
import com.google.android.apps.mytracks.util.ApiFeatures;
|
|
||||||
import com.google.android.apps.mytracks.util.EulaUtil;
|
import com.google.android.apps.mytracks.util.EulaUtil;
|
||||||
import com.google.android.apps.mytracks.util.SystemUtils;
|
import com.google.android.apps.mytracks.util.SystemUtils;
|
||||||
import com.google.android.apps.mytracks.util.UriUtils;
|
import com.google.android.apps.mytracks.util.UriUtils;
|
||||||
@@ -46,6 +47,7 @@ import android.content.res.Resources;
|
|||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.RemoteException;
|
import android.os.RemoteException;
|
||||||
|
import android.speech.tts.TextToSpeech;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
@@ -67,7 +69,7 @@ import android.widget.Toast;
|
|||||||
@SuppressWarnings("deprecation")
|
@SuppressWarnings("deprecation")
|
||||||
public class MyTracks extends TabActivity implements OnTouchListener {
|
public class MyTracks extends TabActivity implements OnTouchListener {
|
||||||
private static final int DIALOG_EULA_ID = 0;
|
private static final int DIALOG_EULA_ID = 0;
|
||||||
|
|
||||||
private TrackDataHub dataHub;
|
private TrackDataHub dataHub;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -139,9 +141,8 @@ public class MyTracks extends TabActivity implements OnTouchListener {
|
|||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
Log.d(TAG, "MyTracks.onCreate");
|
Log.d(TAG, "MyTracks.onCreate");
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
ApiFeatures apiFeatures = ApiFeatures.getInstance();
|
|
||||||
if (!SystemUtils.isRelease(this)) {
|
if (!SystemUtils.isRelease(this)) {
|
||||||
apiFeatures.getApiAdapter().enableStrictMode();
|
ApiAdapterFactory.getApiAdapter().enableStrictMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
tracker = GoogleAnalyticsTracker.getInstance();
|
tracker = GoogleAnalyticsTracker.getInstance();
|
||||||
@@ -157,14 +158,14 @@ public class MyTracks extends TabActivity implements OnTouchListener {
|
|||||||
menuManager = new MenuManager(this);
|
menuManager = new MenuManager(this);
|
||||||
serviceConnection = new TrackRecordingServiceConnection(this, serviceBindCallback);
|
serviceConnection = new TrackRecordingServiceConnection(this, serviceBindCallback);
|
||||||
|
|
||||||
// The volume we want to control is the Text-To-Speech volume
|
setVolumeControlStream(TextToSpeech.Engine.DEFAULT_STREAM);
|
||||||
int volumeStream =
|
|
||||||
new StatusAnnouncerFactory(apiFeatures).getVolumeStream();
|
|
||||||
setVolumeControlStream(volumeStream);
|
|
||||||
|
|
||||||
// We don't need a window title bar:
|
// We don't need a window title bar:
|
||||||
requestWindowFeature(Window.FEATURE_NO_TITLE);
|
requestWindowFeature(Window.FEATURE_NO_TITLE);
|
||||||
|
|
||||||
|
// If the user just starts typing (on a device with a keyboard), we start a search.
|
||||||
|
setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
|
||||||
|
|
||||||
final Resources res = getResources();
|
final Resources res = getResources();
|
||||||
final TabHost tabHost = getTabHost();
|
final TabHost tabHost = getTabHost();
|
||||||
tabHost.addTab(tabHost.newTabSpec("tab1")
|
tabHost.addTab(tabHost.newTabSpec("tab1")
|
||||||
@@ -199,7 +200,7 @@ public class MyTracks extends TabActivity implements OnTouchListener {
|
|||||||
showDialog(DIALOG_EULA_ID);
|
showDialog(DIALOG_EULA_ID);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onStart() {
|
protected void onStart() {
|
||||||
Log.d(TAG, "MyTracks.onStart");
|
Log.d(TAG, "MyTracks.onStart");
|
||||||
@@ -219,6 +220,19 @@ public class MyTracks extends TabActivity implements OnTouchListener {
|
|||||||
&& UriUtils.matchesContentUri(data, TracksColumns.CONTENT_URI)) {
|
&& UriUtils.matchesContentUri(data, TracksColumns.CONTENT_URI)) {
|
||||||
long trackId = ContentUris.parseId(data);
|
long trackId = ContentUris.parseId(data);
|
||||||
dataHub.loadTrack(trackId);
|
dataHub.loadTrack(trackId);
|
||||||
|
} else if (Intent.ACTION_VIEW.equals(action)
|
||||||
|
&& WaypointsColumns.CONTENT_ITEMTYPE.equals(intent.getType())
|
||||||
|
&& UriUtils.matchesContentUri(data, WaypointsColumns.CONTENT_URI)) {
|
||||||
|
// TODO(rdamazio): Waypoint URIs should be base/trackid/waypointid
|
||||||
|
long waypointId = ContentUris.parseId(data);
|
||||||
|
Waypoint waypoint = providerUtils.getWaypoint(waypointId);
|
||||||
|
long trackId = waypoint.getTrackId();
|
||||||
|
|
||||||
|
// Request that the waypoint is shown (now or when the right track is loaded).
|
||||||
|
showWaypoint(trackId, waypointId);
|
||||||
|
|
||||||
|
// Load the right track, if not loaded already.
|
||||||
|
dataHub.loadTrack(trackId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -293,7 +307,7 @@ public class MyTracks extends TabActivity implements OnTouchListener {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onCreateOptionsMenu(Menu menu) {
|
public boolean onCreateOptionsMenu(Menu menu) {
|
||||||
super.onCreateOptionsMenu(menu);
|
super.onCreateOptionsMenu(menu);
|
||||||
@@ -366,12 +380,7 @@ public class MyTracks extends TabActivity implements OnTouchListener {
|
|||||||
if (results != null) {
|
if (results != null) {
|
||||||
final long waypointId = results.getLongExtra(WaypointDetails.WAYPOINT_ID_EXTRA, -1);
|
final long waypointId = results.getLongExtra(WaypointDetails.WAYPOINT_ID_EXTRA, -1);
|
||||||
if (waypointId >= 0) {
|
if (waypointId >= 0) {
|
||||||
MapActivity map =
|
showWaypoint(trackId, waypointId);
|
||||||
(MapActivity) getLocalActivityManager().getActivity("tab1");
|
|
||||||
if (map != null) {
|
|
||||||
getTabHost().setCurrentTab(0);
|
|
||||||
map.showWaypoint(waypointId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@@ -387,6 +396,17 @@ public class MyTracks extends TabActivity implements OnTouchListener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void showWaypoint(long trackId, long waypointId) {
|
||||||
|
MapActivity map =
|
||||||
|
(MapActivity) getLocalActivityManager().getActivity("tab1");
|
||||||
|
if (map != null) {
|
||||||
|
getTabHost().setCurrentTab(0);
|
||||||
|
map.showWaypoint(trackId, waypointId);
|
||||||
|
} else {
|
||||||
|
Log.e(TAG, "Couldnt' get map tab");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onTouch(View v, MotionEvent event) {
|
public boolean onTouch(View v, MotionEvent event) {
|
||||||
if (event.getAction() == MotionEvent.ACTION_DOWN) {
|
if (event.getAction() == MotionEvent.ACTION_DOWN) {
|
||||||
|
|||||||
@@ -0,0 +1,217 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2011 Google Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||||
|
* use this file except in compliance with the License. You may obtain a copy of
|
||||||
|
* the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations under
|
||||||
|
* the License.
|
||||||
|
*/
|
||||||
|
package com.google.android.apps.mytracks;
|
||||||
|
|
||||||
|
import static com.google.android.apps.mytracks.Constants.TAG;
|
||||||
|
|
||||||
|
import com.google.android.apps.mytracks.content.SearchEngine;
|
||||||
|
import com.google.android.apps.mytracks.content.SearchEngine.ScoredResult;
|
||||||
|
import com.google.android.apps.mytracks.content.SearchEngine.SearchQuery;
|
||||||
|
import com.google.android.apps.mytracks.content.SearchEngineProvider;
|
||||||
|
import com.google.android.apps.mytracks.content.Track;
|
||||||
|
import com.google.android.apps.mytracks.content.TracksColumns;
|
||||||
|
import com.google.android.apps.mytracks.content.Waypoint;
|
||||||
|
import com.google.android.apps.mytracks.content.WaypointsColumns;
|
||||||
|
import com.google.android.maps.mytracks.R;
|
||||||
|
|
||||||
|
import android.app.ListActivity;
|
||||||
|
import android.app.SearchManager;
|
||||||
|
import android.content.ContentUris;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.location.Location;
|
||||||
|
import android.location.LocationManager;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.provider.SearchRecentSuggestions;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.ListView;
|
||||||
|
import android.widget.SimpleAdapter;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.SortedSet;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activity to search for tracks or waypoints.
|
||||||
|
*
|
||||||
|
* @author Rodrigo Damazio
|
||||||
|
*/
|
||||||
|
public class SearchActivity extends ListActivity {
|
||||||
|
|
||||||
|
private static final String EXTRA_CURRENT_TRACK_ID = "trackId";
|
||||||
|
|
||||||
|
private static final boolean LOG_SCORES = true;
|
||||||
|
|
||||||
|
private SearchEngine engine;
|
||||||
|
private LocationManager locationManager;
|
||||||
|
|
||||||
|
private SearchRecentSuggestions suggestions;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
|
engine = new SearchEngine(this);
|
||||||
|
suggestions = SearchEngineProvider.newHelper(this);
|
||||||
|
locationManager = (LocationManager) getSystemService(LOCATION_SERVICE);
|
||||||
|
|
||||||
|
handleIntent(getIntent());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onNewIntent(Intent intent) {
|
||||||
|
setIntent(intent);
|
||||||
|
handleIntent(intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleIntent(Intent intent) {
|
||||||
|
if (!Intent.ACTION_SEARCH.equals(intent.getAction())) {
|
||||||
|
Log.e(TAG, "Got bad search intent: " + intent);
|
||||||
|
finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String textQuery = intent.getStringExtra(SearchManager.QUERY);
|
||||||
|
Location currentLocation = locationManager.getLastKnownLocation("gps");
|
||||||
|
long currentTrackId = intent.getLongExtra(EXTRA_CURRENT_TRACK_ID, -1);
|
||||||
|
long currentTimestamp = System.currentTimeMillis();
|
||||||
|
|
||||||
|
final SearchQuery query =
|
||||||
|
new SearchQuery(textQuery, currentLocation, currentTrackId, currentTimestamp);
|
||||||
|
|
||||||
|
// Do the actual search in a separate thread.
|
||||||
|
new Thread() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
doSearch(query);
|
||||||
|
}
|
||||||
|
}.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void doSearch(SearchQuery query) {
|
||||||
|
SortedSet<ScoredResult> scoredResults = engine.search(query);
|
||||||
|
|
||||||
|
final List<? extends Map<String, ?>> displayResults = prepareResultsforDisplay(scoredResults);
|
||||||
|
|
||||||
|
if (LOG_SCORES) {
|
||||||
|
Log.i(TAG, "Search scores: " + displayResults);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then go back to the UI thread to display them.
|
||||||
|
runOnUiThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
showSearchResults(displayResults);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Save the query as a suggestion for the future.
|
||||||
|
suggestions.saveRecentQuery(query.textQuery, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Map<String, Object>> prepareResultsforDisplay(
|
||||||
|
Collection<ScoredResult> scoredResults) {
|
||||||
|
ArrayList<Map<String, Object>> output = new ArrayList<Map<String, Object>>(scoredResults.size());
|
||||||
|
for (ScoredResult result : scoredResults) {
|
||||||
|
Map<String, Object> resultMap = new HashMap<String, Object>();
|
||||||
|
if (result.track != null) {
|
||||||
|
prepareTrackForDisplay(result.track, resultMap);
|
||||||
|
} else {
|
||||||
|
prepareWaypointForDisplay(result.waypoint, resultMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
resultMap.put("score", result.score);
|
||||||
|
|
||||||
|
output.add(resultMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void prepareWaypointForDisplay(Waypoint waypoint, Map<String, Object> resultMap) {
|
||||||
|
// TODO: Yellow pushpin for statistics marker.
|
||||||
|
resultMap.put("icon", R.drawable.blue_pushpin);
|
||||||
|
resultMap.put("name", waypoint.getName());
|
||||||
|
resultMap.put("description", waypoint.getDescription());
|
||||||
|
resultMap.put("category", waypoint.getCategory());
|
||||||
|
resultMap.put("time", String.format("%tc", waypoint.getLocation().getTime()));
|
||||||
|
resultMap.put("trackId", waypoint.getTrackId());
|
||||||
|
resultMap.put("waypointId", waypoint.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void prepareTrackForDisplay(Track track, Map<String, Object> resultMap) {
|
||||||
|
resultMap.put("icon", R.drawable.track);
|
||||||
|
resultMap.put("name", track.getName());
|
||||||
|
resultMap.put("description", track.getDescription());
|
||||||
|
resultMap.put("category", track.getCategory());
|
||||||
|
resultMap.put("time", String.format("%tc", track.getStatistics().getStartTime()));
|
||||||
|
resultMap.put("trackId", track.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows the given search results.
|
||||||
|
* Must be run from the UI thread.
|
||||||
|
*
|
||||||
|
* @param data the results to show, properly ordered
|
||||||
|
*/
|
||||||
|
private void showSearchResults(List<? extends Map<String, ?>> data) {
|
||||||
|
SimpleAdapter adapter = new SimpleAdapter(this, data,
|
||||||
|
// TODO: Custom view for search results.
|
||||||
|
R.layout.mytracks_list_item,
|
||||||
|
new String[] {
|
||||||
|
"icon",
|
||||||
|
"name",
|
||||||
|
"description",
|
||||||
|
"category",
|
||||||
|
"time"
|
||||||
|
},
|
||||||
|
new int[] {
|
||||||
|
R.id.trackdetails_item_icon,
|
||||||
|
R.id.trackdetails_item_name,
|
||||||
|
R.id.trackdetails_item_description,
|
||||||
|
R.id.trackdetails_item_category,
|
||||||
|
R.id.trackdetails_item_time
|
||||||
|
});
|
||||||
|
|
||||||
|
setListAdapter(adapter);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onListItemClick(ListView l, View v, int position, long id) {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
Map<String, Object> clickedData = (Map<String, Object>) getListAdapter().getItem(position);
|
||||||
|
|
||||||
|
startActivity(createViewDataIntent(clickedData));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Intent createViewDataIntent(Map<String, Object> clickedData) {
|
||||||
|
Intent intent = new Intent(Intent.ACTION_VIEW);
|
||||||
|
if (clickedData.containsKey("waypointId")) {
|
||||||
|
long waypointId = (Long) clickedData.get("waypointId");
|
||||||
|
Uri uri = ContentUris.withAppendedId(WaypointsColumns.CONTENT_URI, waypointId);
|
||||||
|
intent.setDataAndType(uri, WaypointsColumns.CONTENT_ITEMTYPE);
|
||||||
|
} else {
|
||||||
|
long trackId = (Long) clickedData.get("trackId");
|
||||||
|
Uri uri = ContentUris.withAppendedId(TracksColumns.CONTENT_URI, trackId);
|
||||||
|
intent.setDataAndType(uri, TracksColumns.CONTENT_ITEMTYPE);
|
||||||
|
}
|
||||||
|
return intent;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,13 +20,13 @@ import static com.google.android.apps.mytracks.Constants.TAG;
|
|||||||
import com.google.android.apps.mytracks.io.backup.BackupActivityHelper;
|
import com.google.android.apps.mytracks.io.backup.BackupActivityHelper;
|
||||||
import com.google.android.apps.mytracks.io.backup.BackupPreferencesListener;
|
import com.google.android.apps.mytracks.io.backup.BackupPreferencesListener;
|
||||||
import com.google.android.apps.mytracks.services.sensors.ant.AntUtils;
|
import com.google.android.apps.mytracks.services.sensors.ant.AntUtils;
|
||||||
import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerFactory;
|
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
|
||||||
import com.google.android.apps.mytracks.util.ApiFeatures;
|
|
||||||
import com.google.android.apps.mytracks.util.BluetoothDeviceUtils;
|
import com.google.android.apps.mytracks.util.BluetoothDeviceUtils;
|
||||||
import com.google.android.apps.mytracks.util.UnitConversions;
|
import com.google.android.apps.mytracks.util.UnitConversions;
|
||||||
import com.google.android.maps.mytracks.R;
|
import com.google.android.maps.mytracks.R;
|
||||||
|
|
||||||
import android.app.AlertDialog;
|
import android.app.AlertDialog;
|
||||||
|
import android.bluetooth.BluetoothAdapter;
|
||||||
import android.content.DialogInterface;
|
import android.content.DialogInterface;
|
||||||
import android.content.DialogInterface.OnClickListener;
|
import android.content.DialogInterface.OnClickListener;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
@@ -44,6 +44,7 @@ import android.preference.PreferenceCategory;
|
|||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
import android.preference.PreferenceScreen;
|
import android.preference.PreferenceScreen;
|
||||||
import android.provider.Settings;
|
import android.provider.Settings;
|
||||||
|
import android.speech.tts.TextToSpeech;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
@@ -102,10 +103,7 @@ public class SettingsActivity extends PreferenceActivity {
|
|||||||
super.onCreate(icicle);
|
super.onCreate(icicle);
|
||||||
|
|
||||||
// The volume we want to control is the Text-To-Speech volume
|
// The volume we want to control is the Text-To-Speech volume
|
||||||
ApiFeatures apiFeatures = ApiFeatures.getInstance();
|
setVolumeControlStream(TextToSpeech.Engine.DEFAULT_STREAM);
|
||||||
int volumeStream =
|
|
||||||
new StatusAnnouncerFactory(apiFeatures).getVolumeStream();
|
|
||||||
setVolumeControlStream(volumeStream);
|
|
||||||
|
|
||||||
// Tell it where to read/write preferences
|
// Tell it where to read/write preferences
|
||||||
PreferenceManager preferenceManager = getPreferenceManager();
|
PreferenceManager preferenceManager = getPreferenceManager();
|
||||||
@@ -113,23 +111,13 @@ public class SettingsActivity extends PreferenceActivity {
|
|||||||
preferenceManager.setSharedPreferencesMode(0);
|
preferenceManager.setSharedPreferencesMode(0);
|
||||||
|
|
||||||
// Set up automatic preferences backup
|
// Set up automatic preferences backup
|
||||||
backupListener = apiFeatures.getApiAdapter().getBackupPreferencesListener(this);
|
backupListener = ApiAdapterFactory.getApiAdapter().getBackupPreferencesListener(this);
|
||||||
preferences = preferenceManager.getSharedPreferences();
|
preferences = preferenceManager.getSharedPreferences();
|
||||||
preferences.registerOnSharedPreferenceChangeListener(backupListener);
|
preferences.registerOnSharedPreferenceChangeListener(backupListener);
|
||||||
|
|
||||||
// Load the preferences to be displayed
|
// Load the preferences to be displayed
|
||||||
addPreferencesFromResource(R.xml.preferences);
|
addPreferencesFromResource(R.xml.preferences);
|
||||||
|
|
||||||
// Disable voice announcement if not available
|
|
||||||
if (!apiFeatures.hasTextToSpeech()) {
|
|
||||||
IntegerListPreference announcementFrequency =
|
|
||||||
(IntegerListPreference) findPreference(
|
|
||||||
getString(R.string.announcement_frequency_key));
|
|
||||||
announcementFrequency.setEnabled(false);
|
|
||||||
announcementFrequency.setValue(TASK_FREQUENCY_OFF);
|
|
||||||
announcementFrequency.setSummary(R.string.settings_recording_voice_not_available);
|
|
||||||
}
|
|
||||||
|
|
||||||
setRecordingIntervalOptions();
|
setRecordingIntervalOptions();
|
||||||
setAutoResumeTimeoutOptions();
|
setAutoResumeTimeoutOptions();
|
||||||
|
|
||||||
@@ -557,20 +545,17 @@ public class SettingsActivity extends PreferenceActivity {
|
|||||||
* Configures preference actions related to bluetooth.
|
* Configures preference actions related to bluetooth.
|
||||||
*/
|
*/
|
||||||
private void configureBluetoothPreferences() {
|
private void configureBluetoothPreferences() {
|
||||||
if (BluetoothDeviceUtils.isBluetoothMethodSupported()) {
|
// Populate the list of bluetooth devices
|
||||||
// Populate the list of bluetooth devices
|
populateBluetoothDeviceList();
|
||||||
populateBluetoothDeviceList();
|
// Make the pair devices preference go to the system preferences
|
||||||
|
findPreference(getString(R.string.bluetooth_pairing_key)).setOnPreferenceClickListener(
|
||||||
// Make the pair devices preference go to the system preferences
|
new OnPreferenceClickListener() {
|
||||||
findPreference(getString(R.string.bluetooth_pairing_key))
|
public boolean onPreferenceClick(Preference preference) {
|
||||||
.setOnPreferenceClickListener(new OnPreferenceClickListener() {
|
Intent settingsIntent = new Intent(Settings.ACTION_BLUETOOTH_SETTINGS);
|
||||||
public boolean onPreferenceClick(Preference preference) {
|
startActivity(settingsIntent);
|
||||||
Intent settingsIntent = new Intent(Settings.ACTION_BLUETOOTH_SETTINGS);
|
return false;
|
||||||
startActivity(settingsIntent);
|
}
|
||||||
return false;
|
});
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -582,7 +567,10 @@ public class SettingsActivity extends PreferenceActivity {
|
|||||||
List<String> entryValues = new ArrayList<String>();
|
List<String> entryValues = new ArrayList<String>();
|
||||||
|
|
||||||
// The actual devices
|
// The actual devices
|
||||||
BluetoothDeviceUtils.getInstance().populateDeviceLists(entries, entryValues);
|
BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
|
||||||
|
if (bluetoothAdapter != null) {
|
||||||
|
BluetoothDeviceUtils.populateDeviceLists(bluetoothAdapter, entries, entryValues);
|
||||||
|
}
|
||||||
|
|
||||||
CharSequence[] entriesArray = entries.toArray(new CharSequence[entries.size()]);
|
CharSequence[] entriesArray = entries.toArray(new CharSequence[entries.size()]);
|
||||||
CharSequence[] entryValuesArray = entryValues.toArray(new CharSequence[entryValues.size()]);
|
CharSequence[] entryValuesArray = entryValues.toArray(new CharSequence[entryValues.size()]);
|
||||||
@@ -685,7 +673,7 @@ public class SettingsActivity extends PreferenceActivity {
|
|||||||
SharedPreferences prefs = getPreferenceManager().getSharedPreferences();
|
SharedPreferences prefs = getPreferenceManager().getSharedPreferences();
|
||||||
Editor editor = prefs.edit();
|
Editor editor = prefs.edit();
|
||||||
editor.putString(getString(id), metricspeed);
|
editor.putString(getString(id), metricspeed);
|
||||||
ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges(editor);
|
ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -23,8 +23,6 @@ import com.google.android.apps.mytracks.content.TrackDataHub.ListenerDataType;
|
|||||||
import com.google.android.apps.mytracks.content.TrackDataListener;
|
import com.google.android.apps.mytracks.content.TrackDataListener;
|
||||||
import com.google.android.apps.mytracks.content.Waypoint;
|
import com.google.android.apps.mytracks.content.Waypoint;
|
||||||
import com.google.android.apps.mytracks.services.ServiceUtils;
|
import com.google.android.apps.mytracks.services.ServiceUtils;
|
||||||
import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerFactory;
|
|
||||||
import com.google.android.apps.mytracks.util.ApiFeatures;
|
|
||||||
import com.google.android.maps.mytracks.R;
|
import com.google.android.maps.mytracks.R;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
@@ -32,6 +30,7 @@ import android.content.Context;
|
|||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.location.Location;
|
import android.location.Location;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.speech.tts.TextToSpeech;
|
||||||
import android.util.DisplayMetrics;
|
import android.util.DisplayMetrics;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.Window;
|
import android.view.Window;
|
||||||
@@ -105,9 +104,7 @@ public class StatsActivity extends Activity implements TrackDataListener {
|
|||||||
utils = new StatsUtilities(this);
|
utils = new StatsUtilities(this);
|
||||||
|
|
||||||
// The volume we want to control is the Text-To-Speech volume
|
// The volume we want to control is the Text-To-Speech volume
|
||||||
int volumeStream =
|
setVolumeControlStream(TextToSpeech.Engine.DEFAULT_STREAM);
|
||||||
new StatusAnnouncerFactory(ApiFeatures.getInstance()).getVolumeStream();
|
|
||||||
setVolumeControlStream(volumeStream);
|
|
||||||
|
|
||||||
// We don't need a window title bar:
|
// We don't need a window title bar:
|
||||||
requestWindowFeature(Window.FEATURE_NO_TITLE);
|
requestWindowFeature(Window.FEATURE_NO_TITLE);
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import static com.google.android.apps.mytracks.Constants.TAG;
|
|||||||
|
|
||||||
import com.google.android.apps.mytracks.content.TracksColumns;
|
import com.google.android.apps.mytracks.content.TracksColumns;
|
||||||
import com.google.android.apps.mytracks.io.file.SaveActivity;
|
import com.google.android.apps.mytracks.io.file.SaveActivity;
|
||||||
import com.google.android.apps.mytracks.io.sendtogoogle.SendType;
|
import com.google.android.apps.mytracks.io.sendtogoogle.SendRequest;
|
||||||
import com.google.android.apps.mytracks.io.sendtogoogle.UploadServiceChooserActivity;
|
import com.google.android.apps.mytracks.io.sendtogoogle.UploadServiceChooserActivity;
|
||||||
import com.google.android.apps.mytracks.services.ServiceUtils;
|
import com.google.android.apps.mytracks.services.ServiceUtils;
|
||||||
import com.google.android.apps.mytracks.services.TrackRecordingServiceConnection;
|
import com.google.android.apps.mytracks.services.TrackRecordingServiceConnection;
|
||||||
@@ -36,7 +36,6 @@ import android.database.Cursor;
|
|||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.Parcelable;
|
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.ContextMenu;
|
import android.view.ContextMenu;
|
||||||
import android.view.ContextMenu.ContextMenuInfo;
|
import android.view.ContextMenu.ContextMenuInfo;
|
||||||
@@ -175,21 +174,18 @@ public class TrackList extends ListActivity
|
|||||||
case Constants.MENU_WRITE_TO_SD_CARD:
|
case Constants.MENU_WRITE_TO_SD_CARD:
|
||||||
return false;
|
return false;
|
||||||
case Constants.MENU_SEND_TO_GOOGLE:
|
case Constants.MENU_SEND_TO_GOOGLE:
|
||||||
intent = new Intent(this, UploadServiceChooserActivity.class);
|
intent = new Intent(this, UploadServiceChooserActivity.class)
|
||||||
intent.putExtra(UploadServiceChooserActivity.TRACK_ID, trackId);
|
.putExtra(SendRequest.SEND_REQUEST_KEY, new SendRequest(trackId, true, true, true));
|
||||||
startActivity(intent);
|
startActivity(intent);
|
||||||
return true;
|
return true;
|
||||||
case Constants.MENU_SHARE_MAP:
|
case Constants.MENU_SHARE_MAP:
|
||||||
intent = new Intent(this, UploadServiceChooserActivity.class);
|
intent = new Intent(this, UploadServiceChooserActivity.class)
|
||||||
intent.putExtra(UploadServiceChooserActivity.TRACK_ID, trackId);
|
.putExtra(SendRequest.SEND_REQUEST_KEY, new SendRequest(trackId, true, false, false));
|
||||||
intent.putExtra(UploadServiceChooserActivity.SEND_TYPE, (Parcelable) SendType.MAPS);
|
|
||||||
startActivity(intent);
|
startActivity(intent);
|
||||||
return true;
|
return true;
|
||||||
case Constants.MENU_SHARE_FUSION_TABLE:
|
case Constants.MENU_SHARE_FUSION_TABLE:
|
||||||
intent = new Intent(this, UploadServiceChooserActivity.class);
|
intent = new Intent(this, UploadServiceChooserActivity.class)
|
||||||
intent.putExtra(UploadServiceChooserActivity.TRACK_ID, trackId);
|
.putExtra(SendRequest.SEND_REQUEST_KEY, new SendRequest(trackId, false, true, false));
|
||||||
intent.putExtra(
|
|
||||||
UploadServiceChooserActivity.SEND_TYPE, (Parcelable) SendType.FUSION_TABLES);
|
|
||||||
startActivity(intent);
|
startActivity(intent);
|
||||||
return true;
|
return true;
|
||||||
case Constants.MENU_SAVE_GPX_FILE:
|
case Constants.MENU_SAVE_GPX_FILE:
|
||||||
@@ -286,6 +282,17 @@ public class TrackList extends ListActivity
|
|||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onCreateOptionsMenu(Menu menu) {
|
||||||
|
getMenuInflater().inflate(R.menu.search_only, menu);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Callback from menu/search_only.xml */
|
||||||
|
public void onSearch(@SuppressWarnings("unused") MenuItem i) {
|
||||||
|
onSearchRequested();
|
||||||
|
}
|
||||||
|
|
||||||
private void updateButtonsEnabled() {
|
private void updateButtonsEnabled() {
|
||||||
View deleteAll = findViewById(R.id.tracklist_btn_delete_all);
|
View deleteAll = findViewById(R.id.tracklist_btn_delete_all);
|
||||||
View exportAll = findViewById(R.id.tracklist_btn_export_all);
|
View exportAll = findViewById(R.id.tracklist_btn_export_all);
|
||||||
|
|||||||
@@ -198,6 +198,17 @@ public class WaypointsList extends ListActivity
|
|||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onCreateOptionsMenu(Menu menu) {
|
||||||
|
getMenuInflater().inflate(R.menu.search_only, menu);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Callback from menu/search_only.xml */
|
||||||
|
public void onSearch(@SuppressWarnings("unused") MenuItem i) {
|
||||||
|
onSearchRequested();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onClick(View v) {
|
public void onClick(View v) {
|
||||||
WaypointCreationRequest request;
|
WaypointCreationRequest request;
|
||||||
@@ -266,10 +277,10 @@ public class WaypointsList extends ListActivity
|
|||||||
if (columnIndex == timeIdx) {
|
if (columnIndex == timeIdx) {
|
||||||
long time = cursor.getLong(timeIdx);
|
long time = cursor.getLong(timeIdx);
|
||||||
TextView textView = (TextView) view;
|
TextView textView = (TextView) view;
|
||||||
|
|
||||||
if (time == 0) {
|
if (time == 0) {
|
||||||
textView.setVisibility(View.GONE);
|
textView.setVisibility(View.GONE);
|
||||||
} else {
|
} else {
|
||||||
textView.setText(StringUtils.formatDateTime(WaypointsList.this, time));
|
textView.setText(StringUtils.formatDateTime(WaypointsList.this, time));
|
||||||
textView.setVisibility(View.VISIBLE);
|
textView.setVisibility(View.VISIBLE);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,6 @@
|
|||||||
package com.google.android.apps.mytracks.content;
|
package com.google.android.apps.mytracks.content;
|
||||||
|
|
||||||
import com.google.android.apps.mytracks.Constants;
|
import com.google.android.apps.mytracks.Constants;
|
||||||
import com.google.android.apps.mytracks.util.ApiFeatures;
|
|
||||||
import com.google.android.maps.mytracks.R;
|
import com.google.android.maps.mytracks.R;
|
||||||
|
|
||||||
import android.content.ContentProvider;
|
import android.content.ContentProvider;
|
||||||
@@ -417,12 +416,9 @@ public class MyTracksProvider extends ContentProvider {
|
|||||||
} else {
|
} else {
|
||||||
throw new IllegalArgumentException("Unknown URL " + url);
|
throw new IllegalArgumentException("Unknown URL " + url);
|
||||||
}
|
}
|
||||||
|
Log.i(Constants.TAG, "Build query: "
|
||||||
if (ApiFeatures.getInstance().canReuseSQLiteQueryBuilder()) {
|
+ qb.buildQuery(projection, selection, selectionArgs, null, null, sortOrder, null));
|
||||||
Log.i(Constants.TAG,
|
|
||||||
"Build query: " + qb.buildQuery(projection, selection, selectionArgs,
|
|
||||||
null, null, sortOrder, null));
|
|
||||||
}
|
|
||||||
Cursor c = qb.query(db, projection, selection, selectionArgs, null, null,
|
Cursor c = qb.query(db, projection, selection, selectionArgs, null, null,
|
||||||
sortOrder);
|
sortOrder);
|
||||||
c.setNotificationUri(getContext().getContentResolver(), url);
|
c.setNotificationUri(getContext().getContentResolver(), url);
|
||||||
|
|||||||
@@ -0,0 +1,405 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012 Google Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||||
|
* use this file except in compliance with the License. You may obtain a copy of
|
||||||
|
* the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations under
|
||||||
|
* the License.
|
||||||
|
*/
|
||||||
|
package com.google.android.apps.mytracks.content;
|
||||||
|
|
||||||
|
import com.google.android.apps.mytracks.stats.TripStatistics;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.location.Location;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.SortedSet;
|
||||||
|
import java.util.TreeSet;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Engine for searching for tracks and waypoints by text.
|
||||||
|
*
|
||||||
|
* @author Rodrigo Damazio
|
||||||
|
*/
|
||||||
|
public class SearchEngine {
|
||||||
|
|
||||||
|
/** WHERE query to get tracks by name. */
|
||||||
|
private static final String TRACK_SELECTION_QUERY =
|
||||||
|
TracksColumns.NAME + " LIKE ? OR " +
|
||||||
|
TracksColumns.DESCRIPTION + " LIKE ? OR " +
|
||||||
|
TracksColumns.CATEGORY + " LIKE ?";
|
||||||
|
|
||||||
|
/** WHERE query to get waypoints by name. */
|
||||||
|
private static final String WAYPOINT_SELECTION_QUERY =
|
||||||
|
WaypointsColumns.NAME + " LIKE ? OR " +
|
||||||
|
WaypointsColumns.DESCRIPTION + " LIKE ? OR " +
|
||||||
|
WaypointsColumns.CATEGORY + " LIKE ?";
|
||||||
|
|
||||||
|
/** Order of track results. */
|
||||||
|
private static final String TRACK_SELECTION_ORDER = TracksColumns._ID + " DESC LIMIT 1000";
|
||||||
|
|
||||||
|
/** Order of waypoint results. */
|
||||||
|
private static final String WAYPOINT_SELECTION_ORDER = WaypointsColumns._ID + " DESC";
|
||||||
|
|
||||||
|
/** How much we promote a match in the track category. */
|
||||||
|
private static final double TRACK_CATEGORY_PROMOTION = 2.0;
|
||||||
|
|
||||||
|
/** How much we promote a match in the track description. */
|
||||||
|
private static final double TRACK_DESCRIPTION_PROMOTION = 8.0;
|
||||||
|
|
||||||
|
/** How much we promote a match in the track name. */
|
||||||
|
private static final double TRACK_NAME_PROMOTION = 16.0;
|
||||||
|
|
||||||
|
/** How much we promote a waypoint result if it's in the currently-selected track. */
|
||||||
|
private static final double CURRENT_TRACK_WAYPOINT_PROMOTION = 2.0;
|
||||||
|
|
||||||
|
/** How much we promote a track result if it's the currently-selected track. */
|
||||||
|
private static final double CURRENT_TRACK_DEMOTION = 0.5;
|
||||||
|
|
||||||
|
/** Maximum number of waypoints which will be retrieved and scored. */
|
||||||
|
private static final int MAX_SCORED_WAYPOINTS = 100;
|
||||||
|
|
||||||
|
/** Oldest timestamp for which we rank based on time (2000-01-01 00:00:00.000) */
|
||||||
|
private static final long OLDEST_ALLOWED_TIMESTAMP = 946692000000L;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Description of a search query, along with all contextual data needed to execute it.
|
||||||
|
*/
|
||||||
|
public static class SearchQuery {
|
||||||
|
public SearchQuery(String textQuery, Location currentLocation, long currentTrackId,
|
||||||
|
long currentTimestamp) {
|
||||||
|
this.textQuery = textQuery.toLowerCase();
|
||||||
|
this.currentLocation = currentLocation;
|
||||||
|
this.currentTrackId = currentTrackId;
|
||||||
|
this.currentTimestamp = currentTimestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public final String textQuery;
|
||||||
|
public final Location currentLocation;
|
||||||
|
public final long currentTrackId;
|
||||||
|
public final long currentTimestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Description of a search result which has been retrieved and scored.
|
||||||
|
*/
|
||||||
|
public static class ScoredResult {
|
||||||
|
ScoredResult(Track track, double score) {
|
||||||
|
this.track = track;
|
||||||
|
this.waypoint = null;
|
||||||
|
this.score = score;
|
||||||
|
}
|
||||||
|
|
||||||
|
ScoredResult(Waypoint waypoint, double score) {
|
||||||
|
this.track = null;
|
||||||
|
this.waypoint = waypoint;
|
||||||
|
this.score = score;
|
||||||
|
}
|
||||||
|
|
||||||
|
public final Track track;
|
||||||
|
public final Waypoint waypoint;
|
||||||
|
public final double score;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "ScoredResult ["
|
||||||
|
+ (track != null ? ("trackId=" + track.getId() + ", ") : "")
|
||||||
|
+ (waypoint != null ? ("wptId=" + waypoint.getId() + ", ") : "")
|
||||||
|
+ "score=" + score + "]";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Comparador for scored results. */
|
||||||
|
private static final Comparator<ScoredResult> SCORED_RESULT_COMPARATOR =
|
||||||
|
new Comparator<ScoredResult>() {
|
||||||
|
@Override
|
||||||
|
public int compare(ScoredResult r1, ScoredResult r2) {
|
||||||
|
// Score ordering.
|
||||||
|
int scoreDiff = Double.compare(r2.score, r1.score);
|
||||||
|
if (scoreDiff != 0) {
|
||||||
|
return scoreDiff;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make tracks come before waypoints.
|
||||||
|
if (r1.waypoint != null && r2.track != null) {
|
||||||
|
return 1;
|
||||||
|
} else if (r1.track != null && r2.waypoint != null) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally, use arbitrary ordering, by ID.
|
||||||
|
long id1 = r1.track != null ? r1.track.getId() : r1.waypoint.getId();
|
||||||
|
long id2 = r2.track != null ? r2.track.getId() : r2.waypoint.getId();
|
||||||
|
long idDiff = id2 - id1;
|
||||||
|
return Long.signum(idDiff);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private final MyTracksProviderUtils providerUtils;
|
||||||
|
|
||||||
|
public SearchEngine(Context ctx) {
|
||||||
|
providerUtils = MyTracksProviderUtils.Factory.get(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SearchEngine(MyTracksProviderUtils providerUtils) {
|
||||||
|
this.providerUtils = providerUtils;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes a search query and returns a set of sorted results.
|
||||||
|
*
|
||||||
|
* @param query the query to execute
|
||||||
|
* @return a set of results, sorted according to their score
|
||||||
|
*/
|
||||||
|
public SortedSet<ScoredResult> search(SearchQuery query) {
|
||||||
|
ArrayList<Track> tracks = new ArrayList<Track>();
|
||||||
|
ArrayList<Waypoint> waypoints = new ArrayList<Waypoint>();
|
||||||
|
TreeSet<ScoredResult> scoredResults = new TreeSet<ScoredResult>(SCORED_RESULT_COMPARATOR);
|
||||||
|
|
||||||
|
retrieveTracks(query, tracks);
|
||||||
|
retrieveWaypoints(query, waypoints);
|
||||||
|
|
||||||
|
scoreTrackResults(tracks, query, scoredResults);
|
||||||
|
scoreWaypointResults(waypoints, query, scoredResults);
|
||||||
|
|
||||||
|
return scoredResults;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves tracks matching the given query from the database.
|
||||||
|
*
|
||||||
|
* @param query the query to retrieve for
|
||||||
|
* @param tracks list to fill with the resulting tracks
|
||||||
|
*/
|
||||||
|
private void retrieveTracks(SearchQuery query, ArrayList<Track> tracks) {
|
||||||
|
String queryLikeSelection = "%" + query.textQuery + "%";
|
||||||
|
String[] trackSelectionArgs = new String[] {
|
||||||
|
queryLikeSelection,
|
||||||
|
queryLikeSelection,
|
||||||
|
queryLikeSelection };
|
||||||
|
|
||||||
|
Cursor tracksCursor = providerUtils.getTracksCursor(
|
||||||
|
TRACK_SELECTION_QUERY, trackSelectionArgs, TRACK_SELECTION_ORDER);
|
||||||
|
if (tracksCursor != null) {
|
||||||
|
try {
|
||||||
|
tracks.ensureCapacity(tracksCursor.getCount());
|
||||||
|
|
||||||
|
while (tracksCursor.moveToNext()) {
|
||||||
|
tracks.add(providerUtils.createTrack(tracksCursor));
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
tracksCursor.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves waypoints matching the given query from the database.
|
||||||
|
*
|
||||||
|
* @param query the query to retrieve for
|
||||||
|
* @param waypoints list to fill with the resulting waypoints
|
||||||
|
*/
|
||||||
|
private void retrieveWaypoints(SearchQuery query, ArrayList<Waypoint> waypoints) {
|
||||||
|
String queryLikeSelection2 = "%" + query.textQuery + "%";
|
||||||
|
String[] waypointSelectionArgs = new String[] {
|
||||||
|
queryLikeSelection2,
|
||||||
|
queryLikeSelection2,
|
||||||
|
queryLikeSelection2 };
|
||||||
|
Cursor waypointsCursor = providerUtils.getWaypointsCursor(
|
||||||
|
WAYPOINT_SELECTION_QUERY, waypointSelectionArgs, WAYPOINT_SELECTION_ORDER,
|
||||||
|
MAX_SCORED_WAYPOINTS);
|
||||||
|
if (waypointsCursor != null) {
|
||||||
|
try {
|
||||||
|
waypoints.ensureCapacity(waypointsCursor.getCount());
|
||||||
|
|
||||||
|
while (waypointsCursor.moveToNext()) {
|
||||||
|
waypoints.add(providerUtils.createWaypoint(waypointsCursor));
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
waypointsCursor.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scores a collection of track results.
|
||||||
|
*
|
||||||
|
* @param tracks the results to score
|
||||||
|
* @param query the query to score for
|
||||||
|
* @param output the collection to fill with scored results
|
||||||
|
*/
|
||||||
|
private void scoreTrackResults(Collection<Track> tracks, SearchQuery query, Collection<ScoredResult> output) {
|
||||||
|
for (Track track : tracks) {
|
||||||
|
// Calculate the score.
|
||||||
|
double score = scoreTrackResult(query, track);
|
||||||
|
|
||||||
|
// Add to the output.
|
||||||
|
output.add(new ScoredResult(track, score));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scores a single track result.
|
||||||
|
*
|
||||||
|
* @param query the query to score for
|
||||||
|
* @param track the results to score
|
||||||
|
* @return the score for the track
|
||||||
|
*/
|
||||||
|
private double scoreTrackResult(SearchQuery query, Track track) {
|
||||||
|
double score = 1.0;
|
||||||
|
|
||||||
|
score *= getTitleBoost(query, track.getName(), track.getDescription(), track.getCategory());
|
||||||
|
|
||||||
|
TripStatistics statistics = track.getStatistics();
|
||||||
|
// TODO: Also boost for proximity to the currently-centered position on the map.
|
||||||
|
score *= getDistanceBoost(query, statistics.getMeanLatitude(), statistics.getMeanLongitude());
|
||||||
|
|
||||||
|
long meanTimestamp = (statistics.getStartTime() + statistics.getStopTime()) / 2L;
|
||||||
|
score *= getTimeBoost(query, meanTimestamp);
|
||||||
|
|
||||||
|
// Score the currently-selected track lower (user is already there, wouldn't be searching for it).
|
||||||
|
if (track.getId() == query.currentTrackId) {
|
||||||
|
score *= CURRENT_TRACK_DEMOTION;
|
||||||
|
}
|
||||||
|
|
||||||
|
return score;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scores a collection of waypoint results.
|
||||||
|
*
|
||||||
|
* @param waypoints the results to score
|
||||||
|
* @param query the query to score for
|
||||||
|
* @param output the collection to fill with scored results
|
||||||
|
*/
|
||||||
|
private void scoreWaypointResults(Collection<Waypoint> waypoints, SearchQuery query, Collection<ScoredResult> output) {
|
||||||
|
for (Waypoint waypoint : waypoints) {
|
||||||
|
// Calculate the score.
|
||||||
|
double score = scoreWaypointResult(query, waypoint);
|
||||||
|
|
||||||
|
// Add to the output.
|
||||||
|
output.add(new ScoredResult(waypoint, score));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scores a single waypoint result.
|
||||||
|
*
|
||||||
|
* @param query the query to score for
|
||||||
|
* @param waypoint the results to score
|
||||||
|
* @return the score for the waypoint
|
||||||
|
*/
|
||||||
|
private double scoreWaypointResult(SearchQuery query, Waypoint waypoint) {
|
||||||
|
double score = 1.0;
|
||||||
|
|
||||||
|
Location location = waypoint.getLocation();
|
||||||
|
score *= getTitleBoost(query, waypoint.getName(), waypoint.getDescription(), waypoint.getCategory());
|
||||||
|
// TODO: Also boost for proximity to the currently-centered position on the map.
|
||||||
|
score *= getDistanceBoost(query, location.getLatitude(), location.getLongitude());
|
||||||
|
score *= getTimeBoost(query, location.getTime());
|
||||||
|
|
||||||
|
// Score waypoints in the currently-selected track higher (searching inside the current track).
|
||||||
|
if (query.currentTrackId != -1 && waypoint.getTrackId() == query.currentTrackId) {
|
||||||
|
score *= CURRENT_TRACK_WAYPOINT_PROMOTION;
|
||||||
|
}
|
||||||
|
|
||||||
|
return score;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the boosting of the score due to the field(s) in which the match occured.
|
||||||
|
*
|
||||||
|
* @param query the query to boost for
|
||||||
|
* @param name the name of the track or waypoint
|
||||||
|
* @param description the description of the track or waypoint
|
||||||
|
* @param category the category of the track or waypoint
|
||||||
|
* @return the total boost to be applied to the result
|
||||||
|
*/
|
||||||
|
private double getTitleBoost(SearchQuery query,
|
||||||
|
String name, String description, String category) {
|
||||||
|
// Title boost: track name > description > category.
|
||||||
|
double boost = 1.0;
|
||||||
|
if (name.toLowerCase().contains(query.textQuery)) {
|
||||||
|
boost *= TRACK_NAME_PROMOTION;
|
||||||
|
}
|
||||||
|
if (description.toLowerCase().contains(query.textQuery)) {
|
||||||
|
boost *= TRACK_DESCRIPTION_PROMOTION;
|
||||||
|
}
|
||||||
|
if (category.toLowerCase().contains(query.textQuery)) {
|
||||||
|
boost *= TRACK_CATEGORY_PROMOTION;
|
||||||
|
}
|
||||||
|
return boost;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the boosting of the score due to the recency of the matched entity.
|
||||||
|
*
|
||||||
|
* @param query the query to boost for
|
||||||
|
* @param timestamp the timestamp to calculate the boost for
|
||||||
|
* @return the total boost to be applied to the result
|
||||||
|
*/
|
||||||
|
private double getTimeBoost(SearchQuery query, long timestamp) {
|
||||||
|
if (timestamp < OLDEST_ALLOWED_TIMESTAMP) {
|
||||||
|
// Safety: if timestamp is too old or invalid, don't rank based on time.
|
||||||
|
return 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Score recent tracks higher.
|
||||||
|
long timeAgoHours = (query.currentTimestamp - timestamp) / (60L * 60L * 1000L);
|
||||||
|
if (timeAgoHours > 0L) {
|
||||||
|
return squash(timeAgoHours);
|
||||||
|
} else {
|
||||||
|
// Should rarely happen (track recorded in the last hour).
|
||||||
|
return Double.POSITIVE_INFINITY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the boosting of the score due to proximity to a location.
|
||||||
|
*
|
||||||
|
* @param query the query to boost for
|
||||||
|
* @param latitude the latitude to calculate the boost for
|
||||||
|
* @param longitude the longitude to calculate the boost for
|
||||||
|
* @return the total boost to be applied to the result
|
||||||
|
*/
|
||||||
|
private double getDistanceBoost(SearchQuery query, double latitude, double longitude) {
|
||||||
|
if (query.currentLocation == null) {
|
||||||
|
return 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
float[] distanceResults = new float[1];
|
||||||
|
|
||||||
|
Location.distanceBetween(
|
||||||
|
latitude, longitude,
|
||||||
|
query.currentLocation.getLatitude(), query.currentLocation.getLongitude(),
|
||||||
|
distanceResults);
|
||||||
|
|
||||||
|
// Score tracks close to the current location higher.
|
||||||
|
double distanceKm = distanceResults[0] / 1000.0;
|
||||||
|
if (distanceKm > 0.0) {
|
||||||
|
// Use the inverse of the amortized distance.
|
||||||
|
return squash(distanceKm);
|
||||||
|
} else {
|
||||||
|
// Should rarely happen (distance is exactly 0).
|
||||||
|
return Double.POSITIVE_INFINITY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Squashes a number by calculating 1 / log (1 + x).
|
||||||
|
*/
|
||||||
|
private static double squash(double x) {
|
||||||
|
return 1.0 / Math.log1p(x);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012 Google Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||||
|
* use this file except in compliance with the License. You may obtain a copy of
|
||||||
|
* the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations under
|
||||||
|
* the License.
|
||||||
|
*/
|
||||||
|
package com.google.android.apps.mytracks.content;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.SearchRecentSuggestionsProvider;
|
||||||
|
import android.provider.SearchRecentSuggestions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Content provider for search suggestions.
|
||||||
|
*
|
||||||
|
* @author Rodrigo Damazio
|
||||||
|
*/
|
||||||
|
public class SearchEngineProvider extends SearchRecentSuggestionsProvider {
|
||||||
|
|
||||||
|
private static final String AUTHORITY = "com.google.android.maps.mytracks.search";
|
||||||
|
private static final int MODE = DATABASE_MODE_QUERIES;
|
||||||
|
|
||||||
|
public SearchEngineProvider() {
|
||||||
|
setupSuggestions(AUTHORITY, MODE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Also add suggestions from the database.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates and returns a helper for adding recent queries or clearing the recent query history.
|
||||||
|
*/
|
||||||
|
public static SearchRecentSuggestions newHelper(Context context) {
|
||||||
|
return new SearchRecentSuggestions(context, AUTHORITY, MODE);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,7 +28,7 @@ import com.google.android.apps.mytracks.content.MyTracksProviderUtils.DoubleBuff
|
|||||||
import com.google.android.apps.mytracks.content.MyTracksProviderUtils.LocationIterator;
|
import com.google.android.apps.mytracks.content.MyTracksProviderUtils.LocationIterator;
|
||||||
import com.google.android.apps.mytracks.content.TrackDataListener.ProviderState;
|
import com.google.android.apps.mytracks.content.TrackDataListener.ProviderState;
|
||||||
import com.google.android.apps.mytracks.content.TrackDataListeners.ListenerRegistration;
|
import com.google.android.apps.mytracks.content.TrackDataListeners.ListenerRegistration;
|
||||||
import com.google.android.apps.mytracks.util.ApiFeatures;
|
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
|
||||||
import com.google.android.apps.mytracks.util.LocationUtils;
|
import com.google.android.apps.mytracks.util.LocationUtils;
|
||||||
import com.google.android.maps.mytracks.R;
|
import com.google.android.maps.mytracks.R;
|
||||||
|
|
||||||
@@ -196,7 +196,7 @@ public class TrackDataHub {
|
|||||||
private DoubleBufferedLocationFactory locationFactory;
|
private DoubleBufferedLocationFactory locationFactory;
|
||||||
|
|
||||||
private boolean started = false;
|
private boolean started = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds a new {@link TrackDataHub} instance.
|
* Builds a new {@link TrackDataHub} instance.
|
||||||
*/
|
*/
|
||||||
@@ -274,7 +274,7 @@ public class TrackDataHub {
|
|||||||
Log.w(TAG, "Not started, ignoring");
|
Log.w(TAG, "Not started, ignoring");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unregister internal listeners even if there are external listeners registered.
|
// Unregister internal listeners even if there are external listeners registered.
|
||||||
dataSourceManager.unregisterAllListeners();
|
dataSourceManager.unregisterAllListeners();
|
||||||
listenerHandlerThread.getLooper().quit();
|
listenerHandlerThread.getLooper().quit();
|
||||||
@@ -399,7 +399,7 @@ public class TrackDataHub {
|
|||||||
|
|
||||||
// Save the selection to memory and flush.
|
// Save the selection to memory and flush.
|
||||||
selectedTrackId = trackId;
|
selectedTrackId = trackId;
|
||||||
ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges(
|
ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(
|
||||||
preferences.edit().putLong(SELECTED_TRACK_KEY, trackId));
|
preferences.edit().putLong(SELECTED_TRACK_KEY, trackId));
|
||||||
|
|
||||||
// Force it to reload data from the beginning.
|
// Force it to reload data from the beginning.
|
||||||
|
|||||||
@@ -1,93 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2010 Google Inc.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
|
||||||
* use this file except in compliance with the License. You may obtain a copy of
|
|
||||||
* the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
* License for the specific language governing permissions and limitations under
|
|
||||||
* the License.
|
|
||||||
*/
|
|
||||||
package com.google.android.apps.mytracks.io;
|
|
||||||
|
|
||||||
import android.content.Intent;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This interface describes a class that will fetch and maintain a Google
|
|
||||||
* authentication token.
|
|
||||||
*
|
|
||||||
* @author Sandor Dornbush
|
|
||||||
*/
|
|
||||||
public interface AuthManager {
|
|
||||||
/**
|
|
||||||
* Callback for authentication token retrieval operations.
|
|
||||||
*/
|
|
||||||
public interface AuthCallback {
|
|
||||||
/**
|
|
||||||
* Indicates that we're done fetching an auth token.
|
|
||||||
*
|
|
||||||
* @param success if true, indicates we have the requested auth token available
|
|
||||||
* to be retrieved using {@link AuthManager#getAuthToken}
|
|
||||||
*/
|
|
||||||
void onAuthResult(boolean success);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the login process. The user should be asked to login if they
|
|
||||||
* haven't already. The {@link AuthCallback} provided will be executed when the
|
|
||||||
* auth token fetching is done (successfully or not).
|
|
||||||
*
|
|
||||||
* @param whenFinished A {@link AuthCallback} to execute when the auth token
|
|
||||||
* fetching is done
|
|
||||||
*/
|
|
||||||
void doLogin(AuthCallback whenFinished, Object o);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The {@link android.app.Activity} owner of this class should call this
|
|
||||||
* function when it gets {@link android.app.Activity#onActivityResult} with
|
|
||||||
* the request code passed into the constructor. The resultCode and results
|
|
||||||
* should come directly from the {@link android.app.Activity#onActivityResult}
|
|
||||||
* function. This function will return true if an auth token was successfully
|
|
||||||
* fetched or the process is not finished.
|
|
||||||
*
|
|
||||||
* @param resultCode The result code passed in to the
|
|
||||||
* {@link android.app.Activity}'s
|
|
||||||
* {@link android.app.Activity#onActivityResult} function
|
|
||||||
* @param results The data passed in to the {@link android.app.Activity}'s
|
|
||||||
* {@link android.app.Activity#onActivityResult} function
|
|
||||||
*/
|
|
||||||
void authResult(int resultCode, Intent results);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the current auth token. Response may be null if no valid auth
|
|
||||||
* token has been fetched.
|
|
||||||
*
|
|
||||||
* @return The current auth token or null if no auth token has been
|
|
||||||
* fetched
|
|
||||||
*/
|
|
||||||
String getAuthToken();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Invalidates the existing auth token and request a new one. The
|
|
||||||
* {@link Runnable} provided will be executed when the new auth token is
|
|
||||||
* successfully fetched.
|
|
||||||
*
|
|
||||||
* @param whenFinished A {@link Runnable} to execute when a new auth token
|
|
||||||
* is successfully fetched
|
|
||||||
*/
|
|
||||||
void invalidateAndRefresh(AuthCallback whenFinished);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an object that represents the given account, if possible.
|
|
||||||
*
|
|
||||||
* @param accountName the name of the account
|
|
||||||
* @param accountType the type of the account
|
|
||||||
* @return the account object
|
|
||||||
*/
|
|
||||||
Object getAccountObject(String accountName, String accountType);
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2010 Google Inc.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
|
||||||
* use this file except in compliance with the License. You may obtain a copy of
|
|
||||||
* the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
* License for the specific language governing permissions and limitations under
|
|
||||||
* the License.
|
|
||||||
*/
|
|
||||||
package com.google.android.apps.mytracks.io;
|
|
||||||
|
|
||||||
import static com.google.android.apps.mytracks.Constants.TAG;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A factory for getting the platform specific AuthManager.
|
|
||||||
*
|
|
||||||
* @author Sandor Dornbush
|
|
||||||
*/
|
|
||||||
public class AuthManagerFactory {
|
|
||||||
|
|
||||||
private AuthManagerFactory() {
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns whether the modern AuthManager should be used
|
|
||||||
*/
|
|
||||||
public static boolean useModernAuthManager() {
|
|
||||||
return Integer.parseInt(Build.VERSION.SDK) >= 7;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a right {@link AuthManager} for the platform.
|
|
||||||
* @return A new AuthManager
|
|
||||||
*/
|
|
||||||
public static AuthManager getAuthManager(Activity activity, int code,
|
|
||||||
Bundle extras, boolean requireGoogle, String service) {
|
|
||||||
if (useModernAuthManager()) {
|
|
||||||
Log.i(TAG, "Creating modern auth manager: " + service);
|
|
||||||
return new ModernAuthManager(activity, service);
|
|
||||||
} else {
|
|
||||||
Log.i(TAG, "Creating legacy auth manager: " + service);
|
|
||||||
return new AuthManagerOld(activity, code, extras, requireGoogle, service);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2009 Google Inc.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
|
||||||
* use this file except in compliance with the License. You may obtain a copy of
|
|
||||||
* the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
* License for the specific language governing permissions and limitations under
|
|
||||||
* the License.
|
|
||||||
*/
|
|
||||||
package com.google.android.apps.mytracks.io;
|
|
||||||
|
|
||||||
import com.google.android.googlelogindist.GoogleLoginServiceConstants;
|
|
||||||
import com.google.android.googlelogindist.GoogleLoginServiceHelper;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.os.Bundle;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* AuthManager keeps track of the current auth token for a user. The advantage
|
|
||||||
* over just passing around a String is that this class can renew the auth
|
|
||||||
* token if necessary, and it will change for all classes using this
|
|
||||||
* AuthManager.
|
|
||||||
*/
|
|
||||||
public class AuthManagerOld implements AuthManager {
|
|
||||||
/** The activity that will handle auth result callbacks. */
|
|
||||||
private final Activity activity;
|
|
||||||
|
|
||||||
/** The code used to tell the activity that it is an auth result. */
|
|
||||||
private final int code;
|
|
||||||
|
|
||||||
/** Extras to pass into the getCredentials function. */
|
|
||||||
private final Bundle extras;
|
|
||||||
|
|
||||||
/** True if the account must be a Google account (not a domain account). */
|
|
||||||
private final boolean requireGoogle;
|
|
||||||
|
|
||||||
/** The name of the service to authorize for. */
|
|
||||||
private final String service;
|
|
||||||
|
|
||||||
/** The handler to call when a new auth token is fetched. */
|
|
||||||
private AuthCallback authCallback;
|
|
||||||
|
|
||||||
/** The most recently fetched auth token or null if none is available. */
|
|
||||||
private String authToken;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* AuthManager requires many of the same parameters as
|
|
||||||
* {@link GoogleLoginServiceHelper#getCredentials(Activity, int, Bundle,
|
|
||||||
* boolean, String, boolean)}. The activity must have
|
|
||||||
* a handler in {@link Activity#onActivityResult} that calls
|
|
||||||
* {@link #authResult(int, Intent)} if the request code is the code given
|
|
||||||
* here.
|
|
||||||
*
|
|
||||||
* @param activity An activity with a handler in
|
|
||||||
* {@link Activity#onActivityResult} that calls
|
|
||||||
* {@link #authResult(int, Intent)} when {@literal code} is the request
|
|
||||||
* code
|
|
||||||
* @param code The request code to pass to
|
|
||||||
* {@link Activity#onActivityResult} when
|
|
||||||
* {@link #authResult(int, Intent)} should be called
|
|
||||||
* @param extras A {@link Bundle} of extras for
|
|
||||||
* {@link GoogleLoginServiceHelper}
|
|
||||||
* @param requireGoogle True if the account must be a Google account
|
|
||||||
* @param service The name of the service to authenticate as
|
|
||||||
*/
|
|
||||||
public AuthManagerOld(Activity activity, int code, Bundle extras,
|
|
||||||
boolean requireGoogle, String service) {
|
|
||||||
this.activity = activity;
|
|
||||||
this.code = code;
|
|
||||||
this.extras = extras;
|
|
||||||
this.requireGoogle = requireGoogle;
|
|
||||||
this.service = service;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void doLogin(AuthCallback callback, Object o) {
|
|
||||||
authCallback = callback;
|
|
||||||
activity.runOnUiThread(new LoginRunnable());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Runnable which actually gets login credentials.
|
|
||||||
*/
|
|
||||||
private class LoginRunnable implements Runnable {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
GoogleLoginServiceHelper.getCredentials(
|
|
||||||
activity, code, extras, requireGoogle, service, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void authResult(int resultCode, Intent results) {
|
|
||||||
if (resultCode != Activity.RESULT_OK) {
|
|
||||||
runAuthCallback(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
authToken = results.getStringExtra(GoogleLoginServiceConstants.AUTHTOKEN_KEY);
|
|
||||||
if (authToken == null) {
|
|
||||||
// Retry, without prompting the user.
|
|
||||||
GoogleLoginServiceHelper.getCredentials(
|
|
||||||
activity, code, extras, requireGoogle, service, false);
|
|
||||||
} else {
|
|
||||||
// Notify all active listeners that we have a new auth token.
|
|
||||||
runAuthCallback(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void runAuthCallback(boolean success) {
|
|
||||||
authCallback.onAuthResult(success);
|
|
||||||
authCallback = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getAuthToken() {
|
|
||||||
return authToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void invalidateAndRefresh(AuthCallback callback) {
|
|
||||||
authCallback = callback;
|
|
||||||
activity.runOnUiThread(new Runnable() {
|
|
||||||
public void run() {
|
|
||||||
GoogleLoginServiceHelper.invalidateAuthToken(activity, code, authToken);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Object getAccountObject(String accountName, String accountType) {
|
|
||||||
throw new UnsupportedOperationException("Legacy auth manager knows nothing about accounts");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,217 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2010 Google Inc.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
|
||||||
* use this file except in compliance with the License. You may obtain a copy of
|
|
||||||
* the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
* License for the specific language governing permissions and limitations under
|
|
||||||
* the License.
|
|
||||||
*/
|
|
||||||
package com.google.android.apps.mytracks.io;
|
|
||||||
|
|
||||||
import static com.google.android.apps.mytracks.Constants.ACCOUNT_TYPE;
|
|
||||||
import static com.google.android.apps.mytracks.Constants.TAG;
|
|
||||||
|
|
||||||
import com.google.android.apps.mytracks.AccountChooser;
|
|
||||||
|
|
||||||
import android.accounts.Account;
|
|
||||||
import android.accounts.AccountManager;
|
|
||||||
import android.accounts.AccountManagerCallback;
|
|
||||||
import android.accounts.AccountManagerFuture;
|
|
||||||
import android.accounts.AuthenticatorException;
|
|
||||||
import android.accounts.OperationCanceledException;
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* AuthManager keeps track of the current auth token for a user. The advantage
|
|
||||||
* over just passing around a String is that this class can renew the auth
|
|
||||||
* token if necessary, and it will change for all classes using this
|
|
||||||
* AuthManager.
|
|
||||||
*/
|
|
||||||
public class ModernAuthManager implements AuthManager {
|
|
||||||
/** The activity that will handle auth result callbacks. */
|
|
||||||
private final Activity activity;
|
|
||||||
|
|
||||||
/** The name of the service to authorize for. */
|
|
||||||
private final String service;
|
|
||||||
|
|
||||||
/** The most recently fetched auth token or null if none is available. */
|
|
||||||
private String authToken;
|
|
||||||
|
|
||||||
private final AccountManager accountManager;
|
|
||||||
|
|
||||||
private AuthCallback authCallback;
|
|
||||||
|
|
||||||
private Account lastAccount;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* AuthManager requires many of the same parameters as
|
|
||||||
* {@link com.google.android.googlelogindist.GoogleLoginServiceHelper
|
|
||||||
* #getCredentials(Activity, int, Bundle, boolean, String, boolean)}.
|
|
||||||
* The activity must have a handler in {@link Activity#onActivityResult} that
|
|
||||||
* calls {@link #authResult(int, Intent)} if the request code is the code
|
|
||||||
* given here.
|
|
||||||
*
|
|
||||||
* @param activity An activity with a handler in
|
|
||||||
* {@link Activity#onActivityResult} that calls
|
|
||||||
* {@link #authResult(int, Intent)} when {@literal code} is the request
|
|
||||||
* code
|
|
||||||
* @param service The name of the service to authenticate as
|
|
||||||
*/
|
|
||||||
public ModernAuthManager(Activity activity, String service) {
|
|
||||||
this.activity = activity;
|
|
||||||
this.service = service;
|
|
||||||
this.accountManager = AccountManager.get(activity);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Call this to do the initial login. The user will be asked to login if
|
|
||||||
* they haven't already. The {@link Runnable} provided will be executed
|
|
||||||
* when the auth token is successfully fetched.
|
|
||||||
*
|
|
||||||
* @param runnable A {@link Runnable} to execute when the auth token
|
|
||||||
* has been successfully fetched and is available via
|
|
||||||
* {@link #getAuthToken()}
|
|
||||||
*/
|
|
||||||
public void doLogin(AuthCallback runnable, Object o) {
|
|
||||||
this.authCallback = runnable;
|
|
||||||
if (!(o instanceof Account)) {
|
|
||||||
throw new IllegalArgumentException("ModernAuthManager requires an account.");
|
|
||||||
}
|
|
||||||
Account account = (Account) o;
|
|
||||||
doLogin(account);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void doLogin(Account account) {
|
|
||||||
// Keep the account in case we need to retry.
|
|
||||||
this.lastAccount = account;
|
|
||||||
|
|
||||||
// NOTE: Many Samsung phones have a crashing bug in
|
|
||||||
// AccountManager#getAuthToken(Account, String, boolean, AccountManagerCallback<Bundle>)
|
|
||||||
// so we use the other version of the method.
|
|
||||||
// More details here:
|
|
||||||
// http://forum.xda-developers.com/showthread.php?p=15155487
|
|
||||||
// http://android.git.kernel.org/?p=platform/frameworks/base.git;a=blob;f=core/java/android/accounts/AccountManagerService.java
|
|
||||||
accountManager.getAuthToken(account, service, null, activity,
|
|
||||||
new AccountManagerCallback<Bundle>() {
|
|
||||||
public void run(AccountManagerFuture<Bundle> future) {
|
|
||||||
try {
|
|
||||||
authToken = future.getResult().getString(
|
|
||||||
AccountManager.KEY_AUTHTOKEN);
|
|
||||||
Log.i(TAG, "Got auth token");
|
|
||||||
} catch (OperationCanceledException e) {
|
|
||||||
Log.e(TAG, "Auth token operation Canceled", e);
|
|
||||||
} catch (IOException e) {
|
|
||||||
Log.e(TAG, "Auth token IO exception", e);
|
|
||||||
} catch (AuthenticatorException e) {
|
|
||||||
Log.e(TAG, "Authentication Failed", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
runAuthCallback();
|
|
||||||
}
|
|
||||||
}, null /* handler */);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The {@link Activity} passed into the constructor should call this
|
|
||||||
* function when it gets {@link Activity#onActivityResult} with the request
|
|
||||||
* code passed into the constructor. The resultCode and results should
|
|
||||||
* come directly from the {@link Activity#onActivityResult} function. This
|
|
||||||
* function will return true if an auth token was successfully fetched or
|
|
||||||
* the process is not finished.
|
|
||||||
*
|
|
||||||
* @param resultCode The result code passed in to the {@link Activity}'s
|
|
||||||
* {@link Activity#onActivityResult} function
|
|
||||||
* @param results The data passed in to the {@link Activity}'s
|
|
||||||
* {@link Activity#onActivityResult} function
|
|
||||||
*/
|
|
||||||
public void authResult(int resultCode, Intent results) {
|
|
||||||
boolean retry = false;
|
|
||||||
if (results == null) {
|
|
||||||
Log.e(TAG, "No auth token!!");
|
|
||||||
} else {
|
|
||||||
authToken = results.getStringExtra(AccountManager.KEY_AUTHTOKEN);
|
|
||||||
retry = results.getBooleanExtra("retry", false);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (authToken == null && retry) {
|
|
||||||
Log.i(TAG, "Retrying to get auth result");
|
|
||||||
doLogin(lastAccount);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
runAuthCallback();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the current auth token. Response may be null if no valid auth
|
|
||||||
* token has been fetched.
|
|
||||||
*
|
|
||||||
* @return The current auth token or null if no auth token has been
|
|
||||||
* fetched
|
|
||||||
*/
|
|
||||||
public String getAuthToken() {
|
|
||||||
return authToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Invalidates the existing auth token and request a new one. The callback
|
|
||||||
* provided will be executed when the new auth token is successfully fetched.
|
|
||||||
*
|
|
||||||
* @param callback A callback to execute when a new auth token is successfully
|
|
||||||
* fetched
|
|
||||||
*/
|
|
||||||
public void invalidateAndRefresh(final AuthCallback callback) {
|
|
||||||
this.authCallback = callback;
|
|
||||||
|
|
||||||
activity.runOnUiThread(new Runnable() {
|
|
||||||
public void run() {
|
|
||||||
accountManager.invalidateAuthToken(ACCOUNT_TYPE, authToken);
|
|
||||||
authToken = null;
|
|
||||||
|
|
||||||
AccountChooser accountChooser = new AccountChooser();
|
|
||||||
accountChooser.chooseAccount(activity,
|
|
||||||
new AccountChooser.AccountHandler() {
|
|
||||||
@Override
|
|
||||||
public void onAccountSelected(Account account) {
|
|
||||||
if (account != null) {
|
|
||||||
doLogin(account);
|
|
||||||
} else {
|
|
||||||
runAuthCallback();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void runAuthCallback() {
|
|
||||||
lastAccount = null;
|
|
||||||
|
|
||||||
if (authCallback != null) {
|
|
||||||
(new Thread() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
authCallback.onAuthResult(authToken != null);
|
|
||||||
authCallback = null;
|
|
||||||
}
|
|
||||||
}).start();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Object getAccountObject(String accountName, String accountType) {
|
|
||||||
return new Account(accountName, accountType);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,280 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2008 Google Inc.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
|
||||||
* use this file except in compliance with the License. You may obtain a copy of
|
|
||||||
* the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
* License for the specific language governing permissions and limitations under
|
|
||||||
* the License.
|
|
||||||
*/
|
|
||||||
package com.google.android.apps.mytracks.io;
|
|
||||||
|
|
||||||
import com.google.android.apps.mytracks.Constants;
|
|
||||||
import com.google.android.apps.mytracks.ProgressIndicator;
|
|
||||||
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
|
|
||||||
import com.google.android.apps.mytracks.content.Track;
|
|
||||||
import com.google.android.apps.mytracks.io.docs.DocsHelper;
|
|
||||||
import com.google.android.apps.mytracks.io.gdata.GDataClientFactory;
|
|
||||||
import com.google.android.apps.mytracks.io.gdata.GDataWrapper;
|
|
||||||
import com.google.android.common.gdata.AndroidXmlParserFactory;
|
|
||||||
import com.google.android.maps.mytracks.R;
|
|
||||||
import com.google.wireless.gdata.client.GDataClient;
|
|
||||||
import com.google.wireless.gdata.client.GDataServiceClient;
|
|
||||||
import com.google.wireless.gdata.docs.DocumentsClient;
|
|
||||||
import com.google.wireless.gdata.docs.SpreadsheetsClient;
|
|
||||||
import com.google.wireless.gdata.docs.XmlDocsGDataParserFactory;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A helper class used to transmit tracks statistics to Google Docs/Trix.
|
|
||||||
*
|
|
||||||
* @author Sandor Dornbush
|
|
||||||
*/
|
|
||||||
public class SendToDocs {
|
|
||||||
/** The GData service name for Google Spreadsheets (aka Trix) */
|
|
||||||
public static final String GDATA_SERVICE_NAME_TRIX = "wise";
|
|
||||||
|
|
||||||
/** The GData service name for the Google Docs Document List */
|
|
||||||
public static final String GDATA_SERVICE_NAME_DOCLIST = "writely";
|
|
||||||
|
|
||||||
private final Activity activity;
|
|
||||||
private final AuthManager trixAuth;
|
|
||||||
private final AuthManager docListAuth;
|
|
||||||
private final ProgressIndicator progressIndicator;
|
|
||||||
private final boolean metricUnits;
|
|
||||||
|
|
||||||
private boolean success = true;
|
|
||||||
private Runnable onCompletion = null;
|
|
||||||
|
|
||||||
|
|
||||||
public SendToDocs(Activity activity, AuthManager trixAuth,
|
|
||||||
AuthManager docListAuth, ProgressIndicator progressIndicator) {
|
|
||||||
this.activity = activity;
|
|
||||||
this.trixAuth = trixAuth;
|
|
||||||
this.docListAuth = docListAuth;
|
|
||||||
this.progressIndicator = progressIndicator;
|
|
||||||
|
|
||||||
SharedPreferences preferences = activity.getSharedPreferences(
|
|
||||||
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
|
|
||||||
if (preferences != null) {
|
|
||||||
metricUnits =
|
|
||||||
preferences.getBoolean(activity.getString(R.string.metric_units_key),
|
|
||||||
true);
|
|
||||||
} else {
|
|
||||||
metricUnits = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void sendToDocs(final long trackId) {
|
|
||||||
Log.d(Constants.TAG,
|
|
||||||
"Sending to Google Docs: trackId = " + trackId);
|
|
||||||
new Thread("SendToGoogleDocs") {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
doUpload(trackId);
|
|
||||||
}
|
|
||||||
}.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void doUpload(long trackId) {
|
|
||||||
success = false;
|
|
||||||
try {
|
|
||||||
if (trackId == -1) {
|
|
||||||
Log.w(Constants.TAG, "Cannot get track id.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the track from the provider:
|
|
||||||
Track track =
|
|
||||||
MyTracksProviderUtils.Factory.get(activity).getTrack(trackId);
|
|
||||||
if (track == null) {
|
|
||||||
Log.w(Constants.TAG, "Cannot get track.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Transmit track stats via GData feed:
|
|
||||||
// -------------------------------
|
|
||||||
|
|
||||||
Log.d(Constants.TAG, "SendToDocs: Uploading to spreadsheet");
|
|
||||||
success = uploadToDocs(track);
|
|
||||||
Log.d(Constants.TAG, "SendToDocs: Done.");
|
|
||||||
} finally {
|
|
||||||
if (onCompletion != null) {
|
|
||||||
activity.runOnUiThread(onCompletion);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean wasSuccess() {
|
|
||||||
return success;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setOnCompletion(Runnable onCompletion) {
|
|
||||||
this.onCompletion = onCompletion;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Uploads the statistics about a track to Google Docs using the docs GData
|
|
||||||
* feed.
|
|
||||||
*
|
|
||||||
* @param track the track
|
|
||||||
*/
|
|
||||||
private boolean uploadToDocs(Track track) {
|
|
||||||
GDataWrapper<GDataServiceClient> docListWrapper = new GDataWrapper<GDataServiceClient>();
|
|
||||||
docListWrapper.setAuthManager(docListAuth);
|
|
||||||
docListWrapper.setRetryOnAuthFailure(true);
|
|
||||||
|
|
||||||
GDataWrapper<GDataServiceClient> trixWrapper = new GDataWrapper<GDataServiceClient>();
|
|
||||||
trixWrapper.setAuthManager(trixAuth);
|
|
||||||
trixWrapper.setRetryOnAuthFailure(true);
|
|
||||||
|
|
||||||
DocsHelper docsHelper = new DocsHelper();
|
|
||||||
|
|
||||||
GDataClient androidClient = null;
|
|
||||||
try {
|
|
||||||
androidClient = GDataClientFactory.getGDataClient(activity);
|
|
||||||
SpreadsheetsClient gdataClient = new SpreadsheetsClient(androidClient,
|
|
||||||
new XmlDocsGDataParserFactory(new AndroidXmlParserFactory()));
|
|
||||||
trixWrapper.setClient(gdataClient);
|
|
||||||
Log.d(Constants.TAG,
|
|
||||||
"GData connection prepared: " + this.docListAuth);
|
|
||||||
String sheetTitle = "My Tracks";
|
|
||||||
|
|
||||||
if (track.getCategory() != null && !track.getCategory().equals("")) {
|
|
||||||
sheetTitle += "-" + track.getCategory();
|
|
||||||
}
|
|
||||||
|
|
||||||
DocumentsClient docsGdataClient = new DocumentsClient(androidClient,
|
|
||||||
new XmlDocsGDataParserFactory(new AndroidXmlParserFactory()));
|
|
||||||
docListWrapper.setClient(docsGdataClient);
|
|
||||||
|
|
||||||
// First try to find the spreadsheet:
|
|
||||||
String spreadsheetId = null;
|
|
||||||
try {
|
|
||||||
spreadsheetId = docsHelper.requestSpreadsheetId(docListWrapper,
|
|
||||||
sheetTitle);
|
|
||||||
} catch (IOException e) {
|
|
||||||
Log.i(Constants.TAG, "Spreadsheet lookup failed.", e);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (spreadsheetId == null) {
|
|
||||||
progressIndicator.setProgressValue(65);
|
|
||||||
// Waiting a few seconds and trying again. Maybe the server just had a
|
|
||||||
// hickup (unfortunately that happens quite a lot...).
|
|
||||||
try {
|
|
||||||
Thread.sleep(5000);
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
Log.e(Constants.TAG, "Sleep interrupted", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
spreadsheetId = docsHelper.requestSpreadsheetId(docListWrapper,
|
|
||||||
sheetTitle);
|
|
||||||
} catch (IOException e) {
|
|
||||||
Log.i(Constants.TAG, "2nd spreadsheet lookup failed.", e);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// We were unable to find an existing spreadsheet, so create a new one.
|
|
||||||
progressIndicator.setProgressValue(70);
|
|
||||||
if (spreadsheetId == null) {
|
|
||||||
Log.i(Constants.TAG, "Creating new spreadsheet: " + sheetTitle);
|
|
||||||
|
|
||||||
try {
|
|
||||||
spreadsheetId = docsHelper.createSpreadsheet(activity, docListWrapper,
|
|
||||||
sheetTitle);
|
|
||||||
} catch (IOException e) {
|
|
||||||
Log.i(Constants.TAG, "Failed to create new spreadsheet "
|
|
||||||
+ sheetTitle, e);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
progressIndicator.setProgressValue(80);
|
|
||||||
|
|
||||||
if (spreadsheetId == null) {
|
|
||||||
progressIndicator.setProgressValue(85);
|
|
||||||
// The previous creation might have succeeded even though GData
|
|
||||||
// reported an error. Seems to be a know bug,
|
|
||||||
// see http://code.google.com/p/gdata-issues/issues/detail?id=929
|
|
||||||
// Try to find the created spreadsheet:
|
|
||||||
Log.w(Constants.TAG,
|
|
||||||
"Create might have failed. Trying to find created document.");
|
|
||||||
try {
|
|
||||||
Thread.sleep(5000);
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
Log.e(Constants.TAG, "Sleep interrupted", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
spreadsheetId = docsHelper.requestSpreadsheetId(docListWrapper,
|
|
||||||
sheetTitle);
|
|
||||||
} catch (IOException e) {
|
|
||||||
Log.i(Constants.TAG, "Failed create-failed lookup", e);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (spreadsheetId == null) {
|
|
||||||
progressIndicator.setProgressValue(87);
|
|
||||||
// Re-try
|
|
||||||
try {
|
|
||||||
Thread.sleep(5000);
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
Log.e(Constants.TAG, "Sleep interrupted", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
spreadsheetId = docsHelper.requestSpreadsheetId(docListWrapper,
|
|
||||||
sheetTitle);
|
|
||||||
} catch (IOException e) {
|
|
||||||
Log.i(Constants.TAG, "Failed create-failed relookup", e);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (spreadsheetId == null) {
|
|
||||||
Log.i(Constants.TAG,
|
|
||||||
"Creating new spreadsheet really failed.");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String worksheetId = null;
|
|
||||||
try {
|
|
||||||
worksheetId = docsHelper.getWorksheetId(trixWrapper, spreadsheetId);
|
|
||||||
if (worksheetId == null) {
|
|
||||||
throw new IOException("Worksheet ID lookup returned empty");
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
Log.i(Constants.TAG, "Looking up worksheet id failed.", e);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
progressIndicator.setProgressValue(90);
|
|
||||||
|
|
||||||
docsHelper.addTrackRow(activity, trixAuth, spreadsheetId, worksheetId,
|
|
||||||
track, metricUnits);
|
|
||||||
Log.i(Constants.TAG, "Done uploading to docs.");
|
|
||||||
} catch (IOException e) {
|
|
||||||
Log.e(Constants.TAG, "Unable to upload docs.", e);
|
|
||||||
return false;
|
|
||||||
} finally {
|
|
||||||
if (androidClient != null) {
|
|
||||||
androidClient.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,716 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2010 Google Inc.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
|
||||||
* use this file except in compliance with the License. You may obtain a copy of
|
|
||||||
* the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
* License for the specific language governing permissions and limitations under
|
|
||||||
* the License.
|
|
||||||
*/
|
|
||||||
package com.google.android.apps.mytracks.io;
|
|
||||||
|
|
||||||
import static com.google.android.apps.mytracks.Constants.TAG;
|
|
||||||
|
|
||||||
import com.google.android.apps.mytracks.Constants;
|
|
||||||
import com.google.android.apps.mytracks.ProgressIndicator;
|
|
||||||
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
|
|
||||||
import com.google.android.apps.mytracks.content.Track;
|
|
||||||
import com.google.android.apps.mytracks.content.Waypoint;
|
|
||||||
import com.google.android.apps.mytracks.io.gdata.GDataWrapper;
|
|
||||||
import com.google.android.apps.mytracks.io.gdata.GDataWrapper.QueryFunction;
|
|
||||||
import com.google.android.apps.mytracks.io.sendtogoogle.SendType;
|
|
||||||
import com.google.android.apps.mytracks.stats.DoubleBuffer;
|
|
||||||
import com.google.android.apps.mytracks.stats.TripStatistics;
|
|
||||||
import com.google.android.apps.mytracks.util.ApiFeatures;
|
|
||||||
import com.google.android.apps.mytracks.util.LocationUtils;
|
|
||||||
import com.google.android.apps.mytracks.util.StringUtils;
|
|
||||||
import com.google.android.apps.mytracks.util.SystemUtils;
|
|
||||||
import com.google.android.apps.mytracks.util.UnitConversions;
|
|
||||||
import com.google.android.maps.mytracks.R;
|
|
||||||
import com.google.api.client.googleapis.GoogleHeaders;
|
|
||||||
import com.google.api.client.googleapis.MethodOverride;
|
|
||||||
import com.google.api.client.http.GenericUrl;
|
|
||||||
import com.google.api.client.http.HttpRequest;
|
|
||||||
import com.google.api.client.http.HttpRequestFactory;
|
|
||||||
import com.google.api.client.http.HttpResponse;
|
|
||||||
import com.google.api.client.http.HttpResponseException;
|
|
||||||
import com.google.api.client.http.HttpTransport;
|
|
||||||
import com.google.api.client.http.InputStreamContent;
|
|
||||||
import com.google.api.client.util.Strings;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.location.Location;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.net.URLEncoder;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.Vector;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A helper class used to transmit tracks to Google Fusion Tables.
|
|
||||||
* A new instance should be used for each upload.
|
|
||||||
*
|
|
||||||
* @author Leif Hendrik Wilden
|
|
||||||
*/
|
|
||||||
public class SendToFusionTables implements Runnable {
|
|
||||||
|
|
||||||
private static final String CONTENT_TYPE = "application/x-www-form-urlencoded";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Listener invoked when sending to fusion tables completes.
|
|
||||||
*/
|
|
||||||
public interface OnSendCompletedListener {
|
|
||||||
void onSendCompleted(String tableId, boolean success);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The GData service id for Fusion Tables. */
|
|
||||||
public static final String SERVICE_ID = "fusiontables";
|
|
||||||
|
|
||||||
/** The path for viewing a map visualization of a table. */
|
|
||||||
private static final String FUSIONTABLES_MAP =
|
|
||||||
"https://www.google.com/fusiontables/embedviz?" +
|
|
||||||
"viz=MAP&q=select+col0,+col1,+col2,+col3+from+%s+&h=false&" +
|
|
||||||
"lat=%f&lng=%f&z=%d&t=1&l=col2";
|
|
||||||
|
|
||||||
/** Standard base feed url for Fusion Tables. */
|
|
||||||
private static final String FUSIONTABLES_BASE_FEED_URL =
|
|
||||||
"https://www.google.com/fusiontables/api/query";
|
|
||||||
|
|
||||||
private static final int MAX_POINTS_PER_UPLOAD = 2048;
|
|
||||||
|
|
||||||
private static final String GDATA_VERSION = "2";
|
|
||||||
|
|
||||||
// This class reports upload status to the user as a completion percentage
|
|
||||||
// using a progress bar. Progress is defined as follows:
|
|
||||||
//
|
|
||||||
// 0% Getting track metadata
|
|
||||||
// 5% Creating Fusion Table (GData to FT server)
|
|
||||||
// 10%-90% Uploading the track data to Fusion Tables
|
|
||||||
// 95% Uploading waypoints
|
|
||||||
// 100% Done
|
|
||||||
private static final int PROGRESS_INITIALIZATION = 0;
|
|
||||||
private static final int PROGRESS_FUSION_TABLE_CREATE = 5;
|
|
||||||
private static final int PROGRESS_UPLOAD_DATA_MIN = 10;
|
|
||||||
private static final int PROGRESS_UPLOAD_DATA_MAX = 90;
|
|
||||||
private static final int PROGRESS_UPLOAD_WAYPOINTS = 95;
|
|
||||||
private static final int PROGRESS_COMPLETE = 100;
|
|
||||||
|
|
||||||
private final Activity context;
|
|
||||||
private final AuthManager auth;
|
|
||||||
private final long trackId;
|
|
||||||
private final ProgressIndicator progressIndicator;
|
|
||||||
private final OnSendCompletedListener onCompletion;
|
|
||||||
private final StringUtils stringUtils;
|
|
||||||
private final MyTracksProviderUtils providerUtils;
|
|
||||||
|
|
||||||
// Progress status
|
|
||||||
private int totalLocationsRead;
|
|
||||||
private int totalLocationsPrepared;
|
|
||||||
private int totalLocationsUploaded;
|
|
||||||
private int totalLocations;
|
|
||||||
private int totalSegmentsUploaded;
|
|
||||||
|
|
||||||
private HttpRequestFactory httpRequestFactory;
|
|
||||||
private String tableId;
|
|
||||||
|
|
||||||
private static String MARKER_TYPE_START = "large_green";
|
|
||||||
private static String MARKER_TYPE_END = "large_red";
|
|
||||||
private static String MARKER_TYPE_WAYPOINT = "large_yellow";
|
|
||||||
|
|
||||||
public SendToFusionTables(Activity context, AuthManager auth, long trackId,
|
|
||||||
ProgressIndicator progressIndicator, OnSendCompletedListener onCompletion) {
|
|
||||||
this.context = context;
|
|
||||||
this.auth = auth;
|
|
||||||
this.trackId = trackId;
|
|
||||||
this.progressIndicator = progressIndicator;
|
|
||||||
this.onCompletion = onCompletion;
|
|
||||||
this.stringUtils = new StringUtils(context);
|
|
||||||
this.providerUtils = MyTracksProviderUtils.Factory.get(context);
|
|
||||||
|
|
||||||
HttpTransport transport = ApiFeatures.getInstance().getApiAdapter().getHttpTransport();
|
|
||||||
httpRequestFactory = transport.createRequestFactory(new MethodOverride());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
Log.d(Constants.TAG, "Sending to Fusion tables: trackId = " + trackId);
|
|
||||||
doUpload();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String getMapVisualizationUrl(Track track) {
|
|
||||||
if (track == null || track.getStatistics() == null || track.getTableId() == null) {
|
|
||||||
Log.w(TAG, "Unable to get track URL");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO(leifhendrik): Determine correct bounding box and zoom level that will show the entire track.
|
|
||||||
TripStatistics stats = track.getStatistics();
|
|
||||||
double latE6 = stats.getBottom() + (stats.getTop() - stats.getBottom()) / 2;
|
|
||||||
double lonE6 = stats.getLeft() + (stats.getRight() - stats.getLeft()) / 2;
|
|
||||||
int z = 15;
|
|
||||||
|
|
||||||
// We explicitly format with Locale.US because we need the latitude and
|
|
||||||
// longitude to be formatted in a locale-independent manner. Specifically,
|
|
||||||
// we need the decimal separator to be a period rather than a comma.
|
|
||||||
return String.format(Locale.US, FUSIONTABLES_MAP, track.getTableId(),
|
|
||||||
latE6 / 1.E6, lonE6 / 1.E6, z);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void doUpload() {
|
|
||||||
boolean success = true;
|
|
||||||
try {
|
|
||||||
progressIndicator.setProgressValue(PROGRESS_INITIALIZATION);
|
|
||||||
progressIndicator.setProgressMessage(
|
|
||||||
context.getString(R.string.send_google_progress_reading_track));
|
|
||||||
|
|
||||||
// Get the track meta-data
|
|
||||||
Track track = providerUtils.getTrack(trackId);
|
|
||||||
if (track == null) {
|
|
||||||
Log.w(Constants.TAG, "Cannot get track.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
String originalDescription = track.getDescription();
|
|
||||||
|
|
||||||
// Create a new table:
|
|
||||||
progressIndicator.setProgressValue(PROGRESS_FUSION_TABLE_CREATE);
|
|
||||||
String creatingFormat = context.getString(R.string.send_google_progress_creating);
|
|
||||||
String serviceName = context.getString(SendType.FUSION_TABLES.getServiceName());
|
|
||||||
progressIndicator.setProgressMessage(String.format(creatingFormat, serviceName));
|
|
||||||
if (!createNewTable(track) || !makeTableUnlisted()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
progressIndicator.setProgressValue(PROGRESS_UPLOAD_DATA_MIN);
|
|
||||||
String sendingFormat = context.getString(R.string.send_google_progress_sending);
|
|
||||||
progressIndicator.setProgressMessage(String.format(sendingFormat, serviceName));
|
|
||||||
|
|
||||||
// Upload all of the segments of the track plus start/end markers
|
|
||||||
if (!uploadAllTrackPoints(track, originalDescription)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
progressIndicator.setProgressValue(PROGRESS_UPLOAD_WAYPOINTS);
|
|
||||||
|
|
||||||
// Upload all the waypoints.
|
|
||||||
if (!uploadWaypoints(track)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.d(Constants.TAG, "SendToFusionTables: Done: " + success);
|
|
||||||
progressIndicator.setProgressValue(PROGRESS_COMPLETE);
|
|
||||||
} finally {
|
|
||||||
final boolean finalSuccess = success;
|
|
||||||
context.runOnUiThread(new Runnable() {
|
|
||||||
public void run() {
|
|
||||||
if (onCompletion != null) {
|
|
||||||
onCompletion.onSendCompleted(tableId, finalSuccess);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new table.
|
|
||||||
* If successful sets {@link #tableId}.
|
|
||||||
*
|
|
||||||
* @return true in case of success.
|
|
||||||
*/
|
|
||||||
private boolean createNewTable(Track track) {
|
|
||||||
Log.d(Constants.TAG, "Creating a new fusion table.");
|
|
||||||
String query = "CREATE TABLE '" + sqlEscape(track.getName()) +
|
|
||||||
"' (name:STRING,description:STRING,geometry:LOCATION,marker:STRING)";
|
|
||||||
return runUpdate(query);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean makeTableUnlisted() {
|
|
||||||
Log.d(Constants.TAG, "Setting visibility to unlisted.");
|
|
||||||
String query = "UPDATE TABLE " + tableId + " SET VISIBILITY = UNLISTED";
|
|
||||||
return runUpdate(query);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Formats given values SQL style. Escapes single quotes with a backslash.
|
|
||||||
*
|
|
||||||
* @param values the values to format
|
|
||||||
* @return the values formatted as: ('value1','value2',...,'value_n').
|
|
||||||
*/
|
|
||||||
private static String values(String... values) {
|
|
||||||
StringBuilder builder = new StringBuilder("(");
|
|
||||||
for (int i = 0; i < values.length; i++) {
|
|
||||||
if (i > 0) {
|
|
||||||
builder.append(',');
|
|
||||||
}
|
|
||||||
builder.append('\'');
|
|
||||||
builder.append(sqlEscape(values[i]));
|
|
||||||
builder.append('\'');
|
|
||||||
}
|
|
||||||
builder.append(')');
|
|
||||||
return builder.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String sqlEscape(String value) {
|
|
||||||
return value.replaceAll("'", "''");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new row representing a marker.
|
|
||||||
*
|
|
||||||
* @param name the marker name
|
|
||||||
* @param description the marker description
|
|
||||||
* @param location the marker location
|
|
||||||
* @return true in case of success.
|
|
||||||
*/
|
|
||||||
private boolean createNewPoint(String name, String description, Location location,
|
|
||||||
String marker) {
|
|
||||||
Log.d(Constants.TAG, "Creating a new row with a point.");
|
|
||||||
String query = "INSERT INTO " + tableId + " (name,description,geometry,marker) VALUES "
|
|
||||||
+ values(name, description, getKmlPoint(location), marker);
|
|
||||||
return runUpdate(query);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new row representing a line segment.
|
|
||||||
*
|
|
||||||
* @param track the track/segment to draw
|
|
||||||
* @return true in case of success.
|
|
||||||
*/
|
|
||||||
private boolean createNewLineString(Track track) {
|
|
||||||
Log.d(Constants.TAG, "Creating a new row with a point.");
|
|
||||||
String query = "INSERT INTO " + tableId
|
|
||||||
+ " (name,description,geometry) VALUES "
|
|
||||||
+ values(track.getName(), track.getDescription(), getKmlLineString(track));
|
|
||||||
return runUpdate(query);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean uploadAllTrackPoints(final Track track, String originalDescription) {
|
|
||||||
SharedPreferences preferences = context.getSharedPreferences(
|
|
||||||
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
|
|
||||||
boolean metricUnits = true;
|
|
||||||
if (preferences != null) {
|
|
||||||
metricUnits = preferences.getBoolean(context.getString(R.string.metric_units_key), true);
|
|
||||||
}
|
|
||||||
|
|
||||||
Cursor locationsCursor = providerUtils.getLocationsCursor(track.getId(), 0, -1, false);
|
|
||||||
try {
|
|
||||||
if (locationsCursor == null || !locationsCursor.moveToFirst()) {
|
|
||||||
Log.w(Constants.TAG, "Unable to get any points to upload");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
totalLocationsRead = 0;
|
|
||||||
totalLocationsPrepared = 0;
|
|
||||||
totalLocationsUploaded = 0;
|
|
||||||
totalLocations = locationsCursor.getCount();
|
|
||||||
totalSegmentsUploaded = 0;
|
|
||||||
|
|
||||||
// Limit the number of elevation readings. Ideally we would want around 250.
|
|
||||||
int elevationSamplingFrequency =
|
|
||||||
Math.max(1, (int) (totalLocations / 250.0));
|
|
||||||
Log.d(Constants.TAG,
|
|
||||||
"Using elevation sampling factor: " + elevationSamplingFrequency
|
|
||||||
+ " on " + totalLocations);
|
|
||||||
double totalDistance = 0;
|
|
||||||
|
|
||||||
Vector<Double> distances = new Vector<Double>();
|
|
||||||
Vector<Double> elevations = new Vector<Double>();
|
|
||||||
DoubleBuffer elevationBuffer = new DoubleBuffer(Constants.ELEVATION_SMOOTHING_FACTOR);
|
|
||||||
|
|
||||||
List<Location> locations = new ArrayList<Location>(MAX_POINTS_PER_UPLOAD);
|
|
||||||
Location lastLocation = null;
|
|
||||||
do {
|
|
||||||
if (totalLocationsRead % 100 == 0) {
|
|
||||||
updateTrackDataUploadProgress();
|
|
||||||
}
|
|
||||||
|
|
||||||
Location loc = providerUtils.createLocation(locationsCursor);
|
|
||||||
locations.add(loc);
|
|
||||||
|
|
||||||
if (totalLocationsRead == 0) {
|
|
||||||
// Put a marker at the first point of the first valid segment:
|
|
||||||
String name = track.getName() + " " + context.getString(R.string.marker_label_start);
|
|
||||||
createNewPoint(name, "", loc, MARKER_TYPE_START);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add to the elevation profile.
|
|
||||||
if (loc != null && LocationUtils.isValidLocation(loc)) {
|
|
||||||
// All points go into the smoothing buffer...
|
|
||||||
elevationBuffer.setNext(metricUnits ? loc.getAltitude()
|
|
||||||
: loc.getAltitude() * UnitConversions.M_TO_FT);
|
|
||||||
if (lastLocation != null) {
|
|
||||||
double dist = lastLocation.distanceTo(loc);
|
|
||||||
totalDistance += dist;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ...but only a few points are really used to keep the url short.
|
|
||||||
if (totalLocationsRead % elevationSamplingFrequency == 0) {
|
|
||||||
distances.add(totalDistance);
|
|
||||||
elevations.add(elevationBuffer.getAverage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the location was not valid, it's a segment split, so make sure the
|
|
||||||
// distance between the previous segment and the new one is not accounted
|
|
||||||
// for in the next iteration.
|
|
||||||
lastLocation = loc;
|
|
||||||
|
|
||||||
// Every now and then, upload the accumulated points
|
|
||||||
if (totalLocationsRead % MAX_POINTS_PER_UPLOAD == MAX_POINTS_PER_UPLOAD - 1) {
|
|
||||||
if (!prepareAndUploadPoints(track, locations)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
totalLocationsRead++;
|
|
||||||
} while (locationsCursor.moveToNext());
|
|
||||||
|
|
||||||
// Do a final upload with what's left
|
|
||||||
if (!prepareAndUploadPoints(track, locations)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Put an end marker at the last point of the last valid segment:
|
|
||||||
if (lastLocation != null) {
|
|
||||||
track.setDescription("<p>" + originalDescription + "</p><p>"
|
|
||||||
+ stringUtils.generateTrackDescription(track, distances, elevations)
|
|
||||||
+ "</p>");
|
|
||||||
String name = track.getName() + " " + context.getString(R.string.marker_label_end);
|
|
||||||
return createNewPoint(name, track.getDescription(), lastLocation, MARKER_TYPE_END);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} finally {
|
|
||||||
if (locationsCursor != null) {
|
|
||||||
locationsCursor.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Appends the given location to the string in the format:
|
|
||||||
* longitude,latitude[,altitude]
|
|
||||||
*
|
|
||||||
* @param location the location to be added
|
|
||||||
* @param builder the string builder to use
|
|
||||||
*/
|
|
||||||
private void appendCoordinate(Location location, StringBuilder builder) {
|
|
||||||
builder
|
|
||||||
.append(location.getLongitude())
|
|
||||||
.append(",")
|
|
||||||
.append(location.getLatitude());
|
|
||||||
if (location.hasAltitude()) {
|
|
||||||
builder.append(",");
|
|
||||||
builder.append(location.getAltitude());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets a KML Point tag for the given location.
|
|
||||||
*
|
|
||||||
* @param location The location.
|
|
||||||
* @return the kml.
|
|
||||||
*/
|
|
||||||
private String getKmlPoint(Location location) {
|
|
||||||
StringBuilder builder = new StringBuilder("<Point><coordinates>");
|
|
||||||
appendCoordinate(location, builder);
|
|
||||||
builder.append("</coordinates></Point>");
|
|
||||||
return builder.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a KML LineString.
|
|
||||||
*
|
|
||||||
* @param track the track.
|
|
||||||
* @return the KML LineString.
|
|
||||||
*/
|
|
||||||
private String getKmlLineString(Track track) {
|
|
||||||
StringBuilder builder = new StringBuilder("<LineString><coordinates>");
|
|
||||||
for (Location location : track.getLocations()) {
|
|
||||||
appendCoordinate(location, builder);
|
|
||||||
builder.append(' ');
|
|
||||||
}
|
|
||||||
builder.append("</coordinates></LineString>");
|
|
||||||
return builder.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean prepareAndUploadPoints(Track track, List<Location> locations) {
|
|
||||||
updateTrackDataUploadProgress();
|
|
||||||
|
|
||||||
int numLocations = locations.size();
|
|
||||||
if (numLocations < 2) {
|
|
||||||
Log.d(Constants.TAG, "Not preparing/uploading too few points");
|
|
||||||
totalLocationsUploaded += numLocations;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prepare/pre-process the points
|
|
||||||
ArrayList<Track> splitTracks = prepareLocations(track, locations);
|
|
||||||
|
|
||||||
// Start uploading them
|
|
||||||
for (Track splitTrack : splitTracks) {
|
|
||||||
if (totalSegmentsUploaded > 1) {
|
|
||||||
splitTrack.setName(splitTrack.getName() + " "
|
|
||||||
+ String.format(
|
|
||||||
context.getString(R.string.send_google_track_part_label), totalSegmentsUploaded));
|
|
||||||
}
|
|
||||||
totalSegmentsUploaded++;
|
|
||||||
Log.d(Constants.TAG,
|
|
||||||
"SendToFusionTables: Prepared feature for upload w/ "
|
|
||||||
+ splitTrack.getLocations().size() + " points.");
|
|
||||||
|
|
||||||
// Transmit tracks via GData feed:
|
|
||||||
// -------------------------------
|
|
||||||
Log.d(Constants.TAG,
|
|
||||||
"SendToFusionTables: Uploading to table " + tableId + " w/ auth " + auth);
|
|
||||||
if (!uploadTrackPoints(splitTrack)) {
|
|
||||||
Log.e(Constants.TAG, "Uploading failed");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
locations.clear();
|
|
||||||
totalLocationsUploaded += numLocations;
|
|
||||||
updateTrackDataUploadProgress();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prepares a buffer of locations for transmission to google fusion tables.
|
|
||||||
*
|
|
||||||
* @param track the original track with meta data
|
|
||||||
* @param locations locations on the track
|
|
||||||
* @return an array of tracks each with a sub section of the points in the
|
|
||||||
* original buffer
|
|
||||||
*/
|
|
||||||
private ArrayList<Track> prepareLocations(
|
|
||||||
Track track, Iterable<Location> locations) {
|
|
||||||
ArrayList<Track> splitTracks = new ArrayList<Track>();
|
|
||||||
|
|
||||||
// Create segments from each full track:
|
|
||||||
Track segment = new Track();
|
|
||||||
TripStatistics segmentStats = segment.getStatistics();
|
|
||||||
TripStatistics trackStats = track.getStatistics();
|
|
||||||
segment.setId(track.getId());
|
|
||||||
segment.setName(track.getName());
|
|
||||||
segment.setDescription(/* track.getDescription() */ "");
|
|
||||||
segment.setCategory(track.getCategory());
|
|
||||||
segmentStats.setStartTime(trackStats.getStartTime());
|
|
||||||
segmentStats.setStopTime(trackStats.getStopTime());
|
|
||||||
boolean startNewTrackSegment = false;
|
|
||||||
for (Location loc : locations) {
|
|
||||||
if (totalLocationsPrepared % 100 == 0) {
|
|
||||||
updateTrackDataUploadProgress();
|
|
||||||
}
|
|
||||||
if (loc.getLatitude() > 90) {
|
|
||||||
startNewTrackSegment = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (startNewTrackSegment) {
|
|
||||||
// Close up the last segment.
|
|
||||||
prepareTrackSegment(segment, splitTracks);
|
|
||||||
|
|
||||||
Log.d(Constants.TAG,
|
|
||||||
"MyTracksSendToFusionTables: Starting new track segment...");
|
|
||||||
startNewTrackSegment = false;
|
|
||||||
segment = new Track();
|
|
||||||
segment.setId(track.getId());
|
|
||||||
segment.setName(track.getName());
|
|
||||||
segment.setDescription(/* track.getDescription() */ "");
|
|
||||||
segment.setCategory(track.getCategory());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (loc.getLatitude() <= 90) {
|
|
||||||
segment.addLocation(loc);
|
|
||||||
if (segmentStats.getStartTime() < 0) {
|
|
||||||
segmentStats.setStartTime(loc.getTime());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
totalLocationsPrepared++;
|
|
||||||
}
|
|
||||||
|
|
||||||
prepareTrackSegment(segment, splitTracks);
|
|
||||||
|
|
||||||
return splitTracks;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prepares a track segment for sending to google fusion tables.
|
|
||||||
* The main steps are:
|
|
||||||
* - correcting end time
|
|
||||||
* - decimating locations
|
|
||||||
* - splitting into smaller tracks.
|
|
||||||
*
|
|
||||||
* The final track pieces will be put in the array list splitTracks.
|
|
||||||
*
|
|
||||||
* @param segment the original segment of the track
|
|
||||||
* @param splitTracks an array of smaller track segments
|
|
||||||
*/
|
|
||||||
private void prepareTrackSegment(
|
|
||||||
Track segment, ArrayList<Track> splitTracks) {
|
|
||||||
TripStatistics segmentStats = segment.getStatistics();
|
|
||||||
if (segmentStats.getStopTime() < 0
|
|
||||||
&& segment.getLocations().size() > 0) {
|
|
||||||
segmentStats.setStopTime(segment.getLocations().size() - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Decimate to 2 meter precision. Fusion tables doesn't like too many
|
|
||||||
* points:
|
|
||||||
*/
|
|
||||||
LocationUtils.decimate(segment, 2.0);
|
|
||||||
|
|
||||||
/* If the track still has > 2500 points, split it in pieces: */
|
|
||||||
final int maxPoints = 2500;
|
|
||||||
if (segment.getLocations().size() > maxPoints) {
|
|
||||||
splitTracks.addAll(LocationUtils.split(segment, maxPoints));
|
|
||||||
} else if (segment.getLocations().size() >= 2) {
|
|
||||||
splitTracks.add(segment);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean uploadTrackPoints(Track splitTrack) {
|
|
||||||
int numLocations = splitTrack.getLocations().size();
|
|
||||||
if (numLocations < 2) {
|
|
||||||
// Need at least two points for a polyline:
|
|
||||||
Log.w(Constants.TAG, "Not uploading too few points");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return createNewLineString(splitTrack);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Uploads all of the waypoints associated with this track to a table.
|
|
||||||
*
|
|
||||||
* @param track The track to upload waypoints for.
|
|
||||||
*
|
|
||||||
* @return True on success.
|
|
||||||
*/
|
|
||||||
private boolean uploadWaypoints(final Track track) {
|
|
||||||
// TODO: Stream through the waypoints in chunks.
|
|
||||||
// I am leaving the number of waypoints very high which should not be a
|
|
||||||
// problem because we don't try to load them into objects all at the
|
|
||||||
// same time.
|
|
||||||
boolean success = true;
|
|
||||||
Cursor c = null;
|
|
||||||
try {
|
|
||||||
c = providerUtils.getWaypointsCursor(
|
|
||||||
track.getId(), 0,
|
|
||||||
Constants.MAX_LOADED_WAYPOINTS_POINTS);
|
|
||||||
if (c != null) {
|
|
||||||
if (c.moveToFirst()) {
|
|
||||||
// This will skip the 1st waypoint (it carries the stats for the
|
|
||||||
// last segment).
|
|
||||||
while (c.moveToNext()) {
|
|
||||||
Waypoint wpt = providerUtils.createWaypoint(c);
|
|
||||||
Log.d(Constants.TAG, "SendToFusionTables: Creating waypoint.");
|
|
||||||
success = createNewPoint(wpt.getName(), wpt.getDescription(), wpt.getLocation(),
|
|
||||||
MARKER_TYPE_WAYPOINT);
|
|
||||||
if (!success) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!success) {
|
|
||||||
Log.w(Constants.TAG, "SendToFusionTables: upload waypoints failed.");
|
|
||||||
}
|
|
||||||
return success;
|
|
||||||
} finally {
|
|
||||||
if (c != null) {
|
|
||||||
c.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateTrackDataUploadProgress() {
|
|
||||||
// The percent of the total that represents the completed part of this
|
|
||||||
// segment. We calculate it as an absolute percentage, and then scale it
|
|
||||||
// to fit the completion percentage range alloted to track data upload.
|
|
||||||
double totalPercentage =
|
|
||||||
(totalLocationsRead + totalLocationsPrepared + totalLocationsUploaded)
|
|
||||||
/ (totalLocations * 3.0);
|
|
||||||
|
|
||||||
double scaledPercentage = totalPercentage
|
|
||||||
* (PROGRESS_UPLOAD_DATA_MAX - PROGRESS_UPLOAD_DATA_MIN) + PROGRESS_UPLOAD_DATA_MIN;
|
|
||||||
|
|
||||||
progressIndicator.setProgressValue((int) scaledPercentage);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Runs an update query. Handles authentication.
|
|
||||||
*
|
|
||||||
* @param query The given SQL like query
|
|
||||||
* @return true in case of success
|
|
||||||
*/
|
|
||||||
private boolean runUpdate(final String query) {
|
|
||||||
GDataWrapper<HttpRequestFactory> wrapper = new GDataWrapper<HttpRequestFactory>();
|
|
||||||
wrapper.setAuthManager(auth);
|
|
||||||
wrapper.setRetryOnAuthFailure(true);
|
|
||||||
wrapper.setClient(httpRequestFactory);
|
|
||||||
Log.d(Constants.TAG, "GData connection prepared: " + this.auth);
|
|
||||||
wrapper.runQuery(new QueryFunction<HttpRequestFactory>() {
|
|
||||||
@Override
|
|
||||||
public void query(HttpRequestFactory factory)
|
|
||||||
throws IOException, GDataWrapper.ParseException, GDataWrapper.HttpException,
|
|
||||||
GDataWrapper.AuthenticationException {
|
|
||||||
GenericUrl url = new GenericUrl(FUSIONTABLES_BASE_FEED_URL);
|
|
||||||
|
|
||||||
String sql = "sql=" + URLEncoder.encode(query, "UTF-8");
|
|
||||||
ByteArrayInputStream inputStream = new ByteArrayInputStream(Strings.toBytesUtf8(sql));
|
|
||||||
InputStreamContent isc = new InputStreamContent(null, inputStream );
|
|
||||||
|
|
||||||
HttpRequest request = factory.buildPostRequest(url, isc);
|
|
||||||
|
|
||||||
GoogleHeaders headers = new GoogleHeaders();
|
|
||||||
headers.setApplicationName("Google-MyTracks-" + SystemUtils.getMyTracksVersion(context));
|
|
||||||
headers.gdataVersion = GDATA_VERSION;
|
|
||||||
headers.setGoogleLogin(auth.getAuthToken());
|
|
||||||
headers.setContentType(CONTENT_TYPE);
|
|
||||||
request.setHeaders(headers);
|
|
||||||
|
|
||||||
Log.d(Constants.TAG, "Running update query " + url.toString() + ": " + sql);
|
|
||||||
HttpResponse response;
|
|
||||||
try {
|
|
||||||
response = request.execute();
|
|
||||||
} catch (HttpResponseException e) {
|
|
||||||
throw new GDataWrapper.HttpException(e.getResponse().getStatusCode(),
|
|
||||||
e.getResponse().getStatusMessage());
|
|
||||||
}
|
|
||||||
boolean success = response.isSuccessStatusCode();
|
|
||||||
if (success) {
|
|
||||||
byte[] result = new byte[1024];
|
|
||||||
int read = response.getContent().read(result);
|
|
||||||
String s = new String(result, 0, read, "UTF8");
|
|
||||||
String[] lines = s.split(Strings.LINE_SEPARATOR);
|
|
||||||
if (lines[0].equals("tableid")) {
|
|
||||||
tableId = lines[1];
|
|
||||||
Log.d(Constants.TAG, "tableId = " + tableId);
|
|
||||||
} else {
|
|
||||||
Log.w(Constants.TAG, "Unrecognized response: " + lines[0]);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Log.d(Constants.TAG,
|
|
||||||
"Query failed: " + response.getStatusMessage() + " ("
|
|
||||||
+ response.getStatusCode() + ")");
|
|
||||||
throw new GDataWrapper.HttpException(
|
|
||||||
response.getStatusCode(), response.getStatusMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return wrapper.getErrorType() == GDataWrapper.ERROR_NO_ERROR;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,458 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2008 Google Inc.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
|
||||||
* use this file except in compliance with the License. You may obtain a copy of
|
|
||||||
* the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
* License for the specific language governing permissions and limitations under
|
|
||||||
* the License.
|
|
||||||
*/
|
|
||||||
package com.google.android.apps.mytracks.io;
|
|
||||||
|
|
||||||
import static com.google.android.apps.mytracks.Constants.TAG;
|
|
||||||
|
|
||||||
import com.google.android.apps.mytracks.Constants;
|
|
||||||
import com.google.android.apps.mytracks.ProgressIndicator;
|
|
||||||
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
|
|
||||||
import com.google.android.apps.mytracks.content.Track;
|
|
||||||
import com.google.android.apps.mytracks.content.Waypoint;
|
|
||||||
import com.google.android.apps.mytracks.io.maps.MapsFacade;
|
|
||||||
import com.google.android.apps.mytracks.io.sendtogoogle.SendType;
|
|
||||||
import com.google.android.apps.mytracks.stats.DoubleBuffer;
|
|
||||||
import com.google.android.apps.mytracks.stats.TripStatistics;
|
|
||||||
import com.google.android.apps.mytracks.util.LocationUtils;
|
|
||||||
import com.google.android.apps.mytracks.util.StringUtils;
|
|
||||||
import com.google.android.apps.mytracks.util.UnitConversions;
|
|
||||||
import com.google.android.maps.mytracks.R;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.location.Location;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Vector;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A helper class used to transmit tracks to Google Maps.
|
|
||||||
* A new instance should be used for each upload.
|
|
||||||
*
|
|
||||||
* IMPORTANT: while this code is Apache-licensed, please notice that usage of
|
|
||||||
* the Google Maps servers through this API is only allowed for the My Tracks
|
|
||||||
* application. Other applications looking to upload maps data should look
|
|
||||||
* into using the Fusion Tables API.
|
|
||||||
*
|
|
||||||
* @author Leif Hendrik Wilden
|
|
||||||
*/
|
|
||||||
public class SendToMaps implements Runnable {
|
|
||||||
|
|
||||||
public static final String NEW_MAP_ID = "new";
|
|
||||||
private static final int MAX_POINTS_PER_UPLOAD = 2048;
|
|
||||||
|
|
||||||
private final Activity context;
|
|
||||||
private final AuthManager auth;
|
|
||||||
private final long trackId;
|
|
||||||
private final ProgressIndicator progressIndicator;
|
|
||||||
private final OnSendCompletedListener onCompletion;
|
|
||||||
private final StringUtils stringUtils;
|
|
||||||
private final MyTracksProviderUtils providerUtils;
|
|
||||||
private String mapId;
|
|
||||||
|
|
||||||
private MapsFacade mapsClient;
|
|
||||||
|
|
||||||
// Progress status
|
|
||||||
private int totalLocationsRead;
|
|
||||||
private int totalLocationsPrepared;
|
|
||||||
private int totalLocationsUploaded;
|
|
||||||
private int totalLocations;
|
|
||||||
private int totalSegmentsUploaded;
|
|
||||||
|
|
||||||
public interface OnSendCompletedListener {
|
|
||||||
void onSendCompleted(String mapId, boolean success);
|
|
||||||
}
|
|
||||||
|
|
||||||
public SendToMaps(Activity context, String mapId, AuthManager auth,
|
|
||||||
long trackId, ProgressIndicator progressIndicator,
|
|
||||||
OnSendCompletedListener onCompletion) {
|
|
||||||
this.context = context;
|
|
||||||
this.mapId = mapId;
|
|
||||||
this.auth = auth;
|
|
||||||
this.trackId = trackId;
|
|
||||||
this.progressIndicator = progressIndicator;
|
|
||||||
this.onCompletion = onCompletion;
|
|
||||||
this.stringUtils = new StringUtils(context);
|
|
||||||
this.providerUtils = MyTracksProviderUtils.Factory.get(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
Log.d(TAG, "Sending to Maps: trackId = " + trackId);
|
|
||||||
doUpload();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void doUpload() {
|
|
||||||
boolean success = true;
|
|
||||||
try {
|
|
||||||
progressIndicator.setProgressMessage(
|
|
||||||
context.getString(R.string.send_google_progress_reading_track));
|
|
||||||
|
|
||||||
// Get the track meta-data
|
|
||||||
Track track = providerUtils.getTrack(trackId);
|
|
||||||
if (track == null) {
|
|
||||||
Log.w(Constants.TAG, "Cannot get track.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
String originalDescription = track.getDescription();
|
|
||||||
track.setDescription("<p>" + track.getDescription() + "</p><p>"
|
|
||||||
+ stringUtils.generateTrackDescription(track, null, null) + "</p>");
|
|
||||||
|
|
||||||
mapsClient = new MapsFacade(context, auth);
|
|
||||||
|
|
||||||
// Create a new map if necessary:
|
|
||||||
boolean isNewMap = mapId.equals(NEW_MAP_ID);
|
|
||||||
if (isNewMap) {
|
|
||||||
SharedPreferences preferences = context.getSharedPreferences(
|
|
||||||
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
|
|
||||||
boolean mapPublic = true;
|
|
||||||
if (preferences != null) {
|
|
||||||
mapPublic = preferences.getBoolean(
|
|
||||||
context.getString(R.string.default_map_public_key), true);
|
|
||||||
}
|
|
||||||
|
|
||||||
String creatingFormat = context.getString(R.string.send_google_progress_creating);
|
|
||||||
String serviceName = context.getString(SendType.MAPS.getServiceName());
|
|
||||||
progressIndicator.setProgressMessage(String.format(creatingFormat, serviceName));
|
|
||||||
|
|
||||||
StringBuilder mapIdBuilder = new StringBuilder();
|
|
||||||
success = mapsClient.createNewMap(
|
|
||||||
track.getName(), track.getCategory(), originalDescription, mapPublic, mapIdBuilder);
|
|
||||||
mapId = mapIdBuilder.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Upload all of the segments of the track plus start/end markers
|
|
||||||
if (success) {
|
|
||||||
success = uploadAllTrackPoints(track, originalDescription);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Put waypoints.
|
|
||||||
if (success) {
|
|
||||||
Cursor c = providerUtils.getWaypointsCursor(
|
|
||||||
track.getId(), 0,
|
|
||||||
Constants.MAX_LOADED_WAYPOINTS_POINTS);
|
|
||||||
if (c != null) {
|
|
||||||
try {
|
|
||||||
if (c.getCount() > 1 && c.moveToFirst()) {
|
|
||||||
// This will skip the 1st waypoint (it carries the stats for the
|
|
||||||
// last segment).
|
|
||||||
ArrayList<Waypoint> waypoints = new ArrayList<Waypoint>(c.getCount() - 1);
|
|
||||||
while (c.moveToNext()) {
|
|
||||||
Waypoint wpt = providerUtils.createWaypoint(c);
|
|
||||||
waypoints.add(wpt);
|
|
||||||
}
|
|
||||||
|
|
||||||
success = mapsClient.uploadWaypoints(mapId, waypoints);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
c.close();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
success = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!success) {
|
|
||||||
Log.w(TAG, "SendToMaps: upload waypoints failed.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.d(TAG, "SendToMaps: Done: " + success);
|
|
||||||
progressIndicator.setProgressValue(100);
|
|
||||||
} finally {
|
|
||||||
if (mapsClient != null) {
|
|
||||||
mapsClient.cleanUp();
|
|
||||||
}
|
|
||||||
|
|
||||||
final boolean finalSuccess = success;
|
|
||||||
context.runOnUiThread(new Runnable() {
|
|
||||||
public void run() {
|
|
||||||
if (onCompletion != null) {
|
|
||||||
onCompletion.onSendCompleted(mapId, finalSuccess);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean uploadAllTrackPoints(
|
|
||||||
final Track track, String originalDescription) {
|
|
||||||
SharedPreferences preferences = context.getSharedPreferences(
|
|
||||||
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
|
|
||||||
boolean metricUnits = true;
|
|
||||||
if (preferences != null) {
|
|
||||||
metricUnits =
|
|
||||||
preferences.getBoolean(context.getString(R.string.metric_units_key),
|
|
||||||
true);
|
|
||||||
}
|
|
||||||
|
|
||||||
Cursor locationsCursor =
|
|
||||||
providerUtils.getLocationsCursor(track.getId(), 0, -1, false);
|
|
||||||
try {
|
|
||||||
if (locationsCursor == null || !locationsCursor.moveToFirst()) {
|
|
||||||
Log.w(TAG, "Unable to get any points to upload");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
totalLocationsRead = 0;
|
|
||||||
totalLocationsPrepared = 0;
|
|
||||||
totalLocationsUploaded = 0;
|
|
||||||
totalLocations = locationsCursor.getCount();
|
|
||||||
totalSegmentsUploaded = 0;
|
|
||||||
|
|
||||||
// Limit the number of elevation readings. Ideally we would want around 250.
|
|
||||||
int elevationSamplingFrequency =
|
|
||||||
Math.max(1, (int) (totalLocations / 250.0));
|
|
||||||
Log.d(TAG,
|
|
||||||
"Using elevation sampling factor: " + elevationSamplingFrequency
|
|
||||||
+ " on " + totalLocations);
|
|
||||||
double totalDistance = 0;
|
|
||||||
|
|
||||||
Vector<Double> distances = new Vector<Double>();
|
|
||||||
Vector<Double> elevations = new Vector<Double>();
|
|
||||||
DoubleBuffer elevationBuffer =
|
|
||||||
new DoubleBuffer(Constants.ELEVATION_SMOOTHING_FACTOR);
|
|
||||||
|
|
||||||
List<Location> locations = new ArrayList<Location>(MAX_POINTS_PER_UPLOAD);
|
|
||||||
progressIndicator.setProgressMessage(
|
|
||||||
context.getString(R.string.send_google_progress_reading_track));
|
|
||||||
Location lastLocation = null;
|
|
||||||
do {
|
|
||||||
if (totalLocationsRead % 100 == 0) {
|
|
||||||
updateProgress();
|
|
||||||
}
|
|
||||||
|
|
||||||
Location loc = providerUtils.createLocation(locationsCursor);
|
|
||||||
locations.add(loc);
|
|
||||||
|
|
||||||
if (totalLocationsRead == 0) {
|
|
||||||
// Put a marker at the first point of the first valid segment:
|
|
||||||
mapsClient.uploadMarker(mapId, track.getName(), track.getDescription(), loc, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add to the elevation profile.
|
|
||||||
if (loc != null && LocationUtils.isValidLocation(loc)) {
|
|
||||||
// All points go into the smoothing buffer...
|
|
||||||
elevationBuffer.setNext(metricUnits ? loc.getAltitude()
|
|
||||||
: loc.getAltitude() * UnitConversions.M_TO_FT);
|
|
||||||
if (lastLocation != null) {
|
|
||||||
double dist = lastLocation.distanceTo(loc);
|
|
||||||
totalDistance += dist;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ...but only a few points are really used to keep the url short.
|
|
||||||
if (totalLocationsRead % elevationSamplingFrequency == 0) {
|
|
||||||
distances.add(totalDistance);
|
|
||||||
elevations.add(elevationBuffer.getAverage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the location was not valid, it's a segment split, so make sure the
|
|
||||||
// distance between the previous segment and the new one is not accounted
|
|
||||||
// for in the next iteration.
|
|
||||||
lastLocation = loc;
|
|
||||||
|
|
||||||
// Every now and then, upload the accumulated points
|
|
||||||
if (totalLocationsRead % MAX_POINTS_PER_UPLOAD
|
|
||||||
== MAX_POINTS_PER_UPLOAD - 1) {
|
|
||||||
if (!prepareAndUploadPoints(track, locations)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
totalLocationsRead++;
|
|
||||||
} while (locationsCursor.moveToNext());
|
|
||||||
|
|
||||||
// Do a final upload with what's left
|
|
||||||
if (!prepareAndUploadPoints(track, locations)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Put an end marker at the last point of the last valid segment:
|
|
||||||
if (lastLocation != null) {
|
|
||||||
track.setDescription("<p>" + originalDescription + "</p><p>"
|
|
||||||
+ stringUtils.generateTrackDescription(
|
|
||||||
track, distances, elevations)
|
|
||||||
+ "</p>");
|
|
||||||
return mapsClient.uploadMarker(mapId, track.getName(), track.getDescription(),
|
|
||||||
lastLocation, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} finally {
|
|
||||||
if (locationsCursor != null) {
|
|
||||||
locationsCursor.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private boolean prepareAndUploadPoints(Track track, List<Location> locations) {
|
|
||||||
progressIndicator.setProgressMessage(
|
|
||||||
context.getString(R.string.send_google_progress_preparing_track));
|
|
||||||
updateProgress();
|
|
||||||
|
|
||||||
int numLocations = locations.size();
|
|
||||||
if (numLocations < 2) {
|
|
||||||
Log.d(TAG, "Not preparing/uploading too few points");
|
|
||||||
totalLocationsUploaded += numLocations;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prepare/pre-process the points
|
|
||||||
ArrayList<Track> splitTracks = prepareLocations(track, locations);
|
|
||||||
|
|
||||||
// Start uploading them
|
|
||||||
String sendingFormat = context.getString(R.string.send_google_progress_sending);
|
|
||||||
String serviceName = context.getString(SendType.MAPS.getServiceName());
|
|
||||||
progressIndicator.setProgressMessage(String.format(sendingFormat, serviceName));
|
|
||||||
for (Track splitTrack : splitTracks) {
|
|
||||||
if (totalSegmentsUploaded > 1) {
|
|
||||||
splitTrack.setName(splitTrack.getName() + " "
|
|
||||||
+ String.format(
|
|
||||||
context.getString(R.string.send_google_track_part_label), totalSegmentsUploaded));
|
|
||||||
}
|
|
||||||
totalSegmentsUploaded++;
|
|
||||||
Log.d(TAG,
|
|
||||||
"SendToMaps: Prepared feature for upload w/ "
|
|
||||||
+ splitTrack.getLocations().size() + " points.");
|
|
||||||
|
|
||||||
// Transmit tracks via GData feed:
|
|
||||||
// -------------------------------
|
|
||||||
Log.d(TAG,
|
|
||||||
"SendToMaps: Uploading to map " + mapId + " w/ auth " + auth);
|
|
||||||
if (!mapsClient.uploadTrackPoints(mapId, splitTrack.getName(), splitTrack.getLocations())) {
|
|
||||||
Log.e(TAG, "Uploading failed");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
locations.clear();
|
|
||||||
totalLocationsUploaded += numLocations;
|
|
||||||
updateProgress();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prepares a buffer of locations for transmission to google maps.
|
|
||||||
*
|
|
||||||
* @param track the original track with meta data
|
|
||||||
* @param locations a buffer of locations on the track
|
|
||||||
* @return an array of tracks each with a sub section of the points in the
|
|
||||||
* original buffer
|
|
||||||
*/
|
|
||||||
private ArrayList<Track> prepareLocations(
|
|
||||||
Track track, Iterable<Location> locations) {
|
|
||||||
ArrayList<Track> splitTracks = new ArrayList<Track>();
|
|
||||||
|
|
||||||
// Create segments from each full track:
|
|
||||||
Track segment = new Track();
|
|
||||||
TripStatistics segmentStats = segment.getStatistics();
|
|
||||||
TripStatistics trackStats = track.getStatistics();
|
|
||||||
segment.setId(track.getId());
|
|
||||||
segment.setName(track.getName());
|
|
||||||
segment.setDescription(/* track.getDescription() */ "");
|
|
||||||
segment.setCategory(track.getCategory());
|
|
||||||
segmentStats.setStartTime(trackStats.getStartTime());
|
|
||||||
segmentStats.setStopTime(trackStats.getStopTime());
|
|
||||||
boolean startNewTrackSegment = false;
|
|
||||||
for (Location loc : locations) {
|
|
||||||
if (totalLocationsPrepared % 100 == 0) {
|
|
||||||
updateProgress();
|
|
||||||
}
|
|
||||||
if (loc.getLatitude() > 90) {
|
|
||||||
startNewTrackSegment = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (startNewTrackSegment) {
|
|
||||||
// Close up the last segment.
|
|
||||||
prepareTrackSegment(segment, splitTracks);
|
|
||||||
|
|
||||||
Log.d(TAG,
|
|
||||||
"MyTracksSendToMaps: Starting new track segment...");
|
|
||||||
startNewTrackSegment = false;
|
|
||||||
segment = new Track();
|
|
||||||
segment.setId(track.getId());
|
|
||||||
segment.setName(track.getName());
|
|
||||||
segment.setDescription(/* track.getDescription() */ "");
|
|
||||||
segment.setCategory(track.getCategory());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (loc.getLatitude() <= 90) {
|
|
||||||
segment.addLocation(loc);
|
|
||||||
if (segmentStats.getStartTime() < 0) {
|
|
||||||
segmentStats.setStartTime(loc.getTime());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
totalLocationsPrepared++;
|
|
||||||
}
|
|
||||||
|
|
||||||
prepareTrackSegment(segment, splitTracks);
|
|
||||||
|
|
||||||
return splitTracks;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prepares a track segment for sending to google maps.
|
|
||||||
* The main steps are:
|
|
||||||
* - correcting end time
|
|
||||||
* - decimating locations
|
|
||||||
* - splitting into smaller tracks.
|
|
||||||
*
|
|
||||||
* The final track pieces will be put in the array list splitTracks.
|
|
||||||
*
|
|
||||||
* @param segment the original segment of the track
|
|
||||||
* @param splitTracks an array of smaller track segments
|
|
||||||
*/
|
|
||||||
private void prepareTrackSegment(
|
|
||||||
Track segment, ArrayList<Track> splitTracks) {
|
|
||||||
TripStatistics segmentStats = segment.getStatistics();
|
|
||||||
if (segmentStats.getStopTime() < 0
|
|
||||||
&& segment.getLocations().size() > 0) {
|
|
||||||
segmentStats.setStopTime(segment.getLocations().size() - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Decimate to 2 meter precision. Mapshop doesn't like too many
|
|
||||||
* points:
|
|
||||||
*/
|
|
||||||
LocationUtils.decimate(segment, 2.0);
|
|
||||||
|
|
||||||
/* It the track still has > 500 points, split it in pieces: */
|
|
||||||
if (segment.getLocations().size() > 500) {
|
|
||||||
splitTracks.addAll(LocationUtils.split(segment, 500));
|
|
||||||
} else if (segment.getLocations().size() >= 2) {
|
|
||||||
splitTracks.add(segment);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the current upload progress.
|
|
||||||
*/
|
|
||||||
private void updateProgress() {
|
|
||||||
// The percent of the total that represents the completed part of this
|
|
||||||
// segment.
|
|
||||||
int totalPercentage =
|
|
||||||
(totalLocationsRead + totalLocationsPrepared + totalLocationsUploaded) * 100
|
|
||||||
/ (totalLocations * 3);
|
|
||||||
totalPercentage = Math.min(99, totalPercentage);
|
|
||||||
progressIndicator.setProgressValue(totalPercentage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
package com.google.android.apps.mytracks.io.backup;
|
package com.google.android.apps.mytracks.io.backup;
|
||||||
|
|
||||||
import com.google.android.apps.mytracks.content.ContentTypeIds;
|
import com.google.android.apps.mytracks.content.ContentTypeIds;
|
||||||
import com.google.android.apps.mytracks.util.ApiFeatures;
|
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
|
||||||
|
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.content.SharedPreferences.Editor;
|
import android.content.SharedPreferences.Editor;
|
||||||
@@ -106,7 +106,7 @@ class PreferenceBackupHelper {
|
|||||||
byte typeId = reader.readByte();
|
byte typeId = reader.readByte();
|
||||||
readAndSetPreference(name, typeId, reader, editor);
|
readAndSetPreference(name, typeId, reader, editor);
|
||||||
}
|
}
|
||||||
ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges(editor);
|
ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,369 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2010 Google Inc.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
|
||||||
* use this file except in compliance with the License. You may obtain a copy of
|
|
||||||
* the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
* License for the specific language governing permissions and limitations under
|
|
||||||
* the License.
|
|
||||||
*/
|
|
||||||
package com.google.android.apps.mytracks.io.docs;
|
|
||||||
|
|
||||||
import com.google.android.apps.mytracks.Constants;
|
|
||||||
import com.google.android.apps.mytracks.content.Track;
|
|
||||||
import com.google.android.apps.mytracks.io.AuthManager;
|
|
||||||
import com.google.android.apps.mytracks.io.gdata.GDataWrapper;
|
|
||||||
import com.google.android.apps.mytracks.io.gdata.GDataWrapper.QueryFunction;
|
|
||||||
import com.google.android.apps.mytracks.stats.TripStatistics;
|
|
||||||
import com.google.android.apps.mytracks.util.ResourceUtils;
|
|
||||||
import com.google.android.apps.mytracks.util.StringUtils;
|
|
||||||
import com.google.android.maps.mytracks.R;
|
|
||||||
import com.google.wireless.gdata.client.GDataServiceClient;
|
|
||||||
import com.google.wireless.gdata.client.HttpException;
|
|
||||||
import com.google.wireless.gdata.data.Entry;
|
|
||||||
import com.google.wireless.gdata.docs.SpreadsheetsClient;
|
|
||||||
import com.google.wireless.gdata.docs.SpreadsheetsClient.WorksheetEntry;
|
|
||||||
import com.google.wireless.gdata.parser.GDataParser;
|
|
||||||
import com.google.wireless.gdata.parser.ParseException;
|
|
||||||
import com.google.wireless.gdata2.client.AuthenticationException;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.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 =
|
|
||||||
"https://docs.google.com/feeds/documents/private/full";
|
|
||||||
private static final String DOCS_SPREADSHEET_URL =
|
|
||||||
"https://docs.google.com/feeds/documents/private/full/spreadsheet%3A";
|
|
||||||
private static final String DOCS_WORKSHEETS_URL_FORMAT =
|
|
||||||
"https://spreadsheets.google.com/feeds/worksheets/%s/private/full";
|
|
||||||
private static final String DOCS_MY_SPREADSHEETS_FEED_URL =
|
|
||||||
"https://docs.google.com/feeds/documents/private/full?category=mine,spreadsheet";
|
|
||||||
private static final String DOCS_SPREADSHEET_URL_FORMAT =
|
|
||||||
"https://spreadsheets.google.com/feeds/list/%s/%s/private/full";
|
|
||||||
|
|
||||||
private static final String CONTENT_TYPE_PARAM = "Content-Type";
|
|
||||||
private static final String OPENDOCUMENT_SPREADSHEET_MIME_TYPE =
|
|
||||||
"application/x-vnd.oasis.opendocument.spreadsheet";
|
|
||||||
private static final String ATOM_FEED_MIME_TYPE = "application/atom+xml";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new MyTracks spreadsheet with the given name.
|
|
||||||
*
|
|
||||||
* @param context The context associated with this request.
|
|
||||||
* @param docListWrapper The GData handle for the Document List service.
|
|
||||||
* @param name The name for the newly-created spreadsheet.
|
|
||||||
* @return The spreadsheet ID, if one is created. {@code null} will be
|
|
||||||
* returned if a GData error didn't occur, but no spreadsheet ID was
|
|
||||||
* returned.
|
|
||||||
*/
|
|
||||||
public String createSpreadsheet(final Context context,
|
|
||||||
final GDataWrapper<GDataServiceClient> docListWrapper, final String name) throws IOException {
|
|
||||||
final AtomicReference<String> idSaver = new AtomicReference<String>();
|
|
||||||
|
|
||||||
boolean success = docListWrapper.runQuery(new QueryFunction<GDataServiceClient>() {
|
|
||||||
@Override
|
|
||||||
public void query(GDataServiceClient client) throws IOException,
|
|
||||||
GDataWrapper.AuthenticationException {
|
|
||||||
// Construct and send request
|
|
||||||
URL url = new URL(DOCS_FEED_URL);
|
|
||||||
URLConnection conn = url.openConnection();
|
|
||||||
conn.addRequestProperty(CONTENT_TYPE_PARAM,
|
|
||||||
OPENDOCUMENT_SPREADSHEET_MIME_TYPE);
|
|
||||||
conn.addRequestProperty("Slug", name);
|
|
||||||
conn.addRequestProperty("Authorization",
|
|
||||||
"GoogleLogin auth=" +
|
|
||||||
docListWrapper.getAuthManager().getAuthToken());
|
|
||||||
conn.setDoOutput(true);
|
|
||||||
OutputStream os = conn.getOutputStream();
|
|
||||||
ResourceUtils.readBinaryFileToOutputStream(
|
|
||||||
context, R.raw.mytracks_empty_spreadsheet, os);
|
|
||||||
|
|
||||||
// Get the response
|
|
||||||
// TODO: The following is a horrible ugly hack.
|
|
||||||
// Hopefully we can retire it when there is a proper gdata api.
|
|
||||||
BufferedReader rd = null;
|
|
||||||
String line;
|
|
||||||
StringBuilder resultBuilder = new StringBuilder();
|
|
||||||
try {
|
|
||||||
rd = new BufferedReader(new InputStreamReader(conn.getInputStream()));
|
|
||||||
while ((line = rd.readLine()) != null) {
|
|
||||||
resultBuilder.append(line);
|
|
||||||
}
|
|
||||||
os.close();
|
|
||||||
rd.close();
|
|
||||||
} catch (FileNotFoundException e) {
|
|
||||||
// The GData API sometimes throws an error, even though creation of
|
|
||||||
// the document succeeded. In that case let's just return. The caller
|
|
||||||
// then needs to check if the doc actually exists.
|
|
||||||
return;
|
|
||||||
} finally {
|
|
||||||
os.close();
|
|
||||||
if (rd != null) {
|
|
||||||
rd.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String result = resultBuilder.toString();
|
|
||||||
// Try to find the id.
|
|
||||||
int idTagIndex = result.indexOf("<id>");
|
|
||||||
if (idTagIndex == -1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
int idTagCloseIndex = result.indexOf("</id>", idTagIndex);
|
|
||||||
if (idTagCloseIndex == -1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
int idStringStart = result.indexOf(DOCS_SPREADSHEET_URL, idTagIndex);
|
|
||||||
if (idStringStart == -1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
String id = result.substring(
|
|
||||||
idStringStart + DOCS_SPREADSHEET_URL.length(), idTagCloseIndex);
|
|
||||||
Log.i(Constants.TAG, "Created new spreadsheet: " + id);
|
|
||||||
idSaver.set(id);
|
|
||||||
}});
|
|
||||||
|
|
||||||
if (!success) {
|
|
||||||
throw newIOException(docListWrapper,
|
|
||||||
"Failed to create new spreadsheet.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return idSaver.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve the ID of a spreadsheet with the given name.
|
|
||||||
*
|
|
||||||
* @param docListWrapper The GData handle for the Document List service.
|
|
||||||
* @param title The name of the spreadsheet whose ID is to be retrieved.
|
|
||||||
* @return The spreadsheet ID, if it can be retrieved. {@code null} will
|
|
||||||
* be returned if no spreadsheet exists by the given name.
|
|
||||||
* @throws IOException If an error occurs during the GData request.
|
|
||||||
*/
|
|
||||||
public String requestSpreadsheetId(final GDataWrapper<GDataServiceClient> docListWrapper,
|
|
||||||
final String title) throws IOException {
|
|
||||||
final AtomicReference<String> idSaver = new AtomicReference<String>();
|
|
||||||
|
|
||||||
boolean result = docListWrapper.runQuery(new QueryFunction<GDataServiceClient>() {
|
|
||||||
@Override
|
|
||||||
public void query(GDataServiceClient client)
|
|
||||||
throws IOException, GDataWrapper.ParseException, GDataWrapper.HttpException,
|
|
||||||
GDataWrapper.AuthenticationException {
|
|
||||||
GDataParser listParser;
|
|
||||||
try {
|
|
||||||
listParser = client.getParserForFeed(Entry.class,
|
|
||||||
DOCS_MY_SPREADSHEETS_FEED_URL,
|
|
||||||
docListWrapper.getAuthManager().getAuthToken());
|
|
||||||
listParser.init();
|
|
||||||
|
|
||||||
while (listParser.hasMoreData()) {
|
|
||||||
Entry entry = listParser.readNextEntry(null);
|
|
||||||
String entryTitle = entry.getTitle();
|
|
||||||
Log.i(Constants.TAG, "Found docs entry: " + entryTitle);
|
|
||||||
if (entryTitle.equals(title)) {
|
|
||||||
String entryId = entry.getId();
|
|
||||||
int lastSlash = entryId.lastIndexOf('/');
|
|
||||||
idSaver.set(entryId.substring(lastSlash + 15));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (ParseException e) {
|
|
||||||
throw new GDataWrapper.ParseException(e);
|
|
||||||
} catch (HttpException e) {
|
|
||||||
throw new GDataWrapper.HttpException(e.getStatusCode(), e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!result) {
|
|
||||||
throw newIOException(docListWrapper,
|
|
||||||
"Failed to retrieve spreadsheet list.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return idSaver.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve the ID of the first worksheet in the named spreadsheet.
|
|
||||||
*
|
|
||||||
* @param trixWrapper The GData handle for the spreadsheet service.
|
|
||||||
* @param spreadsheetId The GData ID for the given spreadsheet.
|
|
||||||
* @return The worksheet ID, if it can be retrieved. {@code null} will be
|
|
||||||
* returned if the GData request returns without error, but without an
|
|
||||||
* ID.
|
|
||||||
* @throws IOException If an error occurs during the GData request.
|
|
||||||
*/
|
|
||||||
public String getWorksheetId(final GDataWrapper<GDataServiceClient> trixWrapper,
|
|
||||||
final String spreadsheetId) throws IOException {
|
|
||||||
final AtomicReference<String> idSaver = new AtomicReference<String>();
|
|
||||||
|
|
||||||
boolean result = trixWrapper.runQuery(new QueryFunction<GDataServiceClient>() {
|
|
||||||
@Override
|
|
||||||
public void query(GDataServiceClient client)
|
|
||||||
throws GDataWrapper.AuthenticationException, IOException, GDataWrapper.ParseException, GDataWrapper.HttpException {
|
|
||||||
String uri = String.format(DOCS_WORKSHEETS_URL_FORMAT, spreadsheetId);
|
|
||||||
GDataParser sheetParser;
|
|
||||||
try {
|
|
||||||
sheetParser = ((SpreadsheetsClient) client).getParserForWorksheetsFeed(uri,
|
|
||||||
trixWrapper.getAuthManager().getAuthToken());
|
|
||||||
sheetParser.init();
|
|
||||||
if (!sheetParser.hasMoreData()) {
|
|
||||||
Log.i(Constants.TAG, "Found no worksheets");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Grab the first worksheet.
|
|
||||||
WorksheetEntry worksheetEntry =
|
|
||||||
(WorksheetEntry) sheetParser.readNextEntry(new WorksheetEntry());
|
|
||||||
|
|
||||||
int lastSlash = worksheetEntry.getId().lastIndexOf('/');
|
|
||||||
idSaver.set(worksheetEntry.getId().substring(lastSlash + 1));
|
|
||||||
} catch (ParseException e) {
|
|
||||||
throw new GDataWrapper.ParseException(e);
|
|
||||||
} catch (AuthenticationException e) {
|
|
||||||
throw new GDataWrapper.AuthenticationException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!result) {
|
|
||||||
throw newIOException(trixWrapper, "Failed to retrieve worksheet ID.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return idSaver.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a row to a worksheet containing the stats for a given track.
|
|
||||||
*
|
|
||||||
* @param context The context associated with this request.
|
|
||||||
* @param trixAuth The GData authorization for the spreadsheet service.
|
|
||||||
* @param spreadsheetId The spreadsheet to be modified.
|
|
||||||
* @param worksheetId The worksheet to be modified.
|
|
||||||
* @param track The track whose stats are to be written.
|
|
||||||
* @param metricUnits True if metric units are to be used. If false,
|
|
||||||
* imperial units will be used.
|
|
||||||
* @throws IOException If an error occurs while updating the worksheet.
|
|
||||||
*/
|
|
||||||
public void addTrackRow(Context context, AuthManager trixAuth,
|
|
||||||
String spreadsheetId, String worksheetId, Track track,
|
|
||||||
boolean metricUnits) throws IOException {
|
|
||||||
String worksheetUri = String.format(DOCS_SPREADSHEET_URL_FORMAT,
|
|
||||||
spreadsheetId, worksheetId);
|
|
||||||
TripStatistics stats = track.getStatistics();
|
|
||||||
|
|
||||||
String distanceUnit = context.getString(metricUnits ?
|
|
||||||
R.string.unit_kilometer : R.string.unit_mile);
|
|
||||||
String speedUnit = context.getString(metricUnits ?
|
|
||||||
R.string.unit_kilometer_per_hour : R.string.unit_mile_per_hour);
|
|
||||||
String elevationUnit = context.getString(metricUnits ?
|
|
||||||
R.string.unit_meter : R.string.unit_feet);
|
|
||||||
|
|
||||||
// Prepare the Post-Text we are going to send.
|
|
||||||
DocsTagBuilder tagBuilder = new DocsTagBuilder(metricUnits)
|
|
||||||
.append("name", track.getName())
|
|
||||||
.append("description", track.getDescription())
|
|
||||||
.append("date", StringUtils.formatDateTime(context, stats.getStartTime()))
|
|
||||||
.append("totaltime", StringUtils.formatElapsedTime(stats.getTotalTime()))
|
|
||||||
.append("movingtime", StringUtils.formatElapsedTime(stats.getMovingTime()))
|
|
||||||
.appendLargeUnits("distance", stats.getTotalDistance() / 1000)
|
|
||||||
.append("distanceunit", distanceUnit)
|
|
||||||
.appendLargeUnits("averagespeed", stats.getAverageSpeed() * 3.6)
|
|
||||||
.appendLargeUnits("averagemovingspeed",
|
|
||||||
stats.getAverageMovingSpeed() * 3.6)
|
|
||||||
.appendLargeUnits("maxspeed", stats.getMaxSpeed() * 3.6)
|
|
||||||
.append("speedunit", speedUnit)
|
|
||||||
.appendSmallUnits("elevationgain", stats.getTotalElevationGain())
|
|
||||||
.appendSmallUnits("minelevation", stats.getMinElevation())
|
|
||||||
.appendSmallUnits("maxelevation", stats.getMaxElevation())
|
|
||||||
.append("elevationunit", elevationUnit);
|
|
||||||
|
|
||||||
if (track.getMapId().length() > 0) {
|
|
||||||
tagBuilder.append("map", String.format("%s?msa=0&msid=%s",
|
|
||||||
Constants.MAPSHOP_BASE_URL, track.getMapId()));
|
|
||||||
}
|
|
||||||
|
|
||||||
String postText = new StringBuilder()
|
|
||||||
.append("<entry xmlns='http://www.w3.org/2005/Atom' "
|
|
||||||
+ "xmlns:gsx='http://schemas.google.com/spreadsheets/"
|
|
||||||
+ "2006/extended'>")
|
|
||||||
.append(tagBuilder.build())
|
|
||||||
.append("</entry>")
|
|
||||||
.toString();
|
|
||||||
|
|
||||||
Log.i(Constants.TAG,
|
|
||||||
"Inserting at: " + spreadsheetId + " => " + worksheetUri);
|
|
||||||
|
|
||||||
Log.i(Constants.TAG, postText);
|
|
||||||
|
|
||||||
writeRowData(trixAuth, worksheetUri, postText);
|
|
||||||
|
|
||||||
Log.i(Constants.TAG, "Post finished.");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Writes spreadsheet row data to the indicated worksheet.
|
|
||||||
*
|
|
||||||
* @param trixAuth The GData authorization for the spreadsheet service.
|
|
||||||
* @param worksheetUri The URI of the worksheet to be altered.
|
|
||||||
* @param postText The XML tags describing the change to be made.
|
|
||||||
* @throws IOException Thrown if an error occurs during the write.
|
|
||||||
*/
|
|
||||||
protected void writeRowData(AuthManager trixAuth, String worksheetUri,
|
|
||||||
String postText) throws IOException {
|
|
||||||
// No need for a wrapper because we know that the authorization was good
|
|
||||||
// enough to get this far.
|
|
||||||
URL url = new URL(worksheetUri);
|
|
||||||
URLConnection conn = url.openConnection();
|
|
||||||
conn.addRequestProperty(CONTENT_TYPE_PARAM, ATOM_FEED_MIME_TYPE);
|
|
||||||
conn.addRequestProperty("Authorization",
|
|
||||||
"GoogleLogin auth=" + trixAuth.getAuthToken());
|
|
||||||
conn.setDoOutput(true);
|
|
||||||
OutputStreamWriter wr = new OutputStreamWriter(conn.getOutputStream());
|
|
||||||
wr.write(postText);
|
|
||||||
wr.flush();
|
|
||||||
|
|
||||||
// Get the response.
|
|
||||||
// TODO: Should we parse the response, rather than simply throwing it away?
|
|
||||||
BufferedReader rd =
|
|
||||||
new BufferedReader(new InputStreamReader(conn.getInputStream()));
|
|
||||||
String line;
|
|
||||||
while ((line = rd.readLine()) != null) {
|
|
||||||
// Process line.
|
|
||||||
Log.i(Constants.TAG, "r: " + line);
|
|
||||||
}
|
|
||||||
wr.close();
|
|
||||||
rd.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static IOException newIOException(GDataWrapper<GDataServiceClient> wrapper,
|
|
||||||
String message) {
|
|
||||||
return new IOException(String.format("%s: %d: %s", message,
|
|
||||||
wrapper.getErrorType(), wrapper.getErrorMessage()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.NumberFormat;
|
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* <p>This class builds a string of XML tags used to talk to Docs using GData.
|
|
||||||
*
|
|
||||||
* <p>Sample Usage:
|
|
||||||
*
|
|
||||||
* <code>
|
|
||||||
* String tags = new DocsTagBuilder(false)
|
|
||||||
* .append("tagName", "tagValue")
|
|
||||||
* .appendLargeUnits("bigTagName", 1.0)
|
|
||||||
* .build();
|
|
||||||
* </code>
|
|
||||||
*
|
|
||||||
* <p>results in:
|
|
||||||
*
|
|
||||||
* <code>
|
|
||||||
* <gsx:tagName><![CDATA[tagValue]]></gsx:tagName>
|
|
||||||
* <gsx:bigTagName><![CDATA[1.00]]></gsx:bigTagName>
|
|
||||||
* </code>
|
|
||||||
*
|
|
||||||
* @author Matthew Simmons
|
|
||||||
*/
|
|
||||||
class DocsTagBuilder {
|
|
||||||
|
|
||||||
// Google Docs can only parse numbers in the English locale.
|
|
||||||
private static final NumberFormat LARGE_UNIT_FORMAT = NumberFormat.getNumberInstance(
|
|
||||||
Locale.ENGLISH);
|
|
||||||
private static final NumberFormat SMALL_UNIT_FORMAT = NumberFormat.getIntegerInstance(
|
|
||||||
Locale.ENGLISH);
|
|
||||||
|
|
||||||
static {
|
|
||||||
LARGE_UNIT_FORMAT.setMaximumFractionDigits(2);
|
|
||||||
LARGE_UNIT_FORMAT.setMinimumFractionDigits(2);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected final boolean metricUnits;
|
|
||||||
protected final StringBuilder stringBuilder;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param metricUnits True if metric units are to be used. If false,
|
|
||||||
* imperial units will be used.
|
|
||||||
*/
|
|
||||||
DocsTagBuilder(boolean metricUnits) {
|
|
||||||
this.metricUnits = metricUnits;
|
|
||||||
this.stringBuilder = new StringBuilder();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Appends a tag containing a string value */
|
|
||||||
DocsTagBuilder append(String name, String value) {
|
|
||||||
appendTag(name, value);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Appends a tag containing a numeric value. The value will be formatted
|
|
||||||
* according to the large distance of the specified measurement system (i.e.
|
|
||||||
* kilometers or miles).
|
|
||||||
*
|
|
||||||
* @param name The tag name.
|
|
||||||
* @param d The value to be formatted, in kilometers.
|
|
||||||
*/
|
|
||||||
DocsTagBuilder appendLargeUnits(String name, double d) {
|
|
||||||
double value = metricUnits ? d : (d * UnitConversions.KM_TO_MI);
|
|
||||||
appendTag(name, LARGE_UNIT_FORMAT.format(value));
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Appends a tag containing a numeric value. The value will be formatted
|
|
||||||
* according to the small distance of the specified measurement system (i.e.
|
|
||||||
* meters or feet).
|
|
||||||
*
|
|
||||||
* @param name The tag name.
|
|
||||||
* @param d The value to be formatted, in meters.
|
|
||||||
*/
|
|
||||||
DocsTagBuilder appendSmallUnits(String name, double d) {
|
|
||||||
double value = metricUnits ? d : (d * UnitConversions.M_TO_FT);
|
|
||||||
appendTag(name, SMALL_UNIT_FORMAT.format(value));
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns a string containing all tags which have been added. */
|
|
||||||
String build() {
|
|
||||||
return stringBuilder.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void appendTag(String name, String value) {
|
|
||||||
stringBuilder.append("<gsx:")
|
|
||||||
.append(name)
|
|
||||||
.append(">")
|
|
||||||
.append(StringUtils.stringAsCData(value))
|
|
||||||
.append("</gsx:")
|
|
||||||
.append(name)
|
|
||||||
.append(">");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012 Google Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||||
|
* use this file except in compliance with the License. You may obtain a copy of
|
||||||
|
* the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations under
|
||||||
|
* the License.
|
||||||
|
*/
|
||||||
|
package com.google.android.apps.mytracks.io.docs;
|
||||||
|
|
||||||
|
import com.google.android.apps.mytracks.io.sendtogoogle.SendRequest;
|
||||||
|
import com.google.android.apps.mytracks.io.sendtogoogle.UploadResultActivity;
|
||||||
|
import com.google.android.maps.mytracks.R;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.app.Dialog;
|
||||||
|
import android.app.ProgressDialog;
|
||||||
|
import android.content.DialogInterface;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Bundle;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An activity to send a track to Google Docs.
|
||||||
|
*
|
||||||
|
* @author jshih@google.com (Jimmy Shih)
|
||||||
|
*/
|
||||||
|
public class SendDocsActivity extends Activity {
|
||||||
|
|
||||||
|
private static final int PROGRESS_DIALOG = 1;
|
||||||
|
|
||||||
|
private SendRequest sendRequest;
|
||||||
|
private SendDocsAsyncTask asyncTask;
|
||||||
|
private ProgressDialog progressDialog;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
sendRequest = getIntent().getParcelableExtra(SendRequest.SEND_REQUEST_KEY);
|
||||||
|
|
||||||
|
Object retained = getLastNonConfigurationInstance();
|
||||||
|
if (retained instanceof SendDocsAsyncTask) {
|
||||||
|
asyncTask = (SendDocsAsyncTask) retained;
|
||||||
|
asyncTask.setActivity(this);
|
||||||
|
} else {
|
||||||
|
asyncTask = new SendDocsAsyncTask(this, sendRequest.getTrackId(), sendRequest.getAccount());
|
||||||
|
asyncTask.execute();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object onRetainNonConfigurationInstance() {
|
||||||
|
asyncTask.setActivity(null);
|
||||||
|
return asyncTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Dialog onCreateDialog(int id) {
|
||||||
|
switch (id) {
|
||||||
|
case PROGRESS_DIALOG:
|
||||||
|
progressDialog = new ProgressDialog(this);
|
||||||
|
progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
|
||||||
|
progressDialog.setIcon(android.R.drawable.ic_dialog_info);
|
||||||
|
progressDialog.setTitle(getString(
|
||||||
|
R.string.send_google_progress_title, getString(R.string.send_google_docs)));
|
||||||
|
progressDialog.setMax(100);
|
||||||
|
progressDialog.setProgress(0);
|
||||||
|
progressDialog.setCancelable(true);
|
||||||
|
progressDialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
|
||||||
|
@Override
|
||||||
|
public void onCancel(DialogInterface dialog) {
|
||||||
|
asyncTask.cancel(true);
|
||||||
|
startNextActivity(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return progressDialog;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invokes when the associated AsyncTask completes.
|
||||||
|
*
|
||||||
|
* @param success true if success
|
||||||
|
*/
|
||||||
|
public void onAsyncTaskCompleted(boolean success) {
|
||||||
|
startNextActivity(success);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows the progress dialog.
|
||||||
|
*/
|
||||||
|
public void showProgressDialog() {
|
||||||
|
showDialog(PROGRESS_DIALOG);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the progress dialog value.
|
||||||
|
*/
|
||||||
|
public void setProgressDialogValue(int value) {
|
||||||
|
if (progressDialog != null) {
|
||||||
|
progressDialog.setProgress(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the next activity.
|
||||||
|
*
|
||||||
|
* @param success true if sendDocs is success
|
||||||
|
*/
|
||||||
|
private void startNextActivity(boolean success) {
|
||||||
|
sendRequest.setDocsSuccess(success);
|
||||||
|
Intent intent = new Intent(this, UploadResultActivity.class)
|
||||||
|
.putExtra(SendRequest.SEND_REQUEST_KEY, sendRequest);
|
||||||
|
startActivity(intent);
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,389 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012 Google Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||||
|
* use this file except in compliance with the License. You may obtain a copy of
|
||||||
|
* the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations under
|
||||||
|
* the License.
|
||||||
|
*/
|
||||||
|
package com.google.android.apps.mytracks.io.docs;
|
||||||
|
|
||||||
|
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
|
||||||
|
import com.google.android.apps.mytracks.content.Track;
|
||||||
|
import com.google.android.apps.mytracks.io.gdata.GDataClientFactory;
|
||||||
|
import com.google.android.apps.mytracks.io.gdata.docs.DocumentsClient;
|
||||||
|
import com.google.android.apps.mytracks.io.gdata.docs.SpreadsheetsClient;
|
||||||
|
import com.google.android.apps.mytracks.io.gdata.docs.XmlDocsGDataParserFactory;
|
||||||
|
import com.google.android.common.gdata.AndroidXmlParserFactory;
|
||||||
|
import com.google.android.maps.mytracks.R;
|
||||||
|
import com.google.wireless.gdata.client.GDataClient;
|
||||||
|
import com.google.wireless.gdata.client.HttpException;
|
||||||
|
import com.google.wireless.gdata.parser.ParseException;
|
||||||
|
import com.google.wireless.gdata2.client.AuthenticationException;
|
||||||
|
|
||||||
|
import android.accounts.Account;
|
||||||
|
import android.accounts.AccountManager;
|
||||||
|
import android.accounts.AuthenticatorException;
|
||||||
|
import android.accounts.OperationCanceledException;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.AsyncTask;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AsyncTask to send a track to Google Docs.
|
||||||
|
*
|
||||||
|
* @author jshih@google.com (Jimmy Shih)
|
||||||
|
*/
|
||||||
|
public class SendDocsAsyncTask extends AsyncTask<Void, Integer, Boolean> {
|
||||||
|
private static final int PROGRESS_GET_SPREADSHEET_ID = 0;
|
||||||
|
private static final int PROGRESS_CREATE_SPREADSHEET = 25;
|
||||||
|
private static final int PROGRESS_GET_WORKSHEET_ID = 50;
|
||||||
|
private static final int PROGRESS_ADD_TRACK_INFO = 75;
|
||||||
|
private static final int PROGRESS_COMPLETE = 100;
|
||||||
|
|
||||||
|
private static final String TAG = SendDocsAsyncTask.class.getSimpleName();
|
||||||
|
|
||||||
|
private SendDocsActivity activity;
|
||||||
|
private final long trackId;
|
||||||
|
private final Account account;
|
||||||
|
private final Context context;
|
||||||
|
private final MyTracksProviderUtils myTracksProviderUtils;
|
||||||
|
private final GDataClient gDataClient;
|
||||||
|
private final DocumentsClient documentsClient;
|
||||||
|
private final SpreadsheetsClient spreadsheetsClient;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if can retry sending to Google Docs.
|
||||||
|
*/
|
||||||
|
private boolean canRetry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if the AsyncTask has completed.
|
||||||
|
*/
|
||||||
|
private boolean completed;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if the result is success.
|
||||||
|
*/
|
||||||
|
private boolean success;
|
||||||
|
|
||||||
|
// The following variables are for per upload states
|
||||||
|
private String documentsAuthToken;
|
||||||
|
private String spreadsheetsAuthToken;
|
||||||
|
private String spreadsheetId;
|
||||||
|
private String worksheetId;
|
||||||
|
|
||||||
|
public SendDocsAsyncTask(SendDocsActivity activity, long trackId, Account account) {
|
||||||
|
this.activity = activity;
|
||||||
|
this.trackId = trackId;
|
||||||
|
this.account = account;
|
||||||
|
|
||||||
|
context = activity.getApplicationContext();
|
||||||
|
myTracksProviderUtils = MyTracksProviderUtils.Factory.get(context);
|
||||||
|
gDataClient = GDataClientFactory.getGDataClient(context);
|
||||||
|
documentsClient = new DocumentsClient(
|
||||||
|
gDataClient, new XmlDocsGDataParserFactory(new AndroidXmlParserFactory()));
|
||||||
|
spreadsheetsClient = new SpreadsheetsClient(
|
||||||
|
gDataClient, new XmlDocsGDataParserFactory(new AndroidXmlParserFactory()));
|
||||||
|
|
||||||
|
canRetry = true;
|
||||||
|
completed = false;
|
||||||
|
success = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the activity associated with this AyncTask.
|
||||||
|
*
|
||||||
|
* @param activity the activity.
|
||||||
|
*/
|
||||||
|
public void setActivity(SendDocsActivity activity) {
|
||||||
|
this.activity = activity;
|
||||||
|
if (completed && activity != null) {
|
||||||
|
activity.onAsyncTaskCompleted(success);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPreExecute() {
|
||||||
|
activity.showProgressDialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Boolean doInBackground(Void... params) {
|
||||||
|
return doUpload();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onProgressUpdate(Integer... values) {
|
||||||
|
if (activity != null) {
|
||||||
|
activity.setProgressDialogValue(values[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPostExecute(Boolean result) {
|
||||||
|
closeClient();
|
||||||
|
success = result;
|
||||||
|
completed = true;
|
||||||
|
if (activity != null) {
|
||||||
|
activity.onAsyncTaskCompleted(success);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCancelled() {
|
||||||
|
closeClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void closeClient() {
|
||||||
|
if (gDataClient != null) {
|
||||||
|
gDataClient.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uploads a track to Google Docs.
|
||||||
|
*
|
||||||
|
* @return true if success.
|
||||||
|
*/
|
||||||
|
private boolean doUpload() {
|
||||||
|
// Reset the per upload states
|
||||||
|
documentsAuthToken = null;
|
||||||
|
spreadsheetsAuthToken = null;
|
||||||
|
spreadsheetId = null;
|
||||||
|
worksheetId = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
documentsAuthToken = AccountManager.get(context).blockingGetAuthToken(
|
||||||
|
account, documentsClient.getServiceName(), false);
|
||||||
|
spreadsheetsAuthToken = AccountManager.get(context).blockingGetAuthToken(
|
||||||
|
account, spreadsheetsClient.getServiceName(), false);
|
||||||
|
} catch (OperationCanceledException e) {
|
||||||
|
Log.d(TAG, "Unable to get auth token", e);
|
||||||
|
return retryUpload();
|
||||||
|
} catch (AuthenticatorException e) {
|
||||||
|
Log.d(TAG, "Unable to get auth token", e);
|
||||||
|
return retryUpload();
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.d(TAG, "Unable to get auth token", e);
|
||||||
|
return retryUpload();
|
||||||
|
}
|
||||||
|
|
||||||
|
Track track = myTracksProviderUtils.getTrack(trackId);
|
||||||
|
if (track == null) {
|
||||||
|
Log.d(TAG, "Track is null");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
String title = context.getString(R.string.my_tracks_app_name);
|
||||||
|
if (track.getCategory() != null && !track.getCategory().equals("")) {
|
||||||
|
title += "-" + track.getCategory();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the spreadsheet ID
|
||||||
|
publishProgress(PROGRESS_GET_SPREADSHEET_ID);
|
||||||
|
if (!fetchSpreadSheetId(title, false)) {
|
||||||
|
return retryUpload();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new spreadsheet if necessary
|
||||||
|
publishProgress(PROGRESS_CREATE_SPREADSHEET);
|
||||||
|
if (spreadsheetId == null) {
|
||||||
|
if (!createSpreadSheet(title)) {
|
||||||
|
Log.d(TAG, "Unable to create a new spreadsheet");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The previous creation might have succeeded even though GData
|
||||||
|
// reported an error. Seems to be a know bug.
|
||||||
|
// See http://code.google.com/p/gdata-issues/issues/detail?id=929
|
||||||
|
// Try to find the created spreadsheet.
|
||||||
|
if (spreadsheetId == null) {
|
||||||
|
if (!fetchSpreadSheetId(title, true)) {
|
||||||
|
Log.d(TAG, "Unable to check if the new spreadsheet is created");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (spreadsheetId == null) {
|
||||||
|
Log.d(TAG, "Unable to create a new spreadsheet");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the worksheet ID
|
||||||
|
publishProgress(PROGRESS_GET_WORKSHEET_ID);
|
||||||
|
if (!fetchWorksheetId()) {
|
||||||
|
return retryUpload();
|
||||||
|
}
|
||||||
|
if (worksheetId == null) {
|
||||||
|
Log.d(TAG, "Unable to get a worksheet ID");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the track info
|
||||||
|
publishProgress(PROGRESS_ADD_TRACK_INFO);
|
||||||
|
if (!addTrackInfo(track)) {
|
||||||
|
Log.d(TAG, "Unable to add track info");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
publishProgress(PROGRESS_COMPLETE);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the spreadsheet id. Sets the instance variable
|
||||||
|
* {@link SendDocsAsyncTask#spreadsheetId}.
|
||||||
|
*
|
||||||
|
* @param title the spreadsheet title
|
||||||
|
* @param waitFirst wait before checking
|
||||||
|
* @return true if completes.
|
||||||
|
*/
|
||||||
|
private boolean fetchSpreadSheetId(String title, boolean waitFirst) {
|
||||||
|
if (isCancelled()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (waitFirst) {
|
||||||
|
try {
|
||||||
|
Thread.sleep(5000);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Log.d(TAG, "Unable to wait", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
spreadsheetId = SendDocsUtils.getSpreadsheetId(title, documentsClient, documentsAuthToken);
|
||||||
|
} catch (ParseException e) {
|
||||||
|
Log.d(TAG, "Unable to fetch spreadsheet ID", e);
|
||||||
|
return false;
|
||||||
|
} catch (HttpException e) {
|
||||||
|
Log.d(TAG, "Unable to fetch spreadsheet ID", e);
|
||||||
|
return false;
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.d(TAG, "Unable to fetch spreadsheet ID", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (spreadsheetId == null) {
|
||||||
|
// Waiting a few seconds and trying again. Maybe the server just had a
|
||||||
|
// hickup (unfortunately that happens quite a lot...).
|
||||||
|
try {
|
||||||
|
Thread.sleep(5000);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Log.d(TAG, "Unable to wait", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
spreadsheetId = SendDocsUtils.getSpreadsheetId(title, documentsClient, documentsAuthToken);
|
||||||
|
} catch (ParseException e) {
|
||||||
|
Log.d(TAG, "Unable to fetch spreadsheet ID", e);
|
||||||
|
return false;
|
||||||
|
} catch (HttpException e) {
|
||||||
|
Log.d(TAG, "Unable to fetch spreadsheet ID", e);
|
||||||
|
return false;
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.d(TAG, "Unable to fetch spreadsheet ID", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a spreadsheet. If successful, sets the instance variable
|
||||||
|
* {@link SendDocsAsyncTask#spreadsheetId}.
|
||||||
|
*
|
||||||
|
* @param spreadsheetTitle the spreadsheet title
|
||||||
|
* @return true if completes.
|
||||||
|
*/
|
||||||
|
private boolean createSpreadSheet(String spreadsheetTitle) {
|
||||||
|
if (isCancelled()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
spreadsheetId = SendDocsUtils.createSpreadsheet(spreadsheetTitle, documentsAuthToken, context);
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.d(TAG, "Unable to create spreadsheet", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the worksheet ID. Sets the instance variable
|
||||||
|
* {@link SendDocsAsyncTask#worksheetId}.
|
||||||
|
*
|
||||||
|
* @return true if completes.
|
||||||
|
*/
|
||||||
|
private boolean fetchWorksheetId() {
|
||||||
|
if (isCancelled()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
worksheetId = SendDocsUtils.getWorksheetId(
|
||||||
|
spreadsheetId, spreadsheetsClient, spreadsheetsAuthToken);
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.d(TAG, "Unable to fetch worksheet ID", e);
|
||||||
|
return false;
|
||||||
|
} catch (AuthenticationException e) {
|
||||||
|
Log.d(TAG, "Unable to fetch worksheet ID", e);
|
||||||
|
return false;
|
||||||
|
} catch (ParseException e) {
|
||||||
|
Log.d(TAG, "Unable to fetch worksheet ID", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds track info to a worksheet.
|
||||||
|
*
|
||||||
|
* @param track the track
|
||||||
|
* @return true if completes.
|
||||||
|
*/
|
||||||
|
private boolean addTrackInfo(Track track) {
|
||||||
|
if (isCancelled()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
SendDocsUtils.addTrackInfo(track, spreadsheetId, worksheetId, spreadsheetsAuthToken, context);
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.d(TAG, "Unable to add track info", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retries upload. Invalidates the authToken. If can retry, invokes
|
||||||
|
* {@link SendDocsAsyncTask#doUpload()}. Returns false if cannot retry.
|
||||||
|
*/
|
||||||
|
private boolean retryUpload() {
|
||||||
|
if (isCancelled()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
AccountManager.get(context).invalidateAuthToken(
|
||||||
|
documentsClient.getServiceName(), documentsAuthToken);
|
||||||
|
AccountManager.get(context).invalidateAuthToken(
|
||||||
|
spreadsheetsClient.getServiceName(), spreadsheetsAuthToken);
|
||||||
|
if (canRetry) {
|
||||||
|
canRetry = false;
|
||||||
|
return doUpload();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,410 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012 Google Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||||
|
* use this file except in compliance with the License. You may obtain a copy of
|
||||||
|
* the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations under
|
||||||
|
* the License.
|
||||||
|
*/
|
||||||
|
package com.google.android.apps.mytracks.io.docs;
|
||||||
|
|
||||||
|
import com.google.android.apps.mytracks.Constants;
|
||||||
|
import com.google.android.apps.mytracks.content.Track;
|
||||||
|
import com.google.android.apps.mytracks.io.gdata.docs.DocumentsClient;
|
||||||
|
import com.google.android.apps.mytracks.io.gdata.docs.SpreadsheetsClient;
|
||||||
|
import com.google.android.apps.mytracks.io.gdata.docs.SpreadsheetsClient.WorksheetEntry;
|
||||||
|
import com.google.android.apps.mytracks.stats.TripStatistics;
|
||||||
|
import com.google.android.apps.mytracks.util.ResourceUtils;
|
||||||
|
import com.google.android.apps.mytracks.util.StringUtils;
|
||||||
|
import com.google.android.apps.mytracks.util.UnitConversions;
|
||||||
|
import com.google.android.maps.mytracks.R;
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
import com.google.wireless.gdata.client.HttpException;
|
||||||
|
import com.google.wireless.gdata.data.Entry;
|
||||||
|
import com.google.wireless.gdata.parser.GDataParser;
|
||||||
|
import com.google.wireless.gdata.parser.ParseException;
|
||||||
|
import com.google.wireless.gdata2.client.AuthenticationException;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.FileNotFoundException;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.io.OutputStreamWriter;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.net.URLConnection;
|
||||||
|
import java.net.URLEncoder;
|
||||||
|
import java.text.NumberFormat;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utilities for sending a track to Google Docs.
|
||||||
|
*
|
||||||
|
* @author Sandor Dornbush
|
||||||
|
* @author Matthew Simmons
|
||||||
|
*/
|
||||||
|
public class SendDocsUtils {
|
||||||
|
private static final String GET_SPREADSHEET_BY_TITLE_URI =
|
||||||
|
"https://docs.google.com/feeds/documents/private/full?"
|
||||||
|
+ "category=mine,spreadsheet&title=%s&title-exact=true";
|
||||||
|
private static final String CREATE_SPREADSHEET_URI =
|
||||||
|
"https://docs.google.com/feeds/documents/private/full";
|
||||||
|
private static final String GET_WORKSHEETS_URI =
|
||||||
|
"https://spreadsheets.google.com/feeds/worksheets/%s/private/full";
|
||||||
|
private static final String GET_WORKSHEET_URI =
|
||||||
|
"https://spreadsheets.google.com/feeds/list/%s/%s/private/full";
|
||||||
|
|
||||||
|
private static final String SPREADSHEET_ID_PREFIX =
|
||||||
|
"https://docs.google.com/feeds/documents/private/full/spreadsheet%3A";
|
||||||
|
|
||||||
|
private static final String CONTENT_TYPE = "Content-Type";
|
||||||
|
private static final String ATOM_FEED_MIME_TYPE = "application/atom+xml";
|
||||||
|
private static final String OPENDOCUMENT_SPREADSHEET_MIME_TYPE =
|
||||||
|
"application/x-vnd.oasis.opendocument.spreadsheet";
|
||||||
|
|
||||||
|
private static final String AUTHORIZATION = "Authorization";
|
||||||
|
private static final String AUTHORIZATION_PREFIX = "GoogleLogin auth=";
|
||||||
|
|
||||||
|
private static final String SLUG = "Slug";
|
||||||
|
|
||||||
|
// Google Docs can only parse numbers in the English locale.
|
||||||
|
private static final NumberFormat NUMBER_FORMAT = NumberFormat.getNumberInstance(Locale.ENGLISH);
|
||||||
|
private static final NumberFormat INTEGER_FORMAT = NumberFormat.getIntegerInstance(
|
||||||
|
Locale.ENGLISH);
|
||||||
|
|
||||||
|
static {
|
||||||
|
NUMBER_FORMAT.setMaximumFractionDigits(2);
|
||||||
|
NUMBER_FORMAT.setMinimumFractionDigits(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final String TAG = SendDocsUtils.class.getSimpleName();
|
||||||
|
|
||||||
|
private SendDocsUtils() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the spreadsheet id of a spreadsheet. Returns null if the spreadsheet
|
||||||
|
* doesn't exist.
|
||||||
|
*
|
||||||
|
* @param title the title of the spreadsheet
|
||||||
|
* @param documentsClient the documents client
|
||||||
|
* @param authToken the auth token
|
||||||
|
* @return spreadsheet id or null if it doesn't exist.
|
||||||
|
*/
|
||||||
|
public static String getSpreadsheetId(
|
||||||
|
String title, DocumentsClient documentsClient, String authToken)
|
||||||
|
throws IOException, ParseException, HttpException {
|
||||||
|
GDataParser gDataParser = null;
|
||||||
|
try {
|
||||||
|
String uri = String.format(GET_SPREADSHEET_BY_TITLE_URI, URLEncoder.encode(title));
|
||||||
|
gDataParser = documentsClient.getParserForFeed(Entry.class, uri, authToken);
|
||||||
|
gDataParser.init();
|
||||||
|
|
||||||
|
while (gDataParser.hasMoreData()) {
|
||||||
|
Entry entry = gDataParser.readNextEntry(null);
|
||||||
|
String entryTitle = entry.getTitle();
|
||||||
|
if (entryTitle.equals(title)) {
|
||||||
|
return getEntryId(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
if (gDataParser != null) {
|
||||||
|
gDataParser.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the id from an entry. Returns null if not available.
|
||||||
|
*
|
||||||
|
* @param entry the entry
|
||||||
|
*/
|
||||||
|
@VisibleForTesting
|
||||||
|
static String getEntryId(Entry entry) {
|
||||||
|
String entryId = entry.getId();
|
||||||
|
if (entryId.startsWith(SPREADSHEET_ID_PREFIX)) {
|
||||||
|
return entryId.substring(SPREADSHEET_ID_PREFIX.length());
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new spreadsheet with the given title. Returns the spreadsheet ID
|
||||||
|
* if successful. Returns null otherwise. Note that it is possible that a new
|
||||||
|
* spreadsheet is created, but the returned ID is null.
|
||||||
|
*
|
||||||
|
* @param title the title
|
||||||
|
* @param authToken the auth token
|
||||||
|
* @param context the context
|
||||||
|
*/
|
||||||
|
public static String createSpreadsheet(String title, String authToken, Context context)
|
||||||
|
throws IOException {
|
||||||
|
URL url = new URL(CREATE_SPREADSHEET_URI);
|
||||||
|
URLConnection conn = url.openConnection();
|
||||||
|
conn.addRequestProperty(CONTENT_TYPE, OPENDOCUMENT_SPREADSHEET_MIME_TYPE);
|
||||||
|
conn.addRequestProperty(SLUG, title);
|
||||||
|
conn.addRequestProperty(AUTHORIZATION, AUTHORIZATION_PREFIX + authToken);
|
||||||
|
conn.setDoOutput(true);
|
||||||
|
OutputStream outputStream = conn.getOutputStream();
|
||||||
|
ResourceUtils.readBinaryFileToOutputStream(
|
||||||
|
context, R.raw.mytracks_empty_spreadsheet, outputStream);
|
||||||
|
|
||||||
|
// Get the response
|
||||||
|
BufferedReader bufferedReader = null;
|
||||||
|
StringBuilder resultBuilder = new StringBuilder();
|
||||||
|
try {
|
||||||
|
bufferedReader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
|
||||||
|
String line;
|
||||||
|
while ((line = bufferedReader.readLine()) != null) {
|
||||||
|
resultBuilder.append(line);
|
||||||
|
}
|
||||||
|
} catch (FileNotFoundException e) {
|
||||||
|
// The GData API sometimes throws an error, even though creation of
|
||||||
|
// the document succeeded. In that case let's just return. The caller
|
||||||
|
// then needs to check if the doc actually exists.
|
||||||
|
Log.d(TAG, "Unable to read result after creating a spreadsheet", e);
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
outputStream.close();
|
||||||
|
if (bufferedReader != null) {
|
||||||
|
bufferedReader.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return getNewSpreadsheetId(resultBuilder.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the spreadsheet id from a create spreadsheet result.
|
||||||
|
*
|
||||||
|
* @param result the create spreadsheet result
|
||||||
|
*/
|
||||||
|
@VisibleForTesting
|
||||||
|
static String getNewSpreadsheetId(String result) {
|
||||||
|
int idTagIndex = result.indexOf("<id>");
|
||||||
|
if (idTagIndex == -1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
int idTagCloseIndex = result.indexOf("</id>", idTagIndex);
|
||||||
|
if (idTagCloseIndex == -1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
int idStringStart = result.indexOf(SPREADSHEET_ID_PREFIX, idTagIndex);
|
||||||
|
if (idStringStart == -1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.substring(idStringStart + SPREADSHEET_ID_PREFIX.length(), idTagCloseIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the first worksheet ID of a spreadsheet. Returns null if not
|
||||||
|
* available.
|
||||||
|
*
|
||||||
|
* @param spreadsheetId the spreadsheet ID
|
||||||
|
* @param spreadsheetClient the spreadsheet client
|
||||||
|
* @param authToken the auth token
|
||||||
|
*/
|
||||||
|
public static String getWorksheetId(
|
||||||
|
String spreadsheetId, SpreadsheetsClient spreadsheetClient, String authToken)
|
||||||
|
throws IOException, AuthenticationException, ParseException {
|
||||||
|
GDataParser gDataParser = null;
|
||||||
|
try {
|
||||||
|
String uri = String.format(GET_WORKSHEETS_URI, spreadsheetId);
|
||||||
|
gDataParser = spreadsheetClient.getParserForWorksheetsFeed(uri, authToken);
|
||||||
|
gDataParser.init();
|
||||||
|
if (!gDataParser.hasMoreData()) {
|
||||||
|
Log.d(TAG, "No worksheet");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the first worksheet
|
||||||
|
WorksheetEntry worksheetEntry =
|
||||||
|
(WorksheetEntry) gDataParser.readNextEntry(new WorksheetEntry());
|
||||||
|
return getWorksheetEntryId(worksheetEntry);
|
||||||
|
} finally {
|
||||||
|
if (gDataParser != null) {
|
||||||
|
gDataParser.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the worksheet id from a worksheet entry. Returns null if not available.
|
||||||
|
*
|
||||||
|
* @param entry the worksheet entry
|
||||||
|
*/
|
||||||
|
@VisibleForTesting
|
||||||
|
static String getWorksheetEntryId(WorksheetEntry entry) {
|
||||||
|
String id = entry.getId();
|
||||||
|
int lastSlash = id.lastIndexOf('/');
|
||||||
|
if (lastSlash == -1) {
|
||||||
|
Log.d(TAG, "No id");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return id.substring(lastSlash + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a track's info as a row in a worksheet.
|
||||||
|
*
|
||||||
|
* @param track the track
|
||||||
|
* @param spreadsheetId the spreadsheet ID
|
||||||
|
* @param worksheetId the worksheet ID
|
||||||
|
* @param authToken the auth token
|
||||||
|
* @param context the context
|
||||||
|
*/
|
||||||
|
public static void addTrackInfo(
|
||||||
|
Track track, String spreadsheetId, String worksheetId, String authToken, Context context)
|
||||||
|
throws IOException {
|
||||||
|
String worksheetUri = String.format(GET_WORKSHEET_URI, spreadsheetId, worksheetId);
|
||||||
|
SharedPreferences prefs = context.getSharedPreferences(
|
||||||
|
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
|
||||||
|
boolean metricUnits = prefs.getBoolean(context.getString(R.string.metric_units_key), true);
|
||||||
|
|
||||||
|
addRow(worksheetUri, getRowContent(track, metricUnits, context), authToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the row content containing the track's info.
|
||||||
|
*
|
||||||
|
* @param track the track
|
||||||
|
* @param metricUnits true to use metric
|
||||||
|
* @param context the context
|
||||||
|
*/
|
||||||
|
@VisibleForTesting
|
||||||
|
static String getRowContent(Track track, boolean metricUnits, Context context) {
|
||||||
|
TripStatistics stats = track.getStatistics();
|
||||||
|
|
||||||
|
String distanceUnit = context.getString(
|
||||||
|
metricUnits ? R.string.unit_kilometer : R.string.unit_mile);
|
||||||
|
String speedUnit = context.getString(
|
||||||
|
metricUnits ? R.string.unit_kilometer_per_hour : R.string.unit_mile_per_hour);
|
||||||
|
String elevationUnit = context.getString(
|
||||||
|
metricUnits ? R.string.unit_meter : R.string.unit_feet);
|
||||||
|
|
||||||
|
StringBuilder builder = new StringBuilder().append("<entry xmlns='http://www.w3.org/2005/Atom' "
|
||||||
|
+ "xmlns:gsx='http://schemas.google.com/spreadsheets/2006/extended'>");
|
||||||
|
appendTag(builder, "name", track.getName());
|
||||||
|
appendTag(builder, "description", track.getDescription());
|
||||||
|
appendTag(builder, "date", StringUtils.formatDateTime(context, stats.getStartTime()));
|
||||||
|
appendTag(builder, "totaltime", StringUtils.formatElapsedTime(stats.getTotalTime()));
|
||||||
|
appendTag(builder, "movingtime", StringUtils.formatElapsedTime(stats.getMovingTime()));
|
||||||
|
appendTag(builder, "distance", getDistance(stats.getTotalDistance(), metricUnits));
|
||||||
|
appendTag(builder, "distanceunit", distanceUnit);
|
||||||
|
appendTag(builder, "averagespeed", getSpeed(stats.getAverageSpeed(), metricUnits));
|
||||||
|
appendTag(builder, "averagemovingspeed", getSpeed(stats.getAverageMovingSpeed(), metricUnits));
|
||||||
|
appendTag(builder, "maxspeed", getSpeed(stats.getMaxSpeed(), metricUnits));
|
||||||
|
appendTag(builder, "speedunit", speedUnit);
|
||||||
|
appendTag(builder, "elevationgain", getElevation(stats.getTotalElevationGain(), metricUnits));
|
||||||
|
appendTag(builder, "minelevation", getElevation(stats.getMinElevation(), metricUnits));
|
||||||
|
appendTag(builder, "maxelevation", getElevation(stats.getMaxElevation(), metricUnits));
|
||||||
|
appendTag(builder, "elevationunit", elevationUnit);
|
||||||
|
|
||||||
|
if (track.getMapId().length() > 0) {
|
||||||
|
appendTag(builder, "map",
|
||||||
|
String.format("%s?msa=0&msid=%s", Constants.MAPSHOP_BASE_URL, track.getMapId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.append("</entry>");
|
||||||
|
return builder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appends a name-value pair as a gsx tag to a string builder.
|
||||||
|
*
|
||||||
|
* @param stringBuilder the string builder
|
||||||
|
* @param name the name
|
||||||
|
* @param value the value
|
||||||
|
*/
|
||||||
|
@VisibleForTesting
|
||||||
|
static void appendTag(StringBuilder stringBuilder, String name, String value) {
|
||||||
|
stringBuilder
|
||||||
|
.append("<gsx:")
|
||||||
|
.append(name)
|
||||||
|
.append(">")
|
||||||
|
.append(StringUtils.stringAsCData(value))
|
||||||
|
.append("</gsx:")
|
||||||
|
.append(name)
|
||||||
|
.append(">");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the distance. Performs unit conversion and formatting.
|
||||||
|
*
|
||||||
|
* @param distanceInMeter the distance in meters
|
||||||
|
* @param metricUnits true to use metric
|
||||||
|
*/
|
||||||
|
@VisibleForTesting
|
||||||
|
static final String getDistance(double distanceInMeter, boolean metricUnits) {
|
||||||
|
double distanceInKilometer = distanceInMeter / 1000.0;
|
||||||
|
double distance = metricUnits ? distanceInKilometer
|
||||||
|
: distanceInKilometer * UnitConversions.KM_TO_MI;
|
||||||
|
return NUMBER_FORMAT.format(distance);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the speed. Performs unit conversion and formatting.
|
||||||
|
*
|
||||||
|
* @param speedInMeterPerSecond the speed in meters per second
|
||||||
|
* @param metricUnits true to use metric
|
||||||
|
*/
|
||||||
|
@VisibleForTesting
|
||||||
|
static final String getSpeed(double speedInMeterPerSecond, boolean metricUnits) {
|
||||||
|
double speedInKilometerPerHour = speedInMeterPerSecond * 3.6;
|
||||||
|
double speed = metricUnits ? speedInKilometerPerHour
|
||||||
|
: speedInKilometerPerHour * UnitConversions.KMH_TO_MPH;
|
||||||
|
return NUMBER_FORMAT.format(speed);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the elevation. Performs unit conversion and formatting.
|
||||||
|
*
|
||||||
|
* @param elevationInMeter the elevation value in meters
|
||||||
|
* @param metricUnits true to use metric
|
||||||
|
*/
|
||||||
|
@VisibleForTesting
|
||||||
|
static final String getElevation(double elevationInMeter, boolean metricUnits) {
|
||||||
|
double elevation = metricUnits ? elevationInMeter : elevationInMeter * UnitConversions.M_TO_FT;
|
||||||
|
return INTEGER_FORMAT.format(elevation);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a row to a Google Spreadsheet worksheet.
|
||||||
|
*
|
||||||
|
* @param worksheetUri the worksheet URI
|
||||||
|
* @param rowContent the row content
|
||||||
|
* @param authToken the auth token
|
||||||
|
*/
|
||||||
|
private static final void addRow(String worksheetUri, String rowContent, String authToken)
|
||||||
|
throws IOException {
|
||||||
|
URL url = new URL(worksheetUri);
|
||||||
|
URLConnection conn = url.openConnection();
|
||||||
|
conn.addRequestProperty(CONTENT_TYPE, ATOM_FEED_MIME_TYPE);
|
||||||
|
conn.addRequestProperty(AUTHORIZATION, AUTHORIZATION_PREFIX + authToken);
|
||||||
|
conn.setDoOutput(true);
|
||||||
|
OutputStreamWriter writer = new OutputStreamWriter(conn.getOutputStream());
|
||||||
|
writer.write(rowContent);
|
||||||
|
writer.flush();
|
||||||
|
|
||||||
|
// Get the response
|
||||||
|
BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
|
||||||
|
while ((reader.readLine()) != null) {
|
||||||
|
// Just read till the end
|
||||||
|
}
|
||||||
|
writer.close();
|
||||||
|
reader.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
+149
@@ -0,0 +1,149 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012 Google Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||||
|
* use this file except in compliance with the License. You may obtain a copy of
|
||||||
|
* the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations under
|
||||||
|
* the License.
|
||||||
|
*/
|
||||||
|
package com.google.android.apps.mytracks.io.fusiontables;
|
||||||
|
|
||||||
|
import com.google.android.apps.mytracks.io.docs.SendDocsActivity;
|
||||||
|
import com.google.android.apps.mytracks.io.sendtogoogle.SendRequest;
|
||||||
|
import com.google.android.apps.mytracks.io.sendtogoogle.UploadResultActivity;
|
||||||
|
import com.google.android.maps.mytracks.R;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.app.Dialog;
|
||||||
|
import android.app.ProgressDialog;
|
||||||
|
import android.content.DialogInterface;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Bundle;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An activity to send a track to Google Fusion Tables.
|
||||||
|
* <p>
|
||||||
|
* The activity gets recreated when the screen rotates. To support the activity
|
||||||
|
* displaying a progress dialog, we do the following:
|
||||||
|
* <ul>
|
||||||
|
* <li>use one instance of an AyncTask to send the track</li>
|
||||||
|
* <li>save that instance as the last non configuration instance of the activity
|
||||||
|
* </li>
|
||||||
|
* <li>when a new activity is created, pass the activity to the AsyncTask so
|
||||||
|
* that the AsyncTask can update the progress dialog of the activity</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @author jshih@google.com (Jimmy Shih)
|
||||||
|
*/
|
||||||
|
public class SendFusionTablesActivity extends Activity {
|
||||||
|
|
||||||
|
private static final int PROGRESS_DIALOG = 1;
|
||||||
|
|
||||||
|
private SendRequest sendRequest;
|
||||||
|
private SendFusionTablesAsyncTask asyncTask;
|
||||||
|
private ProgressDialog progressDialog;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
sendRequest = getIntent().getParcelableExtra(SendRequest.SEND_REQUEST_KEY);
|
||||||
|
|
||||||
|
Object retained = getLastNonConfigurationInstance();
|
||||||
|
if (retained instanceof SendFusionTablesAsyncTask) {
|
||||||
|
asyncTask = (SendFusionTablesAsyncTask) retained;
|
||||||
|
asyncTask.setActivity(this);
|
||||||
|
} else {
|
||||||
|
asyncTask = new SendFusionTablesAsyncTask(
|
||||||
|
this, sendRequest.getTrackId(), sendRequest.getAccount());
|
||||||
|
asyncTask.execute();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object onRetainNonConfigurationInstance() {
|
||||||
|
asyncTask.setActivity(null);
|
||||||
|
return asyncTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Dialog onCreateDialog(int id) {
|
||||||
|
switch (id) {
|
||||||
|
case PROGRESS_DIALOG:
|
||||||
|
progressDialog = new ProgressDialog(this);
|
||||||
|
progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
|
||||||
|
progressDialog.setIcon(android.R.drawable.ic_dialog_info);
|
||||||
|
progressDialog.setTitle(getString(
|
||||||
|
R.string.send_google_progress_title, getString(R.string.send_google_fusion_tables)));
|
||||||
|
progressDialog.setMax(100);
|
||||||
|
progressDialog.setProgress(0);
|
||||||
|
progressDialog.setCancelable(true);
|
||||||
|
progressDialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
|
||||||
|
@Override
|
||||||
|
public void onCancel(DialogInterface dialog) {
|
||||||
|
asyncTask.cancel(true);
|
||||||
|
startNextActivity(false, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return progressDialog;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invokes when the associated AsyncTask completes.
|
||||||
|
*
|
||||||
|
* @param success true if success
|
||||||
|
*/
|
||||||
|
public void onAsyncTaskCompleted(boolean success) {
|
||||||
|
startNextActivity(success, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows the progress dialog.
|
||||||
|
*/
|
||||||
|
public void showProgressDialog() {
|
||||||
|
showDialog(PROGRESS_DIALOG);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the progress dialog value.
|
||||||
|
*/
|
||||||
|
public void setProgressDialogValue(int value) {
|
||||||
|
if (progressDialog != null) {
|
||||||
|
progressDialog.setProgress(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the next activity.
|
||||||
|
*
|
||||||
|
* @param success true if sendFusionTables is success
|
||||||
|
* @param isCancel true if it is a cancel request
|
||||||
|
*/
|
||||||
|
private void startNextActivity(boolean success, boolean isCancel) {
|
||||||
|
sendRequest.setFusionTablesSuccess(success);
|
||||||
|
|
||||||
|
Class<?> next;
|
||||||
|
if (isCancel) {
|
||||||
|
next = UploadResultActivity.class;
|
||||||
|
} else {
|
||||||
|
if (sendRequest.isSendDocs()) {
|
||||||
|
next = SendDocsActivity.class;
|
||||||
|
} else {
|
||||||
|
next = UploadResultActivity.class;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Intent intent = new Intent(this, next)
|
||||||
|
.putExtra(SendRequest.SEND_REQUEST_KEY, sendRequest);
|
||||||
|
startActivity(intent);
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
+531
@@ -0,0 +1,531 @@
|
|||||||
|
// Copyright 2012 Google Inc. All Rights Reserved.
|
||||||
|
|
||||||
|
package com.google.android.apps.mytracks.io.fusiontables;
|
||||||
|
|
||||||
|
import com.google.android.apps.mytracks.Constants;
|
||||||
|
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
|
||||||
|
import com.google.android.apps.mytracks.content.Track;
|
||||||
|
import com.google.android.apps.mytracks.content.Waypoint;
|
||||||
|
import com.google.android.apps.mytracks.io.sendtogoogle.SendToGoogleUtils;
|
||||||
|
import com.google.android.apps.mytracks.stats.DoubleBuffer;
|
||||||
|
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
|
||||||
|
import com.google.android.apps.mytracks.util.LocationUtils;
|
||||||
|
import com.google.android.apps.mytracks.util.StringUtils;
|
||||||
|
import com.google.android.apps.mytracks.util.SystemUtils;
|
||||||
|
import com.google.android.apps.mytracks.util.UnitConversions;
|
||||||
|
import com.google.android.maps.mytracks.R;
|
||||||
|
import com.google.api.client.googleapis.GoogleHeaders;
|
||||||
|
import com.google.api.client.googleapis.MethodOverride;
|
||||||
|
import com.google.api.client.http.GenericUrl;
|
||||||
|
import com.google.api.client.http.HttpRequest;
|
||||||
|
import com.google.api.client.http.HttpRequestFactory;
|
||||||
|
import com.google.api.client.http.HttpResponse;
|
||||||
|
import com.google.api.client.http.HttpTransport;
|
||||||
|
import com.google.api.client.http.InputStreamContent;
|
||||||
|
import com.google.api.client.util.Strings;
|
||||||
|
|
||||||
|
import android.accounts.Account;
|
||||||
|
import android.accounts.AccountManager;
|
||||||
|
import android.accounts.AuthenticatorException;
|
||||||
|
import android.accounts.OperationCanceledException;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.location.Location;
|
||||||
|
import android.os.AsyncTask;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.net.URLEncoder;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Vector;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AsyncTask to send a track to Google Fusion Tables.
|
||||||
|
*
|
||||||
|
* @author jshih@google.com (Jimmy Shih)
|
||||||
|
*/
|
||||||
|
public class SendFusionTablesAsyncTask extends AsyncTask<Void, Integer, Boolean> {
|
||||||
|
|
||||||
|
private static final String APP_NAME_PREFIX = "Google-MyTracks-";
|
||||||
|
private static final String SQL_KEY = "sql=";
|
||||||
|
private static final String CONTENT_TYPE = "application/x-www-form-urlencoded";
|
||||||
|
private static final String SERVICE_ID = "fusiontables";
|
||||||
|
private static final String FUSION_TABLES_BASE_URL =
|
||||||
|
"https://www.google.com/fusiontables/api/query";
|
||||||
|
private static final int MAX_POINTS_PER_UPLOAD = 2048;
|
||||||
|
private static final String GDATA_VERSION = "2";
|
||||||
|
|
||||||
|
private static final int PROGRESS_CREATE_TABLE = 0;
|
||||||
|
private static final int PROGRESS_UNLIST_TABLE = 5;
|
||||||
|
private static final int PROGRESS_UPLOAD_DATA_MIN = 10;
|
||||||
|
private static final int PROGRESS_UPLOAD_DATA_MAX = 90;
|
||||||
|
private static final int PROGRESS_UPLOAD_WAYPOINTS = 95;
|
||||||
|
private static final int PROGRESS_COMPLETE = 100;
|
||||||
|
|
||||||
|
// See http://support.google.com/fusiontables/bin/answer.py?hl=en&answer=185991
|
||||||
|
private static final String MARKER_TYPE_START = "large_green";
|
||||||
|
private static final String MARKER_TYPE_END = "large_red";
|
||||||
|
private static final String MARKER_TYPE_WAYPOINT = "large_yellow";
|
||||||
|
|
||||||
|
private static final String TAG = SendFusionTablesAsyncTask.class.getSimpleName();
|
||||||
|
|
||||||
|
private SendFusionTablesActivity activity;
|
||||||
|
|
||||||
|
private final Context context;
|
||||||
|
private final long trackId;
|
||||||
|
private final Account account;
|
||||||
|
private final MyTracksProviderUtils myTracksProviderUtils;
|
||||||
|
private final HttpRequestFactory httpRequestFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if can retry sending to Google Fusion Tables.
|
||||||
|
*/
|
||||||
|
private boolean canRetry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if the AsyncTask has completed.
|
||||||
|
*/
|
||||||
|
private boolean completed;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if the result is success.
|
||||||
|
*/
|
||||||
|
private boolean success;
|
||||||
|
|
||||||
|
// The following variables are for per upload states
|
||||||
|
private String authToken;
|
||||||
|
private String tableId;
|
||||||
|
int currentSegment;
|
||||||
|
|
||||||
|
public SendFusionTablesAsyncTask(
|
||||||
|
SendFusionTablesActivity activity, long trackId, Account account) {
|
||||||
|
this.activity = activity;
|
||||||
|
this.trackId = trackId;
|
||||||
|
this.account = account;
|
||||||
|
|
||||||
|
context = activity.getApplicationContext();
|
||||||
|
myTracksProviderUtils = MyTracksProviderUtils.Factory.get(context);
|
||||||
|
HttpTransport transport = ApiAdapterFactory.getApiAdapter().getHttpTransport();
|
||||||
|
httpRequestFactory = transport.createRequestFactory(new MethodOverride());
|
||||||
|
|
||||||
|
canRetry = true;
|
||||||
|
completed = false;
|
||||||
|
success = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the activity associated with this AyncTask.
|
||||||
|
*
|
||||||
|
* @param activity the activity.
|
||||||
|
*/
|
||||||
|
public void setActivity(SendFusionTablesActivity activity) {
|
||||||
|
this.activity = activity;
|
||||||
|
if (completed && activity != null) {
|
||||||
|
activity.onAsyncTaskCompleted(success);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPreExecute() {
|
||||||
|
activity.showProgressDialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Boolean doInBackground(Void... params) {
|
||||||
|
return doUpload();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onProgressUpdate(Integer... values) {
|
||||||
|
if (activity != null) {
|
||||||
|
activity.setProgressDialogValue(values[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPostExecute(Boolean result) {
|
||||||
|
success = result;
|
||||||
|
completed = true;
|
||||||
|
if (success) {
|
||||||
|
Track track = myTracksProviderUtils.getTrack(trackId);
|
||||||
|
if (track != null) {
|
||||||
|
track.setTableId(tableId);
|
||||||
|
myTracksProviderUtils.updateTrack(track);
|
||||||
|
} else {
|
||||||
|
Log.d(TAG, "No track");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (activity != null) {
|
||||||
|
activity.onAsyncTaskCompleted(success);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uploads a track to Google Fusion Tables.
|
||||||
|
*
|
||||||
|
* @return true if success.
|
||||||
|
*/
|
||||||
|
private boolean doUpload() {
|
||||||
|
// Reset the per upload states
|
||||||
|
authToken = null;
|
||||||
|
tableId = null;
|
||||||
|
currentSegment = 1;
|
||||||
|
|
||||||
|
try {
|
||||||
|
authToken = AccountManager.get(context).blockingGetAuthToken(account, SERVICE_ID, false);
|
||||||
|
} catch (OperationCanceledException e) {
|
||||||
|
Log.d(TAG, "Unable to get auth token", e);
|
||||||
|
return retryUpload();
|
||||||
|
} catch (AuthenticatorException e) {
|
||||||
|
Log.d(TAG, "Unable to get auth token", e);
|
||||||
|
return retryUpload();
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.d(TAG, "Unable to get auth token", e);
|
||||||
|
return retryUpload();
|
||||||
|
}
|
||||||
|
|
||||||
|
Track track = myTracksProviderUtils.getTrack(trackId);
|
||||||
|
if (track == null) {
|
||||||
|
Log.d(TAG, "Track is null");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new table
|
||||||
|
publishProgress(PROGRESS_CREATE_TABLE);
|
||||||
|
if (!createNewTable(track)) {
|
||||||
|
// Retry upload in case the auth token is invalid
|
||||||
|
return retryUpload();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unlist table
|
||||||
|
publishProgress(PROGRESS_UNLIST_TABLE);
|
||||||
|
if (!unlistTable()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upload all the track points plus the start and end markers
|
||||||
|
publishProgress(PROGRESS_UPLOAD_DATA_MIN);
|
||||||
|
if (!uploadAllTrackPoints(track)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upload all the waypoints
|
||||||
|
publishProgress(PROGRESS_UPLOAD_WAYPOINTS);
|
||||||
|
if (!uploadWaypoints()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
publishProgress(PROGRESS_COMPLETE);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retries upload. Invalidates the authToken. If can retry, invokes
|
||||||
|
* {@link SendFusionTablesAsyncTask#doUpload()}. Returns false if cannot
|
||||||
|
* retry.
|
||||||
|
*/
|
||||||
|
private boolean retryUpload() {
|
||||||
|
if (isCancelled()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
AccountManager.get(context).invalidateAuthToken(SERVICE_ID, authToken);
|
||||||
|
if (canRetry) {
|
||||||
|
canRetry = false;
|
||||||
|
return doUpload();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new table.
|
||||||
|
*
|
||||||
|
* @param track the track
|
||||||
|
* @return true if success.
|
||||||
|
*/
|
||||||
|
private boolean createNewTable(Track track) {
|
||||||
|
String query = "CREATE TABLE '" + SendFusionTablesUtils.escapeSqlString(track.getName())
|
||||||
|
+ "' (name:STRING,description:STRING,geometry:LOCATION,marker:STRING)";
|
||||||
|
return sendQuery(query, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unlists a table.
|
||||||
|
*
|
||||||
|
* @return true if success.
|
||||||
|
*/
|
||||||
|
private boolean unlistTable() {
|
||||||
|
String query = "UPDATE TABLE " + tableId + " SET VISIBILITY = UNLISTED";
|
||||||
|
return sendQuery(query, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uploads all the points in a track.
|
||||||
|
*
|
||||||
|
* @param track the track
|
||||||
|
* @return true if success.
|
||||||
|
*/
|
||||||
|
private boolean uploadAllTrackPoints(Track track) {
|
||||||
|
Cursor locationsCursor = null;
|
||||||
|
try {
|
||||||
|
SharedPreferences prefs = context.getSharedPreferences(
|
||||||
|
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
|
||||||
|
boolean metricUnits = prefs.getBoolean(context.getString(R.string.metric_units_key), true);
|
||||||
|
|
||||||
|
locationsCursor = myTracksProviderUtils.getLocationsCursor(trackId, 0, -1, false);
|
||||||
|
if (locationsCursor == null) {
|
||||||
|
Log.d(TAG, "Location cursor is null");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int locationsCount = locationsCursor.getCount();
|
||||||
|
List<Location> locations = new ArrayList<Location>(MAX_POINTS_PER_UPLOAD);
|
||||||
|
Location lastLocation = null;
|
||||||
|
|
||||||
|
// Limit the number of elevation readings. Ideally we would want around
|
||||||
|
// 250.
|
||||||
|
int elevationSamplingFrequency = Math.max(1, (int) (locationsCount / 250.0));
|
||||||
|
double totalDistance = 0;
|
||||||
|
DoubleBuffer elevationBuffer = new DoubleBuffer(Constants.ELEVATION_SMOOTHING_FACTOR);
|
||||||
|
Vector<Double> distances = new Vector<Double>();
|
||||||
|
Vector<Double> elevations = new Vector<Double>();
|
||||||
|
|
||||||
|
for (int i = 0; i < locationsCount; i++) {
|
||||||
|
locationsCursor.moveToPosition(i);
|
||||||
|
|
||||||
|
Location location = myTracksProviderUtils.createLocation(locationsCursor);
|
||||||
|
locations.add(location);
|
||||||
|
|
||||||
|
int readCount = i + 1;
|
||||||
|
|
||||||
|
if (readCount == 1) {
|
||||||
|
// Create a start marker
|
||||||
|
String name = context.getString(R.string.marker_label_start, track.getName());
|
||||||
|
if (!createNewPoint(name, "", location, MARKER_TYPE_START)) {
|
||||||
|
Log.d(TAG, "Unable to create the start marker");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add to the distances and elevations vectors
|
||||||
|
if (LocationUtils.isValidLocation(location)) {
|
||||||
|
// All points go into the smoothing buffer
|
||||||
|
elevationBuffer.setNext(metricUnits ? location.getAltitude()
|
||||||
|
: location.getAltitude() * UnitConversions.M_TO_FT);
|
||||||
|
if (lastLocation != null) {
|
||||||
|
totalDistance += (double) lastLocation.distanceTo(location);
|
||||||
|
}
|
||||||
|
if (readCount % elevationSamplingFrequency == 0) {
|
||||||
|
distances.add(totalDistance);
|
||||||
|
elevations.add(elevationBuffer.getAverage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upload periodically
|
||||||
|
if (readCount % MAX_POINTS_PER_UPLOAD == 0) {
|
||||||
|
if (!prepareAndUploadPoints(track, locations, false)) {
|
||||||
|
Log.d(TAG, "Unable to upload points");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
updateProgress(readCount, locationsCount);
|
||||||
|
locations.clear();
|
||||||
|
}
|
||||||
|
lastLocation = location;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do a final upload with the remaining locations
|
||||||
|
if (!prepareAndUploadPoints(track, locations, true)) {
|
||||||
|
Log.d(TAG, "Unable to upload points");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create an end marker
|
||||||
|
if (lastLocation != null) {
|
||||||
|
StringUtils stringUtils = new StringUtils(context);
|
||||||
|
track.setDescription("<p>" + track.getDescription() + "</p><p>"
|
||||||
|
+ stringUtils.generateTrackDescription(track, distances, elevations) + "</p>");
|
||||||
|
String name = context.getString(R.string.marker_label_end, track.getName());
|
||||||
|
if (!createNewPoint(name, track.getDescription(), lastLocation, MARKER_TYPE_END)) {
|
||||||
|
Log.d(TAG, "Unable to create the end marker");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} finally {
|
||||||
|
if (locationsCursor != null) {
|
||||||
|
locationsCursor.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepares and uploads a list of locations from a track.
|
||||||
|
*
|
||||||
|
* @param track the track
|
||||||
|
* @param locations the locations from the track
|
||||||
|
* @param lastBatch true if it is the last batch of locations
|
||||||
|
*/
|
||||||
|
private boolean prepareAndUploadPoints(Track track, List<Location> locations, boolean lastBatch) {
|
||||||
|
// Prepare locations
|
||||||
|
ArrayList<Track> splitTracks = SendToGoogleUtils.prepareLocations(track, locations);
|
||||||
|
|
||||||
|
// Upload segments
|
||||||
|
boolean onlyOneSegment = lastBatch && currentSegment == 1 && splitTracks.size() == 1;
|
||||||
|
for (Track splitTrack : splitTracks) {
|
||||||
|
if (!onlyOneSegment) {
|
||||||
|
splitTrack.setName(context.getString(
|
||||||
|
R.string.send_google_track_part_label, splitTrack.getName(), currentSegment));
|
||||||
|
}
|
||||||
|
if (!createNewLineString(splitTrack)) {
|
||||||
|
Log.d(TAG, "Upload points failed");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
currentSegment++;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uploads all the waypoints.
|
||||||
|
*
|
||||||
|
* @return true if success.
|
||||||
|
*/
|
||||||
|
private boolean uploadWaypoints() {
|
||||||
|
Cursor cursor = null;
|
||||||
|
try {
|
||||||
|
cursor = myTracksProviderUtils.getWaypointsCursor(
|
||||||
|
trackId, 0, Constants.MAX_LOADED_WAYPOINTS_POINTS);
|
||||||
|
if (cursor != null && cursor.moveToFirst()) {
|
||||||
|
// This will skip the first waypoint (it carries the stats for the
|
||||||
|
// track).
|
||||||
|
while (cursor.moveToNext()) {
|
||||||
|
Waypoint wpt = myTracksProviderUtils.createWaypoint(cursor);
|
||||||
|
if (!createNewPoint(
|
||||||
|
wpt.getName(), wpt.getDescription(), wpt.getLocation(), MARKER_TYPE_WAYPOINT)) {
|
||||||
|
Log.d(TAG, "Upload waypoints failed");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} finally {
|
||||||
|
if (cursor != null) {
|
||||||
|
cursor.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new row in Google Fusion Tables representing a marker as a
|
||||||
|
* point.
|
||||||
|
*
|
||||||
|
* @param name the marker name
|
||||||
|
* @param description the marker description
|
||||||
|
* @param location the marker location
|
||||||
|
* @param type the marker type
|
||||||
|
* @return true if success.
|
||||||
|
*/
|
||||||
|
private boolean createNewPoint(
|
||||||
|
String name, String description, Location location, String type) {
|
||||||
|
String query = "INSERT INTO " + tableId + " (name,description,geometry,marker) VALUES "
|
||||||
|
+ SendFusionTablesUtils.formatSqlValues(
|
||||||
|
name, description, SendFusionTablesUtils.getKmlPoint(location), type);
|
||||||
|
return sendQuery(query, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new row in Google Fusion Tables representing the track as a
|
||||||
|
* line segment.
|
||||||
|
*
|
||||||
|
* @param track the track
|
||||||
|
* @return true if success.
|
||||||
|
*/
|
||||||
|
private boolean createNewLineString(Track track) {
|
||||||
|
String query = "INSERT INTO " + tableId + " (name,description,geometry) VALUES "
|
||||||
|
+ SendFusionTablesUtils.formatSqlValues(track.getName(), track.getDescription(),
|
||||||
|
SendFusionTablesUtils.getKmlLineString(track.getLocations()));
|
||||||
|
return sendQuery(query, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends a query to Google Fusion Tables.
|
||||||
|
*
|
||||||
|
* @param query the Fusion Tables SQL query
|
||||||
|
* @param setTableId true to set the table id
|
||||||
|
* @return true if success.
|
||||||
|
*/
|
||||||
|
private boolean sendQuery(String query, boolean setTableId) {
|
||||||
|
Log.d(TAG, "SendQuery: " + query);
|
||||||
|
|
||||||
|
if (isCancelled()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
GenericUrl url = new GenericUrl(FUSION_TABLES_BASE_URL);
|
||||||
|
String sql = SQL_KEY + URLEncoder.encode(query);
|
||||||
|
ByteArrayInputStream inputStream = new ByteArrayInputStream(Strings.toBytesUtf8(sql));
|
||||||
|
InputStreamContent inputStreamContent = new InputStreamContent(null, inputStream);
|
||||||
|
HttpRequest request;
|
||||||
|
try {
|
||||||
|
request = httpRequestFactory.buildPostRequest(url, inputStreamContent);
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.d(TAG, "Unable to build request", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
GoogleHeaders headers = new GoogleHeaders();
|
||||||
|
headers.setApplicationName(APP_NAME_PREFIX + SystemUtils.getMyTracksVersion(context));
|
||||||
|
headers.gdataVersion = GDATA_VERSION;
|
||||||
|
headers.setGoogleLogin(authToken);
|
||||||
|
headers.setContentType(CONTENT_TYPE);
|
||||||
|
request.setHeaders(headers);
|
||||||
|
|
||||||
|
HttpResponse response;
|
||||||
|
try {
|
||||||
|
response = request.execute();
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.d(TAG, "Unable to execute request", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
boolean isSuccess = response.isSuccessStatusCode();
|
||||||
|
if (isSuccess) {
|
||||||
|
InputStream content;
|
||||||
|
try {
|
||||||
|
content = response.getContent();
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.d(TAG, "Unable to get response", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (setTableId) {
|
||||||
|
tableId = SendFusionTablesUtils.getTableId(content);
|
||||||
|
if (tableId == null) {
|
||||||
|
Log.d(TAG, "tableId is null");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.d(TAG,
|
||||||
|
"sendQuery failed: " + response.getStatusMessage() + ": " + response.getStatusCode());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the progress based on the number of locations uploaded.
|
||||||
|
*
|
||||||
|
* @param uploaded the number of uploaded locations
|
||||||
|
* @param total the number of total locations
|
||||||
|
*/
|
||||||
|
private void updateProgress(int uploaded, int total) {
|
||||||
|
double totalPercentage = uploaded / total;
|
||||||
|
double scaledPercentage = totalPercentage
|
||||||
|
* (PROGRESS_UPLOAD_DATA_MAX - PROGRESS_UPLOAD_DATA_MIN) + PROGRESS_UPLOAD_DATA_MIN;
|
||||||
|
publishProgress((int) scaledPercentage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
+195
@@ -0,0 +1,195 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012 Google Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||||
|
* use this file except in compliance with the License. You may obtain a copy of
|
||||||
|
* the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations under
|
||||||
|
* the License.
|
||||||
|
*/
|
||||||
|
package com.google.android.apps.mytracks.io.fusiontables;
|
||||||
|
|
||||||
|
import com.google.android.apps.mytracks.content.Track;
|
||||||
|
import com.google.android.apps.mytracks.stats.TripStatistics;
|
||||||
|
import com.google.api.client.util.Strings;
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
|
||||||
|
import android.location.Location;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.UnsupportedEncodingException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utilities for sending a track to Google Fusion Tables.
|
||||||
|
*
|
||||||
|
* @author jshih@google.com (Jimmy Shih)
|
||||||
|
*/
|
||||||
|
public class SendFusionTablesUtils {
|
||||||
|
|
||||||
|
private static final String UTF8 = "UTF8";
|
||||||
|
private static final String TABLE_ID = "tableid";
|
||||||
|
private static final String MAP_URL = "https://www.google.com/fusiontables/embedviz?"
|
||||||
|
+ "viz=MAP&q=select+col0,+col1,+col2,+col3+from+%s+&h=false&lat=%f&lng=%f&z=%d&t=1&l=col2";
|
||||||
|
private static final String TAG = SendFusionTablesUtils.class.getSimpleName();
|
||||||
|
|
||||||
|
private SendFusionTablesUtils() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the url to visualize a fusion table on a map.
|
||||||
|
*
|
||||||
|
* @param track the track
|
||||||
|
* @return the url.
|
||||||
|
*/
|
||||||
|
public static String getMapUrl(Track track) {
|
||||||
|
if (track == null || track.getStatistics() == null || track.getTableId() == null) {
|
||||||
|
Log.e(TAG, "Invalid track");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(jshih): Determine the correct bounding box and zoom level that
|
||||||
|
// will show the entire track.
|
||||||
|
TripStatistics stats = track.getStatistics();
|
||||||
|
double latE6 = stats.getBottom() + (stats.getTop() - stats.getBottom()) / 2;
|
||||||
|
double lonE6 = stats.getLeft() + (stats.getRight() - stats.getLeft()) / 2;
|
||||||
|
int z = 15;
|
||||||
|
|
||||||
|
// We explicitly format with Locale.US because we need the latitude and
|
||||||
|
// longitude to be formatted in a locale-independent manner. Specifically,
|
||||||
|
// we need the decimal separator to be a period rather than a comma.
|
||||||
|
return String.format(
|
||||||
|
Locale.US, MAP_URL, track.getTableId(), latE6 / 1.E6, lonE6 / 1.E6, z);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats an array of values as a SQL VALUES like
|
||||||
|
* ('value1','value2',...,'value_n'). Escapes single quotes with two single
|
||||||
|
* quotes.
|
||||||
|
*
|
||||||
|
* @param values an array of values to format
|
||||||
|
* @return the formated SQL VALUES.
|
||||||
|
*/
|
||||||
|
public static String formatSqlValues(String... values) {
|
||||||
|
StringBuilder builder = new StringBuilder("(");
|
||||||
|
for (int i = 0; i < values.length; i++) {
|
||||||
|
if (i > 0) {
|
||||||
|
builder.append(',');
|
||||||
|
}
|
||||||
|
builder.append('\'');
|
||||||
|
builder.append(escapeSqlString(values[i]));
|
||||||
|
builder.append('\'');
|
||||||
|
}
|
||||||
|
builder.append(")");
|
||||||
|
return builder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Escapes a SQL string. Escapes single quotes with two single quotes.
|
||||||
|
*
|
||||||
|
* @param string the string
|
||||||
|
* @return the escaped string.
|
||||||
|
*/
|
||||||
|
public static String escapeSqlString(String string) {
|
||||||
|
return string.replaceAll("'", "''");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a KML Point value representing a location.
|
||||||
|
*
|
||||||
|
* @param location the location
|
||||||
|
* @return the KML Point value.
|
||||||
|
*/
|
||||||
|
public static String getKmlPoint(Location location) {
|
||||||
|
StringBuilder builder = new StringBuilder("<Point><coordinates>");
|
||||||
|
if (location != null) {
|
||||||
|
appendLocation(location, builder);
|
||||||
|
}
|
||||||
|
builder.append("</coordinates></Point>");
|
||||||
|
return builder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a KML LineString value representing an array of locations.
|
||||||
|
*
|
||||||
|
* @param locations the locations.
|
||||||
|
* @return the KML LineString value.
|
||||||
|
*/
|
||||||
|
public static String getKmlLineString(ArrayList<Location> locations) {
|
||||||
|
StringBuilder builder = new StringBuilder("<LineString><coordinates>");
|
||||||
|
if (locations != null) {
|
||||||
|
for (int i = 0; i < locations.size(); i++) {
|
||||||
|
if (i != 0) {
|
||||||
|
builder.append(' ');
|
||||||
|
}
|
||||||
|
appendLocation(locations.get(i), builder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
builder.append("</coordinates></LineString>");
|
||||||
|
return builder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appends a location to a string builder using "longitude,latitude[,altitude]" format.
|
||||||
|
*
|
||||||
|
* @param location the location
|
||||||
|
* @param builder the string builder
|
||||||
|
*/
|
||||||
|
@VisibleForTesting
|
||||||
|
static void appendLocation(Location location, StringBuilder builder) {
|
||||||
|
builder.append(location.getLongitude()).append(",").append(location.getLatitude());
|
||||||
|
if (location.hasAltitude()) {
|
||||||
|
builder.append(",");
|
||||||
|
builder.append(location.getAltitude());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the table id from an input streawm.
|
||||||
|
*
|
||||||
|
* @param inputStream input stream
|
||||||
|
* @return table id or null if not available.
|
||||||
|
*/
|
||||||
|
public static String getTableId(InputStream inputStream) {
|
||||||
|
if (inputStream == null) {
|
||||||
|
Log.d(TAG, "inputStream is null");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
byte[] result = new byte[1024];
|
||||||
|
int read;
|
||||||
|
try {
|
||||||
|
read = inputStream.read(result);
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.d(TAG, "Unable to read result", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (read == -1) {
|
||||||
|
Log.d(TAG, "no data read");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String s;
|
||||||
|
try {
|
||||||
|
s = new String(result, 0, read, UTF8);
|
||||||
|
} catch (UnsupportedEncodingException e) {
|
||||||
|
Log.d(TAG, "Unable to parse result", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String[] lines = s.split(Strings.LINE_SEPARATOR);
|
||||||
|
if (lines.length > 1 && lines[0].equals(TABLE_ID)) {
|
||||||
|
// returns the next line
|
||||||
|
return lines[1];
|
||||||
|
} else {
|
||||||
|
Log.d(TAG, "Response is not valid: " + s);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -36,6 +36,7 @@ public class GDataClientFactory {
|
|||||||
* @return A GDataClient appropriate for this platform
|
* @return A GDataClient appropriate for this platform
|
||||||
*/
|
*/
|
||||||
public static GDataClient getGDataClient(Context context) {
|
public static GDataClient getGDataClient(Context context) {
|
||||||
|
// TODO This should be moved into ApiAdapter
|
||||||
try {
|
try {
|
||||||
// Try to use the official unbundled gdata client implementation.
|
// Try to use the official unbundled gdata client implementation.
|
||||||
// This should work on Froyo and beyond.
|
// This should work on Froyo and beyond.
|
||||||
|
|||||||
@@ -1,309 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2008 Google Inc.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
|
||||||
* use this file except in compliance with the License. You may obtain a copy of
|
|
||||||
* the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
* License for the specific language governing permissions and limitations under
|
|
||||||
* the License.
|
|
||||||
*/
|
|
||||||
package com.google.android.apps.mytracks.io.gdata;
|
|
||||||
|
|
||||||
import com.google.android.apps.mytracks.Constants;
|
|
||||||
import com.google.android.apps.mytracks.io.AuthManager;
|
|
||||||
import com.google.android.apps.mytracks.io.AuthManager.AuthCallback;
|
|
||||||
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import java.io.FileNotFoundException;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.concurrent.ExecutionException;
|
|
||||||
import java.util.concurrent.FutureTask;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
import java.util.concurrent.TimeoutException;
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GDataWrapper provides a wrapper around GData operations that maintains the
|
|
||||||
* GData client, and provides a method to run GData queries with proper error
|
|
||||||
* handling. After a query is run, the wrapper can be queried about the error
|
|
||||||
* that occurred.
|
|
||||||
*
|
|
||||||
* @param <C> the GData service client
|
|
||||||
* @author Sandor Dornbush
|
|
||||||
*/
|
|
||||||
public class GDataWrapper<C> {
|
|
||||||
|
|
||||||
public static class AuthenticationException extends Exception {
|
|
||||||
private Exception exception;
|
|
||||||
private static final long serialVersionUID = 1L;
|
|
||||||
public AuthenticationException(Exception caught) {
|
|
||||||
this.exception = caught;
|
|
||||||
}
|
|
||||||
public Exception getException() {
|
|
||||||
return exception;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
public static class ParseException extends Exception {
|
|
||||||
private Exception exception;
|
|
||||||
private static final long serialVersionUID = 1L;
|
|
||||||
public ParseException(Exception caught) {
|
|
||||||
this.exception = caught;
|
|
||||||
}
|
|
||||||
public Exception getException() {
|
|
||||||
return exception;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
public static class ConflictDetectedException extends Exception {
|
|
||||||
private Exception exception;
|
|
||||||
private static final long serialVersionUID = 1L;
|
|
||||||
public ConflictDetectedException(Exception caught) {
|
|
||||||
this.exception = caught;
|
|
||||||
}
|
|
||||||
public Exception getException() {
|
|
||||||
return exception;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
public static class HttpException extends Exception {
|
|
||||||
private static final long serialVersionUID = 1L;
|
|
||||||
private int statusCode;
|
|
||||||
private String statusMessage;
|
|
||||||
public HttpException(int statusCode, String statusMessage) {
|
|
||||||
super();
|
|
||||||
this.statusCode = statusCode;
|
|
||||||
this.statusMessage = statusMessage;
|
|
||||||
}
|
|
||||||
public int getStatusCode() {
|
|
||||||
return statusCode;
|
|
||||||
}
|
|
||||||
public String getStatusMessage() {
|
|
||||||
return statusMessage;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A QueryFunction is passed in when executing a query. The query function of
|
|
||||||
* the class is called with the GData client as a parameter. The function
|
|
||||||
* should execute whatever operations it desires on the client without concern
|
|
||||||
* for whether the client will throw an error.
|
|
||||||
*/
|
|
||||||
public interface QueryFunction<C> {
|
|
||||||
public abstract void query(C client)
|
|
||||||
throws AuthenticationException, IOException, ParseException,
|
|
||||||
ConflictDetectedException, HttpException;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A AuthenticatedFunction is passed in when executing the google
|
|
||||||
* authenticated service. The authenticated function of the class is called
|
|
||||||
* with the current authentication token for the service. The function should
|
|
||||||
* execute whatever operations with the google service without concern for
|
|
||||||
* whether the client will throw an error.
|
|
||||||
*/
|
|
||||||
public interface AuthenticatedFunction {
|
|
||||||
public abstract void run(String authenticationToken)
|
|
||||||
throws AuthenticationException, IOException;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The types of error that may be encountered
|
|
||||||
// No error occurred.
|
|
||||||
public static final int ERROR_NO_ERROR = 0;
|
|
||||||
// There was an authentication error, the auth token may be invalid.
|
|
||||||
public static final int ERROR_AUTH = 1;
|
|
||||||
// There was an internal error on the server side.
|
|
||||||
public static final int ERROR_INTERNAL = 2;
|
|
||||||
// There was an error connecting to the server.
|
|
||||||
public static final int ERROR_CONNECTION = 3;
|
|
||||||
// The item queried did not exit.
|
|
||||||
public static final int ERROR_NOT_FOUND = 4;
|
|
||||||
// There was an error parsing or serializing locally.
|
|
||||||
public static final int ERROR_LOCAL = 5;
|
|
||||||
// There was a conflict, update the entry and try again.
|
|
||||||
public static final int ERROR_CONFLICT = 6;
|
|
||||||
// A query was run after cleaning up the wrapper, so the client was invalid.
|
|
||||||
public static final int ERROR_CLEANED_UP = 7;
|
|
||||||
// An unknown error occurred.
|
|
||||||
public static final int ERROR_UNKNOWN = 100;
|
|
||||||
|
|
||||||
private static final int AUTH_TOKEN_INVALIDATE_REFRESH_NUM_RETRIES = 1;
|
|
||||||
private static final int AUTH_TOKEN_INVALIDATE_REFRESH_TIMEOUT = 5000;
|
|
||||||
|
|
||||||
private String errorMessage;
|
|
||||||
private int errorType;
|
|
||||||
private C gdataServiceClient;
|
|
||||||
private AuthManager auth;
|
|
||||||
private boolean retryOnAuthFailure;
|
|
||||||
|
|
||||||
public GDataWrapper() {
|
|
||||||
errorType = ERROR_NO_ERROR;
|
|
||||||
errorMessage = null;
|
|
||||||
auth = null;
|
|
||||||
retryOnAuthFailure = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setClient(C gdataServiceClient) {
|
|
||||||
this.gdataServiceClient = gdataServiceClient;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean runAuthenticatedFunction(
|
|
||||||
final AuthenticatedFunction function) {
|
|
||||||
return runCommon(function, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean runQuery(final QueryFunction<C> query) {
|
|
||||||
return runCommon(null, query);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Runs an arbitrary piece of code.
|
|
||||||
*/
|
|
||||||
private boolean runCommon(final AuthenticatedFunction function,
|
|
||||||
final QueryFunction<C> query) {
|
|
||||||
for (int i = 0; i <= AUTH_TOKEN_INVALIDATE_REFRESH_NUM_RETRIES; i++) {
|
|
||||||
runOne(function, query);
|
|
||||||
if (errorType == ERROR_NO_ERROR) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.d(Constants.TAG, "GData error encountered: " + errorMessage);
|
|
||||||
if (errorType == ERROR_AUTH && auth != null) {
|
|
||||||
if (!retryOnAuthFailure || !invalidateAndRefreshAuthToken()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.d(Constants.TAG, "retrying function/query");
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute a given function or query. If one is executed, errorType and
|
|
||||||
* errorMessage will contain the result/status of the function/query.
|
|
||||||
*/
|
|
||||||
private void runOne(final AuthenticatedFunction function,
|
|
||||||
final QueryFunction<C> query) {
|
|
||||||
try {
|
|
||||||
if (function != null) {
|
|
||||||
function.run(this.auth.getAuthToken());
|
|
||||||
} else if (query != null) {
|
|
||||||
query.query(gdataServiceClient);
|
|
||||||
} else {
|
|
||||||
throw new IllegalArgumentException(
|
|
||||||
"invalid invocation of runOne; one of function/query " +
|
|
||||||
"must be non-null");
|
|
||||||
}
|
|
||||||
|
|
||||||
errorType = ERROR_NO_ERROR;
|
|
||||||
errorMessage = null;
|
|
||||||
|
|
||||||
} catch (AuthenticationException e) {
|
|
||||||
Log.e(Constants.TAG, "AuthenticationException", e);
|
|
||||||
errorType = ERROR_AUTH;
|
|
||||||
errorMessage = e.getMessage();
|
|
||||||
} catch (HttpException e) {
|
|
||||||
Log.e(Constants.TAG,
|
|
||||||
"HttpException, code " + e.getStatusCode() + " message " + e.getMessage(), e);
|
|
||||||
errorMessage = e.getMessage();
|
|
||||||
if (e.getStatusCode() == 401) {
|
|
||||||
errorType = ERROR_AUTH;
|
|
||||||
} else {
|
|
||||||
errorType = ERROR_CONNECTION;
|
|
||||||
}
|
|
||||||
} catch (FileNotFoundException e) {
|
|
||||||
Log.e(Constants.TAG, "Exception", e);
|
|
||||||
errorType = ERROR_AUTH;
|
|
||||||
errorMessage = e.getMessage();
|
|
||||||
} catch (IOException e) {
|
|
||||||
Log.e(Constants.TAG, "Exception", e);
|
|
||||||
errorMessage = e.getMessage();
|
|
||||||
if (errorMessage != null && errorMessage.contains("503")) {
|
|
||||||
errorType = ERROR_INTERNAL;
|
|
||||||
} else {
|
|
||||||
errorType = ERROR_CONNECTION;
|
|
||||||
}
|
|
||||||
} catch (ParseException e) {
|
|
||||||
Log.e(Constants.TAG, "Exception", e);
|
|
||||||
errorType = ERROR_LOCAL;
|
|
||||||
errorMessage = e.getMessage();
|
|
||||||
} catch (ConflictDetectedException e) {
|
|
||||||
Log.e(Constants.TAG, "Exception", e);
|
|
||||||
errorType = ERROR_CONFLICT;
|
|
||||||
errorMessage = e.getMessage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Invalidates and refreshes the auth token. Blocks until the refresh has
|
|
||||||
* completed or until we deem the refresh as having timed out.
|
|
||||||
*
|
|
||||||
* @return true If the invalidate/refresh succeeds, false if it fails or
|
|
||||||
* times out.
|
|
||||||
*/
|
|
||||||
private boolean invalidateAndRefreshAuthToken() {
|
|
||||||
Log.d(Constants.TAG, "Retrying due to auth failure");
|
|
||||||
// This FutureTask doesn't do anything -- it exists simply to be
|
|
||||||
// blocked upon using get().
|
|
||||||
final FutureTask<?> whenFinishedFuture = new FutureTask<Object>(new Runnable() {
|
|
||||||
public void run() {}
|
|
||||||
}, null);
|
|
||||||
|
|
||||||
final AtomicBoolean finalSuccess = new AtomicBoolean(false);
|
|
||||||
auth.invalidateAndRefresh(new AuthCallback() {
|
|
||||||
@Override
|
|
||||||
public void onAuthResult(boolean success) {
|
|
||||||
finalSuccess.set(success);
|
|
||||||
whenFinishedFuture.run();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
Log.d(Constants.TAG, "waiting for invalidate");
|
|
||||||
whenFinishedFuture.get(AUTH_TOKEN_INVALIDATE_REFRESH_TIMEOUT,
|
|
||||||
TimeUnit.MILLISECONDS);
|
|
||||||
boolean success = finalSuccess.get();
|
|
||||||
Log.d(Constants.TAG, "invalidate finished, success = " + success);
|
|
||||||
return success;
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
Log.e(Constants.TAG, "Failed to invalidate", e);
|
|
||||||
} catch (ExecutionException e) {
|
|
||||||
Log.e(Constants.TAG, "Failed to invalidate", e);
|
|
||||||
} catch (TimeoutException e) {
|
|
||||||
Log.e(Constants.TAG, "Invalidate didn't complete in time", e);
|
|
||||||
} finally {
|
|
||||||
whenFinishedFuture.cancel(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getErrorType() {
|
|
||||||
return errorType;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getErrorMessage() {
|
|
||||||
return errorMessage;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setAuthManager(AuthManager auth) {
|
|
||||||
this.auth = auth;
|
|
||||||
}
|
|
||||||
|
|
||||||
public AuthManager getAuthManager() {
|
|
||||||
return auth;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setRetryOnAuthFailure(boolean retry) {
|
|
||||||
retryOnAuthFailure = retry;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+1
-1
@@ -13,7 +13,7 @@
|
|||||||
* License for the specific language governing permissions and limitations under
|
* License for the specific language governing permissions and limitations under
|
||||||
* the License.
|
* the License.
|
||||||
*/
|
*/
|
||||||
package com.google.wireless.gdata.docs;
|
package com.google.android.apps.mytracks.io.gdata.docs;
|
||||||
|
|
||||||
import com.google.wireless.gdata.client.GDataClient;
|
import com.google.wireless.gdata.client.GDataClient;
|
||||||
import com.google.wireless.gdata.client.GDataParserFactory;
|
import com.google.wireless.gdata.client.GDataParserFactory;
|
||||||
+1
-1
@@ -13,7 +13,7 @@
|
|||||||
* License for the specific language governing permissions and limitations under
|
* License for the specific language governing permissions and limitations under
|
||||||
* the License.
|
* the License.
|
||||||
*/
|
*/
|
||||||
package com.google.wireless.gdata.docs;
|
package com.google.android.apps.mytracks.io.gdata.docs;
|
||||||
|
|
||||||
import com.google.wireless.gdata.client.GDataClient;
|
import com.google.wireless.gdata.client.GDataClient;
|
||||||
import com.google.wireless.gdata.client.GDataParserFactory;
|
import com.google.wireless.gdata.client.GDataParserFactory;
|
||||||
+1
-1
@@ -13,7 +13,7 @@
|
|||||||
* License for the specific language governing permissions and limitations under
|
* License for the specific language governing permissions and limitations under
|
||||||
* the License.
|
* the License.
|
||||||
*/
|
*/
|
||||||
package com.google.wireless.gdata.docs;
|
package com.google.android.apps.mytracks.io.gdata.docs;
|
||||||
|
|
||||||
import com.google.wireless.gdata.client.GDataParserFactory;
|
import com.google.wireless.gdata.client.GDataParserFactory;
|
||||||
import com.google.wireless.gdata.data.Entry;
|
import com.google.wireless.gdata.data.Entry;
|
||||||
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
// Copyright 2010 Google Inc. All Rights Reserved.
|
// Copyright 2010 Google Inc. All Rights Reserved.
|
||||||
package com.google.android.apps.mytracks.io.maps;
|
package com.google.android.apps.mytracks.io.gdata.maps;
|
||||||
|
|
||||||
import com.google.wireless.gdata.data.Entry;
|
import com.google.wireless.gdata.data.Entry;
|
||||||
|
|
||||||
@@ -9,7 +9,7 @@ import java.util.Map;
|
|||||||
/**
|
/**
|
||||||
* GData entry for a map feature.
|
* GData entry for a map feature.
|
||||||
*/
|
*/
|
||||||
class MapFeatureEntry extends Entry {
|
public class MapFeatureEntry extends Entry {
|
||||||
|
|
||||||
private String mPrivacy = null;
|
private String mPrivacy = null;
|
||||||
private Map<String, String> mAttributes = new HashMap<String, String>();
|
private Map<String, String> mAttributes = new HashMap<String, String>();
|
||||||
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
// Copyright 2010 Google Inc. All Rights Reserved.
|
// Copyright 2010 Google Inc. All Rights Reserved.
|
||||||
package com.google.android.apps.mytracks.io.maps;
|
package com.google.android.apps.mytracks.io.gdata.maps;
|
||||||
|
|
||||||
import com.google.wireless.gdata.client.GDataClient;
|
import com.google.wireless.gdata.client.GDataClient;
|
||||||
import com.google.wireless.gdata.client.GDataParserFactory;
|
import com.google.wireless.gdata.client.GDataParserFactory;
|
||||||
@@ -10,7 +10,7 @@ import android.util.Log;
|
|||||||
/**
|
/**
|
||||||
* Client to talk to Google Maps via GData.
|
* Client to talk to Google Maps via GData.
|
||||||
*/
|
*/
|
||||||
class MapsClient extends GDataServiceClient {
|
public class MapsClient extends GDataServiceClient {
|
||||||
private static final boolean DEBUG = false;
|
private static final boolean DEBUG = false;
|
||||||
public static final boolean LOG_COMMUNICATION = false;
|
public static final boolean LOG_COMMUNICATION = false;
|
||||||
|
|
||||||
+1
-2
@@ -1,11 +1,10 @@
|
|||||||
// Copyright 2009 Google Inc. All Rights Reserved.
|
// Copyright 2009 Google Inc. All Rights Reserved.
|
||||||
package com.google.android.apps.mytracks.io.maps;
|
package com.google.android.apps.mytracks.io.gdata.maps;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constants for Google Maps.
|
* Constants for Google Maps.
|
||||||
*/
|
*/
|
||||||
public class MapsConstants {
|
public class MapsConstants {
|
||||||
static final String TAG = "MapsApi";
|
|
||||||
static final String MAPSHOP_BASE_URL =
|
static final String MAPSHOP_BASE_URL =
|
||||||
"https://maps.google.com/maps/ms";
|
"https://maps.google.com/maps/ms";
|
||||||
public static final String SERVICE_NAME = "local";
|
public static final String SERVICE_NAME = "local";
|
||||||
+4
-4
@@ -1,5 +1,5 @@
|
|||||||
// Copyright 2009 Google Inc. All Rights Reserved.
|
// Copyright 2009 Google Inc. All Rights Reserved.
|
||||||
package com.google.android.apps.mytracks.io.maps;
|
package com.google.android.apps.mytracks.io.gdata.maps;
|
||||||
|
|
||||||
import com.google.android.maps.GeoPoint;
|
import com.google.android.maps.GeoPoint;
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ import java.util.Vector;
|
|||||||
* in a {@link MapsFeatureMetadata} object so that it can be more efficiently
|
* in a {@link MapsFeatureMetadata} object so that it can be more efficiently
|
||||||
* transmitted to other activities.
|
* transmitted to other activities.
|
||||||
*/
|
*/
|
||||||
class MapsFeature {
|
public class MapsFeature {
|
||||||
|
|
||||||
private static final long serialVersionUID = 8439035544430497236L;
|
private static final long serialVersionUID = 8439035544430497236L;
|
||||||
|
|
||||||
@@ -73,7 +73,7 @@ class MapsFeature {
|
|||||||
* Generates a new local id for this feature based on the current time and
|
* Generates a new local id for this feature based on the current time and
|
||||||
* a random number.
|
* a random number.
|
||||||
*/
|
*/
|
||||||
void generateAndroidId() {
|
public void generateAndroidId() {
|
||||||
long time = System.currentTimeMillis();
|
long time = System.currentTimeMillis();
|
||||||
int rand = random.nextInt(10000);
|
int rand = random.nextInt(10000);
|
||||||
androidId = time + "." + rand;
|
androidId = time + "." + rand;
|
||||||
@@ -84,7 +84,7 @@ class MapsFeature {
|
|||||||
*
|
*
|
||||||
* @return The local id for this feature
|
* @return The local id for this feature
|
||||||
*/
|
*/
|
||||||
String getAndroidId() {
|
public String getAndroidId() {
|
||||||
return androidId;
|
return androidId;
|
||||||
}
|
}
|
||||||
|
|
||||||
+2
-1
@@ -1,5 +1,6 @@
|
|||||||
// Copyright 2009 Google Inc. All Rights Reserved.
|
// Copyright 2009 Google Inc. All Rights Reserved.
|
||||||
package com.google.android.apps.mytracks.io.maps;
|
package com.google.android.apps.mytracks.io.gdata.maps;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Metadata about a maps feature.
|
* Metadata about a maps feature.
|
||||||
+4
-8
@@ -1,5 +1,5 @@
|
|||||||
// Copyright 2009 Google Inc. All Rights Reserved.
|
// Copyright 2009 Google Inc. All Rights Reserved.
|
||||||
package com.google.android.apps.mytracks.io.maps;
|
package com.google.android.apps.mytracks.io.gdata.maps;
|
||||||
|
|
||||||
import com.google.wireless.gdata.data.Entry;
|
import com.google.wireless.gdata.data.Entry;
|
||||||
import com.google.wireless.gdata.data.StringUtils;
|
import com.google.wireless.gdata.data.StringUtils;
|
||||||
@@ -17,7 +17,7 @@ import org.xmlpull.v1.XmlSerializer;
|
|||||||
/**
|
/**
|
||||||
* Converter from GData objects to Maps objects.
|
* Converter from GData objects to Maps objects.
|
||||||
*/
|
*/
|
||||||
class MapsGDataConverter {
|
public class MapsGDataConverter {
|
||||||
|
|
||||||
private final XmlSerializer xmlSerializer;
|
private final XmlSerializer xmlSerializer;
|
||||||
|
|
||||||
@@ -34,11 +34,7 @@ class MapsGDataConverter {
|
|||||||
metadata.setSearchable(false);
|
metadata.setSearchable(false);
|
||||||
}
|
}
|
||||||
metadata.setTitle(entry.getTitle());
|
metadata.setTitle(entry.getTitle());
|
||||||
|
metadata.setDescription(entry.getSummary());
|
||||||
String desc = entry.getContent();
|
|
||||||
if (desc.length() > 12) {
|
|
||||||
metadata.setDescription(desc.substring(9, desc.length() - 3));
|
|
||||||
}
|
|
||||||
|
|
||||||
String editUri = entry.getEditUri();
|
String editUri = entry.getEditUri();
|
||||||
if (editUri != null) {
|
if (editUri != null) {
|
||||||
@@ -56,7 +52,7 @@ class MapsGDataConverter {
|
|||||||
MapFeatureEntry entry = new MapFeatureEntry();
|
MapFeatureEntry entry = new MapFeatureEntry();
|
||||||
entry.setEditUri(metadata.getGDataEditUri());
|
entry.setEditUri(metadata.getGDataEditUri());
|
||||||
entry.setTitle(metadata.getTitle());
|
entry.setTitle(metadata.getTitle());
|
||||||
entry.setContent(metadata.getDescription());
|
entry.setSummary(metadata.getDescription());
|
||||||
entry.setPrivacy(metadata.getSearchable() ? "public" : "unlisted");
|
entry.setPrivacy(metadata.getSearchable() ? "public" : "unlisted");
|
||||||
entry.setAuthor("android");
|
entry.setAuthor("android");
|
||||||
entry.setEmail("nobody@google.com");
|
entry.setEmail("nobody@google.com");
|
||||||
+2
-2
@@ -1,10 +1,10 @@
|
|||||||
// Copyright 2009 Google Inc. All Rights Reserved.
|
// Copyright 2009 Google Inc. All Rights Reserved.
|
||||||
package com.google.android.apps.mytracks.io.maps;
|
package com.google.android.apps.mytracks.io.gdata.maps;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Metadata about a Google Maps map.
|
* Metadata about a Google Maps map.
|
||||||
*/
|
*/
|
||||||
class MapsMapMetadata {
|
public class MapsMapMetadata {
|
||||||
|
|
||||||
private String title;
|
private String title;
|
||||||
private String description;
|
private String description;
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
// Copyright 2009 Google Inc. All Rights Reserved.
|
// Copyright 2009 Google Inc. All Rights Reserved.
|
||||||
package com.google.android.apps.mytracks.io.maps;
|
package com.google.android.apps.mytracks.io.gdata.maps;
|
||||||
|
|
||||||
import com.google.wireless.gdata.data.Entry;
|
import com.google.wireless.gdata.data.Entry;
|
||||||
import com.google.wireless.gdata.data.Feed;
|
import com.google.wireless.gdata.data.Feed;
|
||||||
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
// Copyright 2010 Google Inc. All Rights Reserved.
|
// Copyright 2010 Google Inc. All Rights Reserved.
|
||||||
package com.google.android.apps.mytracks.io.maps;
|
package com.google.android.apps.mytracks.io.gdata.maps;
|
||||||
|
|
||||||
import com.google.wireless.gdata.client.GDataParserFactory;
|
import com.google.wireless.gdata.client.GDataParserFactory;
|
||||||
import com.google.wireless.gdata.data.Entry;
|
import com.google.wireless.gdata.data.Entry;
|
||||||
@@ -21,7 +21,7 @@ import org.xmlpull.v1.XmlPullParserException;
|
|||||||
/**
|
/**
|
||||||
* Factory of Xml parsers for gdata maps data.
|
* Factory of Xml parsers for gdata maps data.
|
||||||
*/
|
*/
|
||||||
class XmlMapsGDataParserFactory implements GDataParserFactory {
|
public class XmlMapsGDataParserFactory implements GDataParserFactory {
|
||||||
private XmlParserFactory xmlFactory;
|
private XmlParserFactory xmlFactory;
|
||||||
|
|
||||||
public XmlMapsGDataParserFactory(XmlParserFactory xmlFactory) {
|
public XmlMapsGDataParserFactory(XmlParserFactory xmlFactory) {
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
// Copyright 2010 Google Inc. All Rights Reserved.
|
// Copyright 2010 Google Inc. All Rights Reserved.
|
||||||
package com.google.android.apps.mytracks.io.maps;
|
package com.google.android.apps.mytracks.io.gdata.maps;
|
||||||
|
|
||||||
import com.google.wireless.gdata.data.StringUtils;
|
import com.google.wireless.gdata.data.StringUtils;
|
||||||
import com.google.wireless.gdata.parser.ParseException;
|
import com.google.wireless.gdata.parser.ParseException;
|
||||||
@@ -0,0 +1,236 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012 Google Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||||
|
* use this file except in compliance with the License. You may obtain a copy of
|
||||||
|
* the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations under
|
||||||
|
* the License.
|
||||||
|
*/
|
||||||
|
package com.google.android.apps.mytracks.io.maps;
|
||||||
|
|
||||||
|
import com.google.android.apps.mytracks.io.gdata.maps.MapsMapMetadata;
|
||||||
|
import com.google.android.apps.mytracks.io.sendtogoogle.SendRequest;
|
||||||
|
import com.google.android.maps.mytracks.R;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.app.AlertDialog;
|
||||||
|
import android.app.Dialog;
|
||||||
|
import android.app.ProgressDialog;
|
||||||
|
import android.content.DialogInterface;
|
||||||
|
import android.content.DialogInterface.OnCancelListener;
|
||||||
|
import android.content.DialogInterface.OnClickListener;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.AdapterView;
|
||||||
|
import android.widget.ArrayAdapter;
|
||||||
|
import android.widget.ListView;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An activity to choose a Google Map.
|
||||||
|
*
|
||||||
|
* @author jshih@google.com (Jimmy Shih)
|
||||||
|
*/
|
||||||
|
public class ChooseMapActivity extends Activity {
|
||||||
|
|
||||||
|
private static final int PROGRESS_DIALOG = 1;
|
||||||
|
private static final int ERROR_DIALOG = 2;
|
||||||
|
|
||||||
|
private SendRequest sendRequest;
|
||||||
|
private ChooseMapAsyncTask asyncTask;
|
||||||
|
private ProgressDialog progressDialog;
|
||||||
|
private ArrayAdapter<ListItem> arrayAdapter;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
sendRequest = getIntent().getParcelableExtra(SendRequest.SEND_REQUEST_KEY);
|
||||||
|
setContentView(R.layout.choose_map);
|
||||||
|
|
||||||
|
arrayAdapter = new ArrayAdapter<ListItem>(this, R.layout.choose_map_item, new ArrayList<
|
||||||
|
ListItem>()) {
|
||||||
|
@Override
|
||||||
|
public View getView(int position, View convertView, ViewGroup parent) {
|
||||||
|
if (convertView == null) {
|
||||||
|
convertView = getLayoutInflater().inflate(R.layout.choose_map_item, parent, false);
|
||||||
|
}
|
||||||
|
MapsMapMetadata mapData = getItem(position).getMapData();
|
||||||
|
|
||||||
|
TextView title = (TextView) convertView.findViewById(R.id.choose_map_list_item_title);
|
||||||
|
title.setText(mapData.getTitle());
|
||||||
|
|
||||||
|
TextView description = (TextView) convertView.findViewById(
|
||||||
|
R.id.choose_map_list_item_description);
|
||||||
|
String descriptionText = mapData.getDescription();
|
||||||
|
if (descriptionText == null || descriptionText.equals("")) {
|
||||||
|
description.setVisibility(View.GONE);
|
||||||
|
} else {
|
||||||
|
description.setVisibility(View.VISIBLE);
|
||||||
|
description.setText(descriptionText);
|
||||||
|
}
|
||||||
|
|
||||||
|
TextView searchStatus = (TextView) convertView.findViewById(
|
||||||
|
R.id.choose_map_list_item_search_status);
|
||||||
|
searchStatus.setTextColor(mapData.getSearchable() ? Color.RED : Color.GREEN);
|
||||||
|
searchStatus.setText(mapData.getSearchable() ? R.string.maps_list_public_label
|
||||||
|
: R.string.maps_list_unlisted_label);
|
||||||
|
return convertView;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ListView list = (ListView) findViewById(R.id.choose_map_list_view);
|
||||||
|
list.setEmptyView(findViewById(R.id.choose_map_empty_view));
|
||||||
|
list.setOnItemClickListener(new AdapterView.OnItemClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
|
||||||
|
startNextActivity(arrayAdapter.getItem(position).getMapId());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
list.setAdapter(arrayAdapter);
|
||||||
|
|
||||||
|
Object retained = getLastNonConfigurationInstance();
|
||||||
|
if (retained instanceof ChooseMapAsyncTask) {
|
||||||
|
asyncTask = (ChooseMapAsyncTask) retained;
|
||||||
|
asyncTask.setActivity(this);
|
||||||
|
} else {
|
||||||
|
asyncTask = new ChooseMapAsyncTask(this, sendRequest.getAccount());
|
||||||
|
asyncTask.execute();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object onRetainNonConfigurationInstance() {
|
||||||
|
asyncTask.setActivity(null);
|
||||||
|
return asyncTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Dialog onCreateDialog(int id) {
|
||||||
|
switch (id) {
|
||||||
|
case PROGRESS_DIALOG:
|
||||||
|
progressDialog = new ProgressDialog(this);
|
||||||
|
progressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
|
||||||
|
progressDialog.setMessage(getString(R.string.maps_list_loading));
|
||||||
|
progressDialog.setCancelable(true);
|
||||||
|
progressDialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
|
||||||
|
@Override
|
||||||
|
public void onCancel(DialogInterface dialog) {
|
||||||
|
asyncTask.cancel(true);
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
progressDialog.setIcon(android.R.drawable.ic_dialog_info);
|
||||||
|
progressDialog.setTitle(R.string.generic_progress_title);
|
||||||
|
return progressDialog;
|
||||||
|
case ERROR_DIALOG:
|
||||||
|
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||||
|
builder.setIcon(android.R.drawable.ic_dialog_alert);
|
||||||
|
builder.setTitle(R.string.generic_error_title);
|
||||||
|
builder.setMessage(R.string.maps_list_error);
|
||||||
|
builder.setPositiveButton(R.string.generic_ok, new OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(DialogInterface dialog, int arg1) {
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
builder.setOnCancelListener(new OnCancelListener() {
|
||||||
|
@Override
|
||||||
|
public void onCancel(DialogInterface dialog) {
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return builder.create();
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invokes when the associated AsyncTask completes.
|
||||||
|
*
|
||||||
|
* @param success true if success
|
||||||
|
* @param mapIds an array of map ids
|
||||||
|
* @param mapData an array of map data
|
||||||
|
*/
|
||||||
|
public void onAsyncTaskCompleted(
|
||||||
|
boolean success, ArrayList<String> mapIds, ArrayList<MapsMapMetadata> mapData) {
|
||||||
|
removeDialog(PROGRESS_DIALOG);
|
||||||
|
if (success) {
|
||||||
|
arrayAdapter.clear();
|
||||||
|
// To prevent displaying the emptyView message momentarily before the
|
||||||
|
// arrayAdapter is set, don't set the emptyView message in the xml layout.
|
||||||
|
// Instead, set it only when needed.
|
||||||
|
if (mapIds.size() == 0) {
|
||||||
|
TextView emptyView = (TextView) findViewById(R.id.choose_map_empty_view);
|
||||||
|
emptyView.setText(R.string.maps_list_no_maps);
|
||||||
|
} else {
|
||||||
|
for (int i = 0; i < mapIds.size(); i++) {
|
||||||
|
arrayAdapter.add(new ListItem(mapIds.get(i), mapData.get(i)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showDialog(ERROR_DIALOG);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows the progress dialog.
|
||||||
|
*/
|
||||||
|
public void showProgressDialog() {
|
||||||
|
showDialog(PROGRESS_DIALOG);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the next activity, {@link SendMapsActivity}.
|
||||||
|
*
|
||||||
|
* @param mapId the chosen map id
|
||||||
|
*/
|
||||||
|
private void startNextActivity(String mapId) {
|
||||||
|
sendRequest.setMapId(mapId);
|
||||||
|
Intent intent = new Intent(this, SendMapsActivity.class)
|
||||||
|
.putExtra(SendRequest.SEND_REQUEST_KEY, sendRequest);
|
||||||
|
startActivity(intent);
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A class containing {@link ChooseMapActivity} list item.
|
||||||
|
*
|
||||||
|
* @author jshih@google.com (Jimmy Shih)
|
||||||
|
*/
|
||||||
|
private class ListItem {
|
||||||
|
private String mapId;
|
||||||
|
private MapsMapMetadata mapData;
|
||||||
|
|
||||||
|
private ListItem(String mapId, MapsMapMetadata mapData) {
|
||||||
|
this.mapId = mapId;
|
||||||
|
this.mapData = mapData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the map id.
|
||||||
|
*/
|
||||||
|
public String getMapId() {
|
||||||
|
return mapId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the map data.
|
||||||
|
*/
|
||||||
|
public MapsMapMetadata getMapData() {
|
||||||
|
return mapData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012 Google Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||||
|
* use this file except in compliance with the License. You may obtain a copy of
|
||||||
|
* the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations under
|
||||||
|
* the License.
|
||||||
|
*/
|
||||||
|
package com.google.android.apps.mytracks.io.maps;
|
||||||
|
|
||||||
|
import com.google.android.apps.mytracks.io.gdata.GDataClientFactory;
|
||||||
|
import com.google.android.apps.mytracks.io.gdata.maps.MapFeatureEntry;
|
||||||
|
import com.google.android.apps.mytracks.io.gdata.maps.MapsClient;
|
||||||
|
import com.google.android.apps.mytracks.io.gdata.maps.MapsConstants;
|
||||||
|
import com.google.android.apps.mytracks.io.gdata.maps.MapsGDataConverter;
|
||||||
|
import com.google.android.apps.mytracks.io.gdata.maps.MapsMapMetadata;
|
||||||
|
import com.google.android.apps.mytracks.io.gdata.maps.XmlMapsGDataParserFactory;
|
||||||
|
import com.google.android.common.gdata.AndroidXmlParserFactory;
|
||||||
|
import com.google.wireless.gdata.client.GDataClient;
|
||||||
|
import com.google.wireless.gdata.client.HttpException;
|
||||||
|
import com.google.wireless.gdata.parser.GDataParser;
|
||||||
|
import com.google.wireless.gdata.parser.ParseException;
|
||||||
|
|
||||||
|
import android.accounts.Account;
|
||||||
|
import android.accounts.AccountManager;
|
||||||
|
import android.accounts.AuthenticatorException;
|
||||||
|
import android.accounts.OperationCanceledException;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.AsyncTask;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AsyncTask for {@link ChooseMapActivity} to get all the maps from Google Maps.
|
||||||
|
*
|
||||||
|
* @author jshih@google.com (Jimmy Shih)
|
||||||
|
*/
|
||||||
|
public class ChooseMapAsyncTask extends AsyncTask<Void, Integer, Boolean> {
|
||||||
|
private static final String TAG = ChooseMapAsyncTask.class.getSimpleName();
|
||||||
|
|
||||||
|
private ChooseMapActivity activity;
|
||||||
|
private final Account account;
|
||||||
|
private final Context context;
|
||||||
|
private final GDataClient gDataClient;
|
||||||
|
private final MapsClient mapsClient;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if can retry sending to Google Fusion Tables.
|
||||||
|
*/
|
||||||
|
private boolean canRetry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if the AsyncTask has completed.
|
||||||
|
*/
|
||||||
|
private boolean completed;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if the result is success.
|
||||||
|
*/
|
||||||
|
private boolean success;
|
||||||
|
|
||||||
|
// The following variables are for per request states
|
||||||
|
private String authToken;
|
||||||
|
private ArrayList<String> mapIds;
|
||||||
|
private ArrayList<MapsMapMetadata> mapData;
|
||||||
|
|
||||||
|
public ChooseMapAsyncTask(ChooseMapActivity activity, Account account) {
|
||||||
|
this.activity = activity;
|
||||||
|
this.account = account;
|
||||||
|
|
||||||
|
context = activity.getApplicationContext();
|
||||||
|
gDataClient = GDataClientFactory.getGDataClient(context);
|
||||||
|
mapsClient = new MapsClient(
|
||||||
|
gDataClient, new XmlMapsGDataParserFactory(new AndroidXmlParserFactory()));
|
||||||
|
|
||||||
|
canRetry = true;
|
||||||
|
completed = false;
|
||||||
|
success = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the activity associated with this AyncTask.
|
||||||
|
*
|
||||||
|
* @param activity the activity.
|
||||||
|
*/
|
||||||
|
public void setActivity(ChooseMapActivity activity) {
|
||||||
|
this.activity = activity;
|
||||||
|
if (completed && activity != null) {
|
||||||
|
activity.onAsyncTaskCompleted(success, mapIds, mapData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPreExecute() {
|
||||||
|
activity.showProgressDialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Boolean doInBackground(Void... params) {
|
||||||
|
return getMaps();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCancelled() {
|
||||||
|
closeClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPostExecute(Boolean result) {
|
||||||
|
closeClient();
|
||||||
|
success = result;
|
||||||
|
completed = true;
|
||||||
|
if (activity != null) {
|
||||||
|
activity.onAsyncTaskCompleted(success, mapIds, mapData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the gdata client.
|
||||||
|
*/
|
||||||
|
private void closeClient() {
|
||||||
|
if (gDataClient != null) {
|
||||||
|
gDataClient.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets all the maps from Google Maps.
|
||||||
|
*
|
||||||
|
* @return true if success.
|
||||||
|
*/
|
||||||
|
private boolean getMaps() {
|
||||||
|
// Reset the per request states
|
||||||
|
authToken = null;
|
||||||
|
mapIds = new ArrayList<String>();
|
||||||
|
mapData = new ArrayList<MapsMapMetadata>();
|
||||||
|
|
||||||
|
try {
|
||||||
|
authToken = AccountManager.get(context).blockingGetAuthToken(
|
||||||
|
account, MapsConstants.SERVICE_NAME, false);
|
||||||
|
} catch (OperationCanceledException e) {
|
||||||
|
Log.d(TAG, "Unable to get auth token", e);
|
||||||
|
return retryUpload();
|
||||||
|
} catch (AuthenticatorException e) {
|
||||||
|
Log.d(TAG, "Unable to get auth token", e);
|
||||||
|
return retryUpload();
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.d(TAG, "Unable to get auth token", e);
|
||||||
|
return retryUpload();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCancelled()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
GDataParser gDataParser = null;
|
||||||
|
try {
|
||||||
|
gDataParser = mapsClient.getParserForFeed(
|
||||||
|
MapFeatureEntry.class, MapsClient.getMapsFeed(), authToken);
|
||||||
|
gDataParser.init();
|
||||||
|
while (gDataParser.hasMoreData()) {
|
||||||
|
MapFeatureEntry entry = (MapFeatureEntry) gDataParser.readNextEntry(null);
|
||||||
|
mapIds.add(MapsGDataConverter.getMapidForEntry(entry));
|
||||||
|
mapData.add(MapsGDataConverter.getMapMetadataForEntry(entry));
|
||||||
|
}
|
||||||
|
} catch (ParseException e) {
|
||||||
|
Log.d(TAG, "Unable to get maps", e);
|
||||||
|
return retryUpload();
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.d(TAG, "Unable to get maps", e);
|
||||||
|
return retryUpload();
|
||||||
|
} catch (HttpException e) {
|
||||||
|
Log.d(TAG, "Unable to get maps", e);
|
||||||
|
return retryUpload();
|
||||||
|
} finally {
|
||||||
|
if (gDataParser != null) {
|
||||||
|
gDataParser.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retries upload. Invalidates the authToken. If can retry, invokes
|
||||||
|
* {@link ChooseMapAsyncTask#getMaps()}. Returns false if cannot retry.
|
||||||
|
*/
|
||||||
|
private boolean retryUpload() {
|
||||||
|
if (isCancelled()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
AccountManager.get(context).invalidateAuthToken(MapsConstants.SERVICE_NAME, authToken);
|
||||||
|
if (canRetry) {
|
||||||
|
canRetry = false;
|
||||||
|
return getMaps();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
// Copyright 2009 Google Inc. All Rights Reserved.
|
|
||||||
package com.google.android.apps.mytracks.io.maps;
|
|
||||||
|
|
||||||
import com.google.android.maps.GeoPoint;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A rectangle in geographical space.
|
|
||||||
*/
|
|
||||||
class GeoRect {
|
|
||||||
public int top;
|
|
||||||
public int left;
|
|
||||||
public int bottom;
|
|
||||||
public int right;
|
|
||||||
|
|
||||||
public GeoRect() {
|
|
||||||
top = 0;
|
|
||||||
left = 0;
|
|
||||||
bottom = 0;
|
|
||||||
right = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public GeoRect(GeoPoint center, int latSpan, int longSpan) {
|
|
||||||
top = center.getLatitudeE6() - latSpan / 2;
|
|
||||||
left = center.getLongitudeE6() - longSpan / 2;
|
|
||||||
bottom = center.getLatitudeE6() + latSpan / 2;
|
|
||||||
right = center.getLongitudeE6() + longSpan / 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
public GeoPoint getCenter() {
|
|
||||||
return new GeoPoint(top / 2 + bottom / 2, left / 2 + right / 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getLatSpan() {
|
|
||||||
return bottom - top;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getLongSpan() {
|
|
||||||
return right - left;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean contains(GeoPoint geoPoint) {
|
|
||||||
if (geoPoint.getLatitudeE6() >= top
|
|
||||||
&& geoPoint.getLatitudeE6() <= bottom
|
|
||||||
&& geoPoint.getLongitudeE6() >= left
|
|
||||||
&& geoPoint.getLongitudeE6() <= right) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,379 +0,0 @@
|
|||||||
// Copyright 2011 Google Inc. All Rights Reserved.
|
|
||||||
package com.google.android.apps.mytracks.io.maps;
|
|
||||||
|
|
||||||
import com.google.android.apps.mytracks.content.Waypoint;
|
|
||||||
import com.google.android.apps.mytracks.io.AuthManager;
|
|
||||||
import com.google.android.apps.mytracks.util.StringUtils;
|
|
||||||
import com.google.android.maps.GeoPoint;
|
|
||||||
import com.google.android.maps.mytracks.R;
|
|
||||||
import com.google.wireless.gdata.client.HttpException;
|
|
||||||
import com.google.wireless.gdata.data.Entry;
|
|
||||||
import com.google.wireless.gdata.parser.GDataParser;
|
|
||||||
import com.google.wireless.gdata.parser.ParseException;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.location.Location;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.Collection;
|
|
||||||
|
|
||||||
import org.xmlpull.v1.XmlPullParserException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Single interface which abstracts all access to the Google Maps service.
|
|
||||||
*
|
|
||||||
* @author Rodrigo Damazio
|
|
||||||
*/
|
|
||||||
public class MapsFacade {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Interface for receiving data back from getMapsList.
|
|
||||||
* All calls to the interface will happen before getMapsList returns.
|
|
||||||
*/
|
|
||||||
public interface MapsListCallback {
|
|
||||||
void onReceivedMapListing(String mapId, String title, String description,
|
|
||||||
boolean isPublic);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final String END_ICON_URL =
|
|
||||||
"http://maps.google.com/mapfiles/ms/micons/red-dot.png";
|
|
||||||
private static final String START_ICON_URL =
|
|
||||||
"http://maps.google.com/mapfiles/ms/micons/green-dot.png";
|
|
||||||
|
|
||||||
private final Context context;
|
|
||||||
private final MapsGDataWrapper wrapper;
|
|
||||||
private final MapsGDataConverter gdataConverter;
|
|
||||||
private final String authToken;
|
|
||||||
|
|
||||||
public MapsFacade(Context context, AuthManager auth) {
|
|
||||||
this.context = context;
|
|
||||||
this.authToken = auth.getAuthToken();
|
|
||||||
|
|
||||||
wrapper = new MapsGDataWrapper(context, auth);
|
|
||||||
wrapper.setRetryOnAuthFailure(true);
|
|
||||||
|
|
||||||
try {
|
|
||||||
gdataConverter = new MapsGDataConverter();
|
|
||||||
} catch (XmlPullParserException e) {
|
|
||||||
throw new IllegalStateException("Unable to create maps data converter", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String buildMapUrl(String mapId) {
|
|
||||||
return MapsClient.buildMapUrl(mapId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a list of all maps for the current user.
|
|
||||||
*
|
|
||||||
* @param callback callback to call for each map returned
|
|
||||||
* @return true on success, false otherwise
|
|
||||||
*/
|
|
||||||
public boolean getMapsList(final MapsListCallback callback) {
|
|
||||||
return wrapper.runQuery(new MapsGDataWrapper.QueryFunction() {
|
|
||||||
@Override
|
|
||||||
public void query(MapsClient client) throws IOException, Exception {
|
|
||||||
GDataParser listParser = client.getParserForFeed(
|
|
||||||
MapFeatureEntry.class, MapsClient.getMapsFeed(), authToken);
|
|
||||||
listParser.init();
|
|
||||||
while (listParser.hasMoreData()) {
|
|
||||||
MapFeatureEntry entry =
|
|
||||||
(MapFeatureEntry) listParser.readNextEntry(null);
|
|
||||||
MapsMapMetadata metadata =
|
|
||||||
MapsGDataConverter.getMapMetadataForEntry(entry);
|
|
||||||
String mapId = MapsGDataConverter.getMapidForEntry(entry);
|
|
||||||
|
|
||||||
callback.onReceivedMapListing(
|
|
||||||
mapId, metadata.getTitle(), metadata.getDescription(), metadata.getSearchable());
|
|
||||||
}
|
|
||||||
listParser.close();
|
|
||||||
listParser = null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new map for the current user.
|
|
||||||
*
|
|
||||||
* @param title title of the map
|
|
||||||
* @param category category of the map
|
|
||||||
* @param description description for the map
|
|
||||||
* @param isPublic whether the map should be public
|
|
||||||
* @param mapIdBuilder builder to append the map ID to
|
|
||||||
* @return true on success, false otherwise
|
|
||||||
*/
|
|
||||||
public boolean createNewMap(
|
|
||||||
final String title, final String category, final String description,
|
|
||||||
final boolean isPublic, final StringBuilder mapIdBuilder) {
|
|
||||||
if (mapIdBuilder.length() > 0) {
|
|
||||||
throw new IllegalArgumentException("mapIdBuilder should be empty");
|
|
||||||
}
|
|
||||||
|
|
||||||
return wrapper.runQuery(new MapsGDataWrapper.QueryFunction() {
|
|
||||||
@Override
|
|
||||||
public void query(MapsClient client) throws IOException, Exception {
|
|
||||||
Log.d(MapsConstants.TAG, "Creating a new map.");
|
|
||||||
String mapFeed = MapsClient.getMapsFeed();
|
|
||||||
Log.d(MapsConstants.TAG, "Map feed is " + mapFeed);
|
|
||||||
MapsMapMetadata metaData = new MapsMapMetadata();
|
|
||||||
metaData.setTitle(title);
|
|
||||||
metaData.setDescription(description + " - "
|
|
||||||
+ category + " - " + StringUtils.getCreatedByMyTracks(context, false));
|
|
||||||
metaData.setSearchable(isPublic);
|
|
||||||
Entry entry = MapsGDataConverter.getMapEntryForMetadata(metaData);
|
|
||||||
Log.d(MapsConstants.TAG, "Title: " + entry.getTitle());
|
|
||||||
Entry map = client.createEntry(mapFeed, authToken, entry);
|
|
||||||
|
|
||||||
String mapId = MapsClient.getMapIdFromMapEntryId(map.getId());
|
|
||||||
mapIdBuilder.append(mapId);
|
|
||||||
Log.d(MapsConstants.TAG, "New map id is: " + mapId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Uploads a single start or end marker to the given map.
|
|
||||||
*
|
|
||||||
* @param mapId ID of the map to upload to
|
|
||||||
* @param trackName name of the track being started/ended
|
|
||||||
* @param trackDescription description of the track being started/ended
|
|
||||||
* @param loc the location of the marker
|
|
||||||
* @param isStart true to add a start marker, false to add an end marker
|
|
||||||
* @return true on success, false otherwise
|
|
||||||
*/
|
|
||||||
public boolean uploadMarker(final String mapId, final String trackName,
|
|
||||||
final String trackDescription, final Location loc, final boolean isStart) {
|
|
||||||
return wrapper.runQuery(new MapsGDataWrapper.QueryFunction() {
|
|
||||||
@Override
|
|
||||||
public void query(MapsClient client)
|
|
||||||
throws IOException, Exception {
|
|
||||||
String featureFeed = MapsClient.getFeaturesFeed(mapId);
|
|
||||||
GeoPoint geoPoint = getGeoPoint(loc);
|
|
||||||
insertMarker(client, featureFeed, trackName, trackDescription, geoPoint, isStart);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Inserts a place mark. Second try if 1st try fails. Will throw exception on
|
|
||||||
* 2nd failure.
|
|
||||||
*/
|
|
||||||
private void insertMarker(MapsClient client,
|
|
||||||
String featureFeed,
|
|
||||||
String trackName, String trackDescription,
|
|
||||||
GeoPoint geoPoint,
|
|
||||||
boolean isStart) throws IOException, Exception {
|
|
||||||
MapsFeature feature =
|
|
||||||
buildMapsPlacemarkFeature(trackName, trackDescription, geoPoint, isStart);
|
|
||||||
Entry entry = gdataConverter.getEntryForFeature(feature);
|
|
||||||
Log.d(MapsConstants.TAG, "SendToMaps: Creating placemark "
|
|
||||||
+ entry.getTitle());
|
|
||||||
try {
|
|
||||||
client.createEntry(featureFeed, authToken, entry);
|
|
||||||
Log.d(MapsConstants.TAG, "SendToMaps: createEntry success!");
|
|
||||||
} catch (IOException e) {
|
|
||||||
Log.w(MapsConstants.TAG,
|
|
||||||
"SendToMaps: createEntry 1st try failed. Trying again.");
|
|
||||||
// Retry once (often IOException is thrown on a timeout):
|
|
||||||
client.createEntry(featureFeed, authToken, entry);
|
|
||||||
Log.d(MapsConstants.TAG,
|
|
||||||
"SendToMaps: createEntry success on 2nd try!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Builds a placemark MapsFeature from a track.
|
|
||||||
*
|
|
||||||
* @param trackName the track
|
|
||||||
* @param trackDescription the track description
|
|
||||||
* @param geoPoint the geo point
|
|
||||||
* @param isStart true if it's the start of the track, or false for end
|
|
||||||
* @return a MapsFeature
|
|
||||||
*/
|
|
||||||
private MapsFeature buildMapsPlacemarkFeature(
|
|
||||||
String trackName, String trackDescription,
|
|
||||||
GeoPoint geoPoint, boolean isStart) {
|
|
||||||
String iconUrl;
|
|
||||||
if (isStart) {
|
|
||||||
iconUrl = START_ICON_URL;
|
|
||||||
} else {
|
|
||||||
iconUrl = END_ICON_URL;
|
|
||||||
}
|
|
||||||
String title = trackName + " "
|
|
||||||
+ (isStart ? context.getString(R.string.marker_label_start)
|
|
||||||
: context.getString(R.string.marker_label_end));
|
|
||||||
String description = isStart ? "" : trackDescription;
|
|
||||||
return buildMapsPlacemarkFeature(title, description, iconUrl, geoPoint);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Builds a MapsFeature from a waypoint.
|
|
||||||
*
|
|
||||||
* @param title the title
|
|
||||||
* @param description the description
|
|
||||||
* @param iconUrl the icon url
|
|
||||||
* @param geoPoint the waypoint
|
|
||||||
* @return a MapsFeature
|
|
||||||
*/
|
|
||||||
private static MapsFeature buildMapsPlacemarkFeature(
|
|
||||||
String title, String description, String iconUrl, GeoPoint geoPoint) {
|
|
||||||
MapsFeature mapsFeature = new MapsFeature();
|
|
||||||
mapsFeature.generateAndroidId();
|
|
||||||
mapsFeature.setType(MapsFeature.MARKER);
|
|
||||||
mapsFeature.setIconUrl(iconUrl);
|
|
||||||
mapsFeature.setDescription(description);
|
|
||||||
mapsFeature.addPoint(geoPoint);
|
|
||||||
if (TextUtils.isEmpty(title)) {
|
|
||||||
// Features must have a name (otherwise GData upload may fail):
|
|
||||||
mapsFeature.setTitle("-");
|
|
||||||
} else {
|
|
||||||
mapsFeature.setTitle(title);
|
|
||||||
}
|
|
||||||
mapsFeature.setDescription(description.replaceAll("\n", "<br>"));
|
|
||||||
return mapsFeature;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Uploads a series of waypoints to the given map.
|
|
||||||
*
|
|
||||||
* @param mapId ID of the map to upload to
|
|
||||||
* @param waypoints the waypoints to upload
|
|
||||||
* @return true on success, false otherwise
|
|
||||||
*/
|
|
||||||
public boolean uploadWaypoints(
|
|
||||||
final String mapId, final Iterable<Waypoint> waypoints) {
|
|
||||||
return wrapper.runQuery(new MapsGDataWrapper.QueryFunction() {
|
|
||||||
public void query(MapsClient client) {
|
|
||||||
// TODO(rdamazio): Stream through the waypoints in chunks.
|
|
||||||
// I am leaving the number of waypoints very high which should not be a
|
|
||||||
// problem because we don't try to load them into objects all at the
|
|
||||||
// same time.
|
|
||||||
String featureFeed = MapsClient.getFeaturesFeed(mapId);
|
|
||||||
|
|
||||||
try {
|
|
||||||
for (Waypoint waypoint : waypoints) {
|
|
||||||
MapsFeature feature = buildMapsPlacemarkFeature(
|
|
||||||
waypoint.getName(), waypoint.getDescription(), waypoint.getIcon(),
|
|
||||||
getGeoPoint(waypoint.getLocation()));
|
|
||||||
Entry entry = gdataConverter.getEntryForFeature(feature);
|
|
||||||
|
|
||||||
Log.d(MapsConstants.TAG,
|
|
||||||
"SendToMaps: Creating waypoint.");
|
|
||||||
try {
|
|
||||||
client.createEntry(featureFeed, authToken, entry);
|
|
||||||
Log.d(MapsConstants.TAG,
|
|
||||||
"SendToMaps: createEntry success!");
|
|
||||||
} catch (IOException e) {
|
|
||||||
Log.w(MapsConstants.TAG,
|
|
||||||
"SendToMaps: createEntry 1st try failed. Retrying.");
|
|
||||||
|
|
||||||
// Retry once (often IOException is thrown on a timeout):
|
|
||||||
client.createEntry(featureFeed, authToken, entry);
|
|
||||||
Log.d(MapsConstants.TAG,
|
|
||||||
"SendToMaps: createEntry success on 2nd try!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (ParseException e) {
|
|
||||||
Log.w(MapsConstants.TAG, "ParseException caught.", e);
|
|
||||||
} catch (HttpException e) {
|
|
||||||
Log.w(MapsConstants.TAG, "HttpException caught.", e);
|
|
||||||
} catch (IOException e) {
|
|
||||||
Log.w(MapsConstants.TAG, "IOException caught.", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Uploads a series of points to the given map.
|
|
||||||
*
|
|
||||||
* @param mapId ID of the map to upload to
|
|
||||||
* @param trackName the name of the track
|
|
||||||
* @param locations the locations to upload
|
|
||||||
* @return true on success, false otherwise
|
|
||||||
*/
|
|
||||||
public boolean uploadTrackPoints(
|
|
||||||
final String mapId, final String trackName, final Collection<Location> locations) {
|
|
||||||
return wrapper.runQuery(new MapsGDataWrapper.QueryFunction() {
|
|
||||||
@Override
|
|
||||||
public void query(MapsClient client)
|
|
||||||
throws IOException, Exception {
|
|
||||||
String featureFeed = MapsClient.getFeaturesFeed(mapId);
|
|
||||||
Log.d(MapsConstants.TAG, "Feature feed url: " + featureFeed);
|
|
||||||
uploadTrackPoints(client, featureFeed, trackName, locations);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean uploadTrackPoints(
|
|
||||||
MapsClient client,
|
|
||||||
String featureFeed,
|
|
||||||
String trackName, Collection<Location> locations)
|
|
||||||
throws IOException, Exception {
|
|
||||||
Entry entry = null;
|
|
||||||
int numLocations = locations.size();
|
|
||||||
if (numLocations < 2) {
|
|
||||||
// Need at least two points for a polyline:
|
|
||||||
Log.w(MapsConstants.TAG, "Not uploading too few points");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Put the line:
|
|
||||||
entry = gdataConverter.getEntryForFeature(
|
|
||||||
buildMapsLineFeature(trackName, locations));
|
|
||||||
Log.d(MapsConstants.TAG,
|
|
||||||
"SendToMaps: Creating line " + entry.getTitle());
|
|
||||||
try {
|
|
||||||
client.createEntry(featureFeed, authToken, entry);
|
|
||||||
Log.d(MapsConstants.TAG, "SendToMaps: createEntry success!");
|
|
||||||
} catch (IOException e) {
|
|
||||||
Log.w(MapsConstants.TAG,
|
|
||||||
"SendToMaps: createEntry 1st try failed. Trying again.");
|
|
||||||
// Retry once (often IOException is thrown on a timeout):
|
|
||||||
client.createEntry(featureFeed, authToken, entry);
|
|
||||||
Log.d(MapsConstants.TAG,
|
|
||||||
"SendToMaps: createEntry success on 2nd try!");
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Builds a MapsFeature from a track.
|
|
||||||
*
|
|
||||||
* @param trackName the track name
|
|
||||||
* @param locations locations on the track
|
|
||||||
* @return a MapsFeature
|
|
||||||
*/
|
|
||||||
private static MapsFeature buildMapsLineFeature(String trackName,
|
|
||||||
Iterable<Location> locations) {
|
|
||||||
MapsFeature mapsFeature = new MapsFeature();
|
|
||||||
mapsFeature.generateAndroidId();
|
|
||||||
mapsFeature.setType(MapsFeature.LINE);
|
|
||||||
if (TextUtils.isEmpty(trackName)) {
|
|
||||||
// Features must have a name (otherwise GData upload may fail):
|
|
||||||
mapsFeature.setTitle("-");
|
|
||||||
} else {
|
|
||||||
mapsFeature.setTitle(trackName);
|
|
||||||
}
|
|
||||||
mapsFeature.setColor(0x80FF0000);
|
|
||||||
for (Location loc : locations) {
|
|
||||||
mapsFeature.addPoint(getGeoPoint(loc));
|
|
||||||
}
|
|
||||||
return mapsFeature;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cleans up after a series of uploads.
|
|
||||||
* This closes the connection to Maps and resets retry counters.
|
|
||||||
*/
|
|
||||||
public void cleanUp() {
|
|
||||||
wrapper.cleanUp();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static GeoPoint getGeoPoint(Location location) {
|
|
||||||
return new GeoPoint((int) (location.getLatitude() * 1E6),
|
|
||||||
(int) (location.getLongitude() * 1E6));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,171 +0,0 @@
|
|||||||
// Copyright 2010 Google Inc. All Rights Reserved.
|
|
||||||
package com.google.android.apps.mytracks.io.maps;
|
|
||||||
|
|
||||||
import com.google.android.apps.mytracks.io.AuthManager;
|
|
||||||
import com.google.android.apps.mytracks.io.AuthManager.AuthCallback;
|
|
||||||
import com.google.android.apps.mytracks.io.gdata.GDataClientFactory;
|
|
||||||
import com.google.android.common.gdata.AndroidXmlParserFactory;
|
|
||||||
import com.google.wireless.gdata.client.GDataClient;
|
|
||||||
import com.google.wireless.gdata.client.HttpException;
|
|
||||||
import com.google.wireless.gdata.parser.xml.SimplePullParser.ParseException;
|
|
||||||
import com.google.wireless.gdata2.ConflictDetectedException;
|
|
||||||
import com.google.wireless.gdata2.client.AuthenticationException;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* MapsGDataWrapper provides a wrapper around GData operations that maintains
|
|
||||||
* the GData client, and provides a method to run gdata queries with proper
|
|
||||||
* error handling. After a query is run, the wrapper can be queried about the
|
|
||||||
* error that occurred.
|
|
||||||
*/
|
|
||||||
class MapsGDataWrapper {
|
|
||||||
/**
|
|
||||||
* A QueryFunction is passed in when executing a query. The query function
|
|
||||||
* of the class is called with the GData client as a parameter. The
|
|
||||||
* function should execute whatever operations it desires on the client
|
|
||||||
* without concern for whether the client will throw an error.
|
|
||||||
*/
|
|
||||||
interface QueryFunction {
|
|
||||||
public abstract void query(MapsClient client)
|
|
||||||
throws AuthenticationException, IOException, ParseException,
|
|
||||||
ConflictDetectedException, Exception;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The types of error that may be encountered
|
|
||||||
/** No error occurred. */
|
|
||||||
public static final int ERROR_NO_ERROR = 0;
|
|
||||||
/** There was an authentication error, the auth token may be invalid. */
|
|
||||||
public static final int ERROR_AUTH = 1;
|
|
||||||
/** There was an internal error on the server side. */
|
|
||||||
public static final int ERROR_INTERNAL = 2;
|
|
||||||
/** There was an error connecting to the server. */
|
|
||||||
public static final int ERROR_CONNECTION = 3;
|
|
||||||
/** The item queried did not exit. */
|
|
||||||
public static final int ERROR_NOT_FOUND = 4;
|
|
||||||
/** There was an error parsing or serializing locally. */
|
|
||||||
public static final int ERROR_LOCAL = 5;
|
|
||||||
/** There was a conflict, update the entry and try again. */
|
|
||||||
public static final int ERROR_CONFLICT = 6;
|
|
||||||
/**
|
|
||||||
* A query was run after cleaning up the wrapper, so the client was invalid.
|
|
||||||
*/
|
|
||||||
public static final int ERROR_CLEANED_UP = 7;
|
|
||||||
/** An unknown error occurred. */
|
|
||||||
public static final int ERROR_UNKNOWN = 100;
|
|
||||||
|
|
||||||
private final GDataClient androidGdataClient;
|
|
||||||
private final AuthManager auth;
|
|
||||||
private final MapsClient client;
|
|
||||||
|
|
||||||
private String errorMessage;
|
|
||||||
private int errorType;
|
|
||||||
private boolean retryOnAuthFailure;
|
|
||||||
private int retriesPending;
|
|
||||||
private boolean cleanupCalled;
|
|
||||||
|
|
||||||
public MapsGDataWrapper(Context context, AuthManager auth) {
|
|
||||||
androidGdataClient = GDataClientFactory.getGDataClient(context);
|
|
||||||
this.auth = auth;
|
|
||||||
client =
|
|
||||||
new MapsClient(androidGdataClient, new XmlMapsGDataParserFactory(
|
|
||||||
new AndroidXmlParserFactory()));
|
|
||||||
errorType = ERROR_NO_ERROR;
|
|
||||||
errorMessage = null;
|
|
||||||
retryOnAuthFailure = false;
|
|
||||||
retriesPending = 0;
|
|
||||||
cleanupCalled = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean runQuery(final QueryFunction query) {
|
|
||||||
if (client == null) {
|
|
||||||
errorType = ERROR_CLEANED_UP;
|
|
||||||
errorMessage = "GData Wrapper has already been cleaned up!";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
query.query(client);
|
|
||||||
errorType = ERROR_NO_ERROR;
|
|
||||||
errorMessage = null;
|
|
||||||
return true;
|
|
||||||
} catch (AuthenticationException e) {
|
|
||||||
Log.e(MapsConstants.TAG, "Exception", e);
|
|
||||||
errorType = ERROR_AUTH;
|
|
||||||
errorMessage = e.getMessage();
|
|
||||||
} catch (HttpException e) {
|
|
||||||
Log.e(MapsConstants.TAG, "HttpException", e);
|
|
||||||
errorMessage = e.getMessage();
|
|
||||||
if (errorMessage != null && errorMessage.contains("401")) {
|
|
||||||
errorType = ERROR_AUTH;
|
|
||||||
} else {
|
|
||||||
errorType = ERROR_CONNECTION;
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
Log.e(MapsConstants.TAG, "Exception", e);
|
|
||||||
errorMessage = e.getMessage();
|
|
||||||
if (errorMessage != null && errorMessage.contains("503")) {
|
|
||||||
errorType = ERROR_INTERNAL;
|
|
||||||
} else {
|
|
||||||
errorType = ERROR_CONNECTION;
|
|
||||||
}
|
|
||||||
} catch (ParseException e) {
|
|
||||||
Log.e(MapsConstants.TAG, "Exception", e);
|
|
||||||
errorType = ERROR_LOCAL;
|
|
||||||
errorMessage = e.getMessage();
|
|
||||||
} catch (ConflictDetectedException e) {
|
|
||||||
Log.e(MapsConstants.TAG, "Exception", e);
|
|
||||||
errorType = ERROR_CONFLICT;
|
|
||||||
errorMessage = e.getMessage();
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.e(MapsConstants.TAG, "Exception", e);
|
|
||||||
errorType = ERROR_UNKNOWN;
|
|
||||||
errorMessage = e.getMessage();
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
Log.d(MapsConstants.TAG, "GData error encountered: " + errorMessage);
|
|
||||||
if (errorType == ERROR_AUTH && auth != null) {
|
|
||||||
AuthCallback whenFinished = null;
|
|
||||||
if (retryOnAuthFailure) {
|
|
||||||
retriesPending++;
|
|
||||||
whenFinished = new AuthCallback() {
|
|
||||||
@Override
|
|
||||||
public void onAuthResult(boolean success) {
|
|
||||||
retriesPending--;
|
|
||||||
retryOnAuthFailure = false;
|
|
||||||
runQuery(query);
|
|
||||||
if (cleanupCalled && retriesPending == 0) {
|
|
||||||
cleanUp();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
auth.invalidateAndRefresh(whenFinished);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getErrorType() {
|
|
||||||
return errorType;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getErrorMessage() {
|
|
||||||
return errorMessage;
|
|
||||||
}
|
|
||||||
|
|
||||||
// cleanUp must be called when done using this wrapper to close the client.
|
|
||||||
// Note that the cleanup will be delayed if auth failure retries were
|
|
||||||
// requested and there is a pending retry.
|
|
||||||
public void cleanUp() {
|
|
||||||
if (retriesPending == 0 && !cleanupCalled) {
|
|
||||||
androidGdataClient.close();
|
|
||||||
}
|
|
||||||
cleanupCalled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setRetryOnAuthFailure(boolean retry) {
|
|
||||||
retryOnAuthFailure = retry;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012 Google Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||||
|
* use this file except in compliance with the License. You may obtain a copy of
|
||||||
|
* the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations under
|
||||||
|
* the License.
|
||||||
|
*/
|
||||||
|
package com.google.android.apps.mytracks.io.maps;
|
||||||
|
|
||||||
|
import com.google.android.apps.mytracks.io.docs.SendDocsActivity;
|
||||||
|
import com.google.android.apps.mytracks.io.fusiontables.SendFusionTablesActivity;
|
||||||
|
import com.google.android.apps.mytracks.io.sendtogoogle.SendRequest;
|
||||||
|
import com.google.android.apps.mytracks.io.sendtogoogle.UploadResultActivity;
|
||||||
|
import com.google.android.maps.mytracks.R;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.app.Dialog;
|
||||||
|
import android.app.ProgressDialog;
|
||||||
|
import android.content.DialogInterface;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Bundle;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An activity to send a track to Google Maps.
|
||||||
|
*
|
||||||
|
* @author Jimmy Shih
|
||||||
|
*/
|
||||||
|
public class SendMapsActivity extends Activity {
|
||||||
|
|
||||||
|
private static final int PROGRESS_DIALOG = 1;
|
||||||
|
|
||||||
|
private SendRequest sendRequest;
|
||||||
|
private SendMapsAsyncTask asyncTask;
|
||||||
|
private ProgressDialog progressDialog;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
sendRequest = getIntent().getParcelableExtra(SendRequest.SEND_REQUEST_KEY);
|
||||||
|
|
||||||
|
Object retained = getLastNonConfigurationInstance();
|
||||||
|
if (retained instanceof SendMapsAsyncTask) {
|
||||||
|
asyncTask = (SendMapsAsyncTask) retained;
|
||||||
|
asyncTask.setActivity(this);
|
||||||
|
} else {
|
||||||
|
asyncTask = new SendMapsAsyncTask(
|
||||||
|
this, sendRequest.getTrackId(), sendRequest.getAccount(), sendRequest.getMapId());
|
||||||
|
asyncTask.execute();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object onRetainNonConfigurationInstance() {
|
||||||
|
asyncTask.setActivity(null);
|
||||||
|
return asyncTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Dialog onCreateDialog(int id) {
|
||||||
|
switch (id) {
|
||||||
|
case PROGRESS_DIALOG:
|
||||||
|
progressDialog = new ProgressDialog(this);
|
||||||
|
progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
|
||||||
|
progressDialog.setIcon(android.R.drawable.ic_dialog_info);
|
||||||
|
progressDialog.setTitle(
|
||||||
|
getString(R.string.send_google_progress_title, getString(R.string.send_google_maps)));
|
||||||
|
progressDialog.setMax(100);
|
||||||
|
progressDialog.setProgress(0);
|
||||||
|
progressDialog.setCancelable(true);
|
||||||
|
progressDialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
|
||||||
|
@Override
|
||||||
|
public void onCancel(DialogInterface dialog) {
|
||||||
|
asyncTask.cancel(true);
|
||||||
|
startNextActivity(false, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return progressDialog;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invokes when the associated AsyncTask completes.
|
||||||
|
*
|
||||||
|
* @param success true if success
|
||||||
|
*/
|
||||||
|
public void onAsyncTaskCompleted(boolean success) {
|
||||||
|
startNextActivity(success, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows the progress dialog.
|
||||||
|
*/
|
||||||
|
public void showProgressDialog() {
|
||||||
|
showDialog(PROGRESS_DIALOG);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the progress dialog value.
|
||||||
|
*/
|
||||||
|
public void setProgressDialogValue(int value) {
|
||||||
|
if (progressDialog != null) {
|
||||||
|
progressDialog.setProgress(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the next activity.
|
||||||
|
*
|
||||||
|
* @param success true if sendMaps is success
|
||||||
|
* @param isCancel true if it is a cancel request
|
||||||
|
*/
|
||||||
|
private void startNextActivity(boolean success, boolean isCancel) {
|
||||||
|
sendRequest.setMapsSuccess(success);
|
||||||
|
|
||||||
|
Class<?> next;
|
||||||
|
if (isCancel) {
|
||||||
|
next = UploadResultActivity.class;
|
||||||
|
} else {
|
||||||
|
if (sendRequest.isSendFusionTables()) {
|
||||||
|
next = SendFusionTablesActivity.class;
|
||||||
|
} else if (sendRequest.isSendDocs()) {
|
||||||
|
next = SendDocsActivity.class;
|
||||||
|
} else {
|
||||||
|
next = UploadResultActivity.class;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Intent intent = new Intent(this, next)
|
||||||
|
.putExtra(SendRequest.SEND_REQUEST_KEY, sendRequest);
|
||||||
|
startActivity(intent);
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,558 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012 Google Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||||
|
* use this file except in compliance with the License. You may obtain a copy of
|
||||||
|
* the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations under
|
||||||
|
* the License.
|
||||||
|
*/
|
||||||
|
package com.google.android.apps.mytracks.io.maps;
|
||||||
|
|
||||||
|
import com.google.android.apps.mytracks.Constants;
|
||||||
|
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
|
||||||
|
import com.google.android.apps.mytracks.content.Track;
|
||||||
|
import com.google.android.apps.mytracks.content.Waypoint;
|
||||||
|
import com.google.android.apps.mytracks.io.gdata.GDataClientFactory;
|
||||||
|
import com.google.android.apps.mytracks.io.gdata.maps.MapsClient;
|
||||||
|
import com.google.android.apps.mytracks.io.gdata.maps.MapsConstants;
|
||||||
|
import com.google.android.apps.mytracks.io.gdata.maps.MapsGDataConverter;
|
||||||
|
import com.google.android.apps.mytracks.io.gdata.maps.XmlMapsGDataParserFactory;
|
||||||
|
import com.google.android.apps.mytracks.io.sendtogoogle.SendToGoogleUtils;
|
||||||
|
import com.google.android.apps.mytracks.stats.DoubleBuffer;
|
||||||
|
import com.google.android.apps.mytracks.util.LocationUtils;
|
||||||
|
import com.google.android.apps.mytracks.util.StringUtils;
|
||||||
|
import com.google.android.apps.mytracks.util.UnitConversions;
|
||||||
|
import com.google.android.common.gdata.AndroidXmlParserFactory;
|
||||||
|
import com.google.android.maps.mytracks.R;
|
||||||
|
import com.google.wireless.gdata.client.GDataClient;
|
||||||
|
import com.google.wireless.gdata.client.HttpException;
|
||||||
|
import com.google.wireless.gdata.parser.ParseException;
|
||||||
|
|
||||||
|
import android.accounts.Account;
|
||||||
|
import android.accounts.AccountManager;
|
||||||
|
import android.accounts.AuthenticatorException;
|
||||||
|
import android.accounts.OperationCanceledException;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.location.Location;
|
||||||
|
import android.os.AsyncTask;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Vector;
|
||||||
|
|
||||||
|
import org.xmlpull.v1.XmlPullParserException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AsyncTask to send a track to Google Maps.
|
||||||
|
* <p>
|
||||||
|
* IMPORTANT: While this code is Apache-licensed, please notice that usage of
|
||||||
|
* the Google Maps servers through this API is only allowed for the My Tracks
|
||||||
|
* application. Other applications looking to upload maps data should look into
|
||||||
|
* using the Google Fusion Tables API.
|
||||||
|
*
|
||||||
|
* @author Jimmy Shih
|
||||||
|
*/
|
||||||
|
public class SendMapsAsyncTask extends AsyncTask<Void, Integer, Boolean> {
|
||||||
|
private static final String START_ICON_URL =
|
||||||
|
"http://maps.google.com/mapfiles/ms/micons/green-dot.png";
|
||||||
|
private static final String END_ICON_URL =
|
||||||
|
"http://maps.google.com/mapfiles/ms/micons/red-dot.png";
|
||||||
|
private static final int MAX_POINTS_PER_UPLOAD = 500;
|
||||||
|
|
||||||
|
private static final int PROGRESS_FETCH_MAP_ID = 5;
|
||||||
|
private static final int PROGRESS_UPLOAD_DATA_MIN = 10;
|
||||||
|
private static final int PROGRESS_UPLOAD_DATA_MAX = 90;
|
||||||
|
private static final int PROGRESS_UPLOAD_WAYPOINTS = 95;
|
||||||
|
private static final int PROGRESS_COMPLETE = 100;
|
||||||
|
|
||||||
|
private static final String TAG = SendMapsAsyncTask.class.getSimpleName();
|
||||||
|
|
||||||
|
private SendMapsActivity activity;
|
||||||
|
private final long trackId;
|
||||||
|
private final Account account;
|
||||||
|
private final String chooseMapId;
|
||||||
|
private final Context context;
|
||||||
|
private final MyTracksProviderUtils myTracksProviderUtils;
|
||||||
|
private final GDataClient gDataClient;
|
||||||
|
private final MapsClient mapsClient;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if can retry sending to Google Maps.
|
||||||
|
*/
|
||||||
|
private boolean canRetry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if the AsyncTask has completed.
|
||||||
|
*/
|
||||||
|
private boolean completed;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if the result is success.
|
||||||
|
*/
|
||||||
|
private boolean success;
|
||||||
|
|
||||||
|
// The following variables are for per upload states
|
||||||
|
private MapsGDataConverter mapsGDataConverter;
|
||||||
|
private String authToken;
|
||||||
|
private String mapId;
|
||||||
|
int currentSegment;
|
||||||
|
|
||||||
|
public SendMapsAsyncTask(
|
||||||
|
SendMapsActivity activity, long trackId, Account account, String chooseMapId) {
|
||||||
|
this.activity = activity;
|
||||||
|
this.trackId = trackId;
|
||||||
|
this.account = account;
|
||||||
|
this.chooseMapId = chooseMapId;
|
||||||
|
|
||||||
|
context = activity.getApplicationContext();
|
||||||
|
myTracksProviderUtils = MyTracksProviderUtils.Factory.get(context);
|
||||||
|
gDataClient = GDataClientFactory.getGDataClient(context);
|
||||||
|
mapsClient = new MapsClient(
|
||||||
|
gDataClient, new XmlMapsGDataParserFactory(new AndroidXmlParserFactory()));
|
||||||
|
|
||||||
|
canRetry = true;
|
||||||
|
completed = false;
|
||||||
|
success = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the activity associated with this AyncTask.
|
||||||
|
*
|
||||||
|
* @param activity the activity.
|
||||||
|
*/
|
||||||
|
public void setActivity(SendMapsActivity activity) {
|
||||||
|
this.activity = activity;
|
||||||
|
if (completed && activity != null) {
|
||||||
|
activity.onAsyncTaskCompleted(success);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPreExecute() {
|
||||||
|
activity.showProgressDialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Boolean doInBackground(Void... params) {
|
||||||
|
return doUpload();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onProgressUpdate(Integer... values) {
|
||||||
|
if (activity != null) {
|
||||||
|
activity.setProgressDialogValue(values[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPostExecute(Boolean result) {
|
||||||
|
closeClient();
|
||||||
|
success = result;
|
||||||
|
completed = true;
|
||||||
|
if (success) {
|
||||||
|
Track track = myTracksProviderUtils.getTrack(trackId);
|
||||||
|
if (track != null) {
|
||||||
|
track.setMapId(mapId);
|
||||||
|
myTracksProviderUtils.updateTrack(track);
|
||||||
|
} else {
|
||||||
|
Log.d(TAG, "No track");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (activity != null) {
|
||||||
|
activity.onAsyncTaskCompleted(success);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCancelled() {
|
||||||
|
closeClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the gdata client.
|
||||||
|
*/
|
||||||
|
private void closeClient() {
|
||||||
|
if (gDataClient != null) {
|
||||||
|
gDataClient.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uploads a track to Google Maps.
|
||||||
|
*
|
||||||
|
* @return true if success.
|
||||||
|
*/
|
||||||
|
private boolean doUpload() {
|
||||||
|
// Reset the per upload states
|
||||||
|
mapsGDataConverter = null;
|
||||||
|
authToken = null;
|
||||||
|
mapId = null;
|
||||||
|
currentSegment = 1;
|
||||||
|
|
||||||
|
// Create a maps gdata converter
|
||||||
|
try {
|
||||||
|
mapsGDataConverter = new MapsGDataConverter();
|
||||||
|
} catch (XmlPullParserException e) {
|
||||||
|
Log.d(TAG, "Unable to create a maps gdata converter", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get auth token
|
||||||
|
try {
|
||||||
|
authToken = AccountManager.get(context).blockingGetAuthToken(
|
||||||
|
account, MapsConstants.SERVICE_NAME, false);
|
||||||
|
} catch (OperationCanceledException e) {
|
||||||
|
Log.d(TAG, "Unable to get auth token", e);
|
||||||
|
return retryUpload();
|
||||||
|
} catch (AuthenticatorException e) {
|
||||||
|
Log.d(TAG, "Unable to get auth token", e);
|
||||||
|
return retryUpload();
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.d(TAG, "Unable to get auth token", e);
|
||||||
|
return retryUpload();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the track
|
||||||
|
Track track = myTracksProviderUtils.getTrack(trackId);
|
||||||
|
if (track == null) {
|
||||||
|
Log.d(TAG, "Track is null");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch the mapId, create a new map if necessary
|
||||||
|
publishProgress(PROGRESS_FETCH_MAP_ID);
|
||||||
|
if (!fetchSendMapId(track)) {
|
||||||
|
Log.d("TAG", "Unable to upload all track points");
|
||||||
|
return retryUpload();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upload all the track points plus the start and end markers
|
||||||
|
publishProgress(PROGRESS_UPLOAD_DATA_MIN);
|
||||||
|
if (!uploadAllTrackPoints(track)) {
|
||||||
|
Log.d("TAG", "Unable to upload all track points");
|
||||||
|
return retryUpload();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upload all the waypoints
|
||||||
|
publishProgress(PROGRESS_UPLOAD_WAYPOINTS);
|
||||||
|
if (!uploadWaypoints()) {
|
||||||
|
Log.d("TAG", "Unable to upload waypoints");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
publishProgress(PROGRESS_COMPLETE);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retries upload. Invalidates the authToken. If can retry, invokes
|
||||||
|
* {@link SendMapsAsyncTask#doUpload()}. Returns false if cannot retry.
|
||||||
|
*/
|
||||||
|
private boolean retryUpload() {
|
||||||
|
if (isCancelled()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
AccountManager.get(context).invalidateAuthToken(MapsConstants.SERVICE_NAME, authToken);
|
||||||
|
if (canRetry) {
|
||||||
|
canRetry = false;
|
||||||
|
return doUpload();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the {@link SendMapsAsyncTask#mapId} instance variable for
|
||||||
|
* sending a track to Google Maps.
|
||||||
|
*
|
||||||
|
* @param track the Track
|
||||||
|
* @return true if able to fetch the mapId variable.
|
||||||
|
*/
|
||||||
|
private boolean fetchSendMapId(Track track) {
|
||||||
|
if (isCancelled()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chooseMapId != null) {
|
||||||
|
mapId = chooseMapId;
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
SharedPreferences sharedPreferences = context.getSharedPreferences(
|
||||||
|
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
|
||||||
|
boolean mapPublic = sharedPreferences.getBoolean(
|
||||||
|
context.getString(R.string.default_map_public_key), true);
|
||||||
|
try {
|
||||||
|
String description = track.getCategory() + "\n" + track.getDescription() + "\n"
|
||||||
|
+ StringUtils.getCreatedByMyTracks(context, false);
|
||||||
|
mapId = SendMapsUtils.createNewMap(
|
||||||
|
track.getName(), description, mapPublic, mapsClient, authToken);
|
||||||
|
} catch (ParseException e) {
|
||||||
|
Log.d(TAG, "Unable to create a new map", e);
|
||||||
|
return false;
|
||||||
|
} catch (HttpException e) {
|
||||||
|
Log.d(TAG, "Unable to create a new map", e);
|
||||||
|
return false;
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.d(TAG, "Unable to create a new map", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return mapId != null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uploads all the points in a track.
|
||||||
|
*
|
||||||
|
* @param track the track
|
||||||
|
* @return true if success.
|
||||||
|
*/
|
||||||
|
private boolean uploadAllTrackPoints(Track track) {
|
||||||
|
Cursor locationsCursor = null;
|
||||||
|
try {
|
||||||
|
SharedPreferences prefs = context.getSharedPreferences(
|
||||||
|
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
|
||||||
|
boolean metricUnits = prefs.getBoolean(context.getString(R.string.metric_units_key), true);
|
||||||
|
|
||||||
|
locationsCursor = myTracksProviderUtils.getLocationsCursor(trackId, 0, -1, false);
|
||||||
|
if (locationsCursor == null) {
|
||||||
|
Log.d(TAG, "Location cursor is null");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int locationsCount = locationsCursor.getCount();
|
||||||
|
List<Location> locations = new ArrayList<Location>(MAX_POINTS_PER_UPLOAD);
|
||||||
|
Location lastLocation = null;
|
||||||
|
|
||||||
|
// Limit the number of elevation readings to 250.
|
||||||
|
int elevationSamplingFrequency = Math.max(1, (int) (locationsCount / 250.0));
|
||||||
|
double totalDistance = 0;
|
||||||
|
DoubleBuffer elevationBuffer = new DoubleBuffer(Constants.ELEVATION_SMOOTHING_FACTOR);
|
||||||
|
Vector<Double> distances = new Vector<Double>();
|
||||||
|
Vector<Double> elevations = new Vector<Double>();
|
||||||
|
|
||||||
|
for (int i = 0; i < locationsCount; i++) {
|
||||||
|
locationsCursor.moveToPosition(i);
|
||||||
|
|
||||||
|
Location location = myTracksProviderUtils.createLocation(locationsCursor);
|
||||||
|
locations.add(location);
|
||||||
|
|
||||||
|
int readCount = i + 1;
|
||||||
|
|
||||||
|
if (readCount == 1) {
|
||||||
|
// Create a start marker
|
||||||
|
if (!uploadMarker(context.getString(R.string.marker_label_start, track.getName()), "",
|
||||||
|
START_ICON_URL, location)) {
|
||||||
|
Log.d(TAG, "Unable to create a start marker");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add to the distances and elevations vectors
|
||||||
|
if (LocationUtils.isValidLocation(location)) {
|
||||||
|
// All points go into the smoothing buffer
|
||||||
|
elevationBuffer.setNext(metricUnits ? location.getAltitude()
|
||||||
|
: location.getAltitude() * UnitConversions.M_TO_FT);
|
||||||
|
if (lastLocation != null) {
|
||||||
|
totalDistance += (double) lastLocation.distanceTo(location);
|
||||||
|
}
|
||||||
|
if (readCount % elevationSamplingFrequency == 0) {
|
||||||
|
distances.add(totalDistance);
|
||||||
|
elevations.add(elevationBuffer.getAverage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upload periodically
|
||||||
|
if (readCount % MAX_POINTS_PER_UPLOAD == 0) {
|
||||||
|
if (!prepareAndUploadPoints(track, locations, false)) {
|
||||||
|
Log.d(TAG, "Unable to upload points");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
updateProgress(readCount, locationsCount);
|
||||||
|
locations.clear();
|
||||||
|
}
|
||||||
|
lastLocation = location;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do a final upload with the remaining locations
|
||||||
|
if (!prepareAndUploadPoints(track, locations, true)) {
|
||||||
|
Log.d(TAG, "Unable to upload points");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create an end marker
|
||||||
|
if (lastLocation != null) {
|
||||||
|
StringUtils stringUtils = new StringUtils(context);
|
||||||
|
track.setDescription("<p>" + track.getDescription() + "</p><p>"
|
||||||
|
+ stringUtils.generateTrackDescription(track, distances, elevations) + "</p>");
|
||||||
|
if (!uploadMarker(context.getString(R.string.marker_label_end, track.getName()),
|
||||||
|
track.getDescription(), END_ICON_URL, lastLocation)) {
|
||||||
|
Log.d(TAG, "Unable to create an end marker");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} finally {
|
||||||
|
if (locationsCursor != null) {
|
||||||
|
locationsCursor.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepares and uploads a list of locations from a track.
|
||||||
|
*
|
||||||
|
* @param track the track
|
||||||
|
* @param locations the locations from the track
|
||||||
|
* @param lastBatch true if it is the last batch of locations
|
||||||
|
* @return true if success.
|
||||||
|
*/
|
||||||
|
private boolean prepareAndUploadPoints(Track track, List<Location> locations, boolean lastBatch) {
|
||||||
|
// Prepare locations
|
||||||
|
ArrayList<Track> splitTracks = SendToGoogleUtils.prepareLocations(track, locations);
|
||||||
|
|
||||||
|
// Upload segments
|
||||||
|
boolean onlyOneSegment = lastBatch && currentSegment == 1 && splitTracks.size() == 1;
|
||||||
|
for (Track segment : splitTracks) {
|
||||||
|
if (!onlyOneSegment) {
|
||||||
|
segment.setName(context.getString(
|
||||||
|
R.string.send_google_track_part_label, segment.getName(), currentSegment));
|
||||||
|
}
|
||||||
|
if (!uploadSegment(segment.getName(), segment.getLocations())) {
|
||||||
|
Log.d(TAG, "Unable to upload segment");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
currentSegment++;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uploads a marker.
|
||||||
|
*
|
||||||
|
* @param title marker title
|
||||||
|
* @param description marker description
|
||||||
|
* @param iconUrl marker marker icon
|
||||||
|
* @param location marker location
|
||||||
|
* @return true if success.
|
||||||
|
*/
|
||||||
|
private boolean uploadMarker(
|
||||||
|
String title, String description, String iconUrl, Location location) {
|
||||||
|
if (isCancelled()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (!SendMapsUtils.uploadMarker(mapId, title, description, iconUrl, location, mapsClient,
|
||||||
|
authToken, mapsGDataConverter)) {
|
||||||
|
Log.d(TAG, "Unable to upload marker");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (ParseException e) {
|
||||||
|
Log.d(TAG, "Unable to upload marker", e);
|
||||||
|
return false;
|
||||||
|
} catch (HttpException e) {
|
||||||
|
Log.d(TAG, "Unable to upload marker", e);
|
||||||
|
return false;
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.d(TAG, "Unable to upload marker", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uploads a segment
|
||||||
|
*
|
||||||
|
* @param title segment title
|
||||||
|
* @param locations segment locations
|
||||||
|
* @return true if success
|
||||||
|
*/
|
||||||
|
private boolean uploadSegment(String title, ArrayList<Location> locations) {
|
||||||
|
if (isCancelled()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (!SendMapsUtils.uploadSegment(
|
||||||
|
mapId, title, locations, mapsClient, authToken, mapsGDataConverter)) {
|
||||||
|
Log.d(TAG, "Unable to upload track points");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (ParseException e) {
|
||||||
|
Log.d(TAG, "Unable to upload track points", e);
|
||||||
|
return false;
|
||||||
|
} catch (HttpException e) {
|
||||||
|
Log.d(TAG, "Unable to upload track points", e);
|
||||||
|
return false;
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.d(TAG, "Unable to upload track points", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uploads all the waypoints.
|
||||||
|
*
|
||||||
|
* @return true if success.
|
||||||
|
*/
|
||||||
|
private boolean uploadWaypoints() {
|
||||||
|
Cursor cursor = null;
|
||||||
|
try {
|
||||||
|
cursor = myTracksProviderUtils.getWaypointsCursor(
|
||||||
|
trackId, 0, Constants.MAX_LOADED_WAYPOINTS_POINTS);
|
||||||
|
if (cursor != null && cursor.moveToFirst()) {
|
||||||
|
// This will skip the first waypoint (it carries the stats for the
|
||||||
|
// track).
|
||||||
|
while (cursor.moveToNext()) {
|
||||||
|
if (isCancelled()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Waypoint waypoint = myTracksProviderUtils.createWaypoint(cursor);
|
||||||
|
try {
|
||||||
|
if (!SendMapsUtils.uploadWaypoint(
|
||||||
|
mapId, waypoint, mapsClient, authToken, mapsGDataConverter)) {
|
||||||
|
Log.d(TAG, "Unable to upload waypoint");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (ParseException e) {
|
||||||
|
Log.d(TAG, "Unable to upload waypoint", e);
|
||||||
|
return false;
|
||||||
|
} catch (HttpException e) {
|
||||||
|
Log.d(TAG, "Unable to upload waypoint", e);
|
||||||
|
return false;
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.d(TAG, "Unable to upload waypoint", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} finally {
|
||||||
|
if (cursor != null) {
|
||||||
|
cursor.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the progress based on the number of locations uploaded.
|
||||||
|
*
|
||||||
|
* @param uploaded the number of uploaded locations
|
||||||
|
* @param total the number of total locations
|
||||||
|
*/
|
||||||
|
private void updateProgress(int uploaded, int total) {
|
||||||
|
double totalPercentage = uploaded / total;
|
||||||
|
double scaledPercentage = totalPercentage
|
||||||
|
* (PROGRESS_UPLOAD_DATA_MAX - PROGRESS_UPLOAD_DATA_MIN) + PROGRESS_UPLOAD_DATA_MIN;
|
||||||
|
publishProgress((int) scaledPercentage);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,227 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012 Google Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||||
|
* use this file except in compliance with the License. You may obtain a copy of
|
||||||
|
* the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations under
|
||||||
|
* the License.
|
||||||
|
*/
|
||||||
|
package com.google.android.apps.mytracks.io.maps;
|
||||||
|
|
||||||
|
import com.google.android.apps.mytracks.content.Track;
|
||||||
|
import com.google.android.apps.mytracks.content.Waypoint;
|
||||||
|
import com.google.android.apps.mytracks.io.gdata.maps.MapsClient;
|
||||||
|
import com.google.android.apps.mytracks.io.gdata.maps.MapsFeature;
|
||||||
|
import com.google.android.apps.mytracks.io.gdata.maps.MapsGDataConverter;
|
||||||
|
import com.google.android.apps.mytracks.io.gdata.maps.MapsMapMetadata;
|
||||||
|
import com.google.android.maps.GeoPoint;
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
import com.google.wireless.gdata.client.HttpException;
|
||||||
|
import com.google.wireless.gdata.data.Entry;
|
||||||
|
import com.google.wireless.gdata.parser.ParseException;
|
||||||
|
|
||||||
|
import android.location.Location;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utilities for sending a track to Google Maps.
|
||||||
|
*
|
||||||
|
* @author jshih@google.com (Jimmy Shih)
|
||||||
|
*/
|
||||||
|
public class SendMapsUtils {
|
||||||
|
|
||||||
|
private static final String EMPTY_TITLE = "-";
|
||||||
|
private static final int LINE_COLOR = 0x80FF0000;
|
||||||
|
private static final String TAG = SendMapsUtils.class.getSimpleName();
|
||||||
|
|
||||||
|
private SendMapsUtils() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the Google Maps url for a track.
|
||||||
|
*
|
||||||
|
* @param track the track
|
||||||
|
* @return the url if available.
|
||||||
|
*/
|
||||||
|
public static String getMapUrl(Track track) {
|
||||||
|
if (track == null || track.getMapId() == null) {
|
||||||
|
Log.e(TAG, "Invalid track");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return MapsClient.buildMapUrl(track.getMapId());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new Google Map.
|
||||||
|
*
|
||||||
|
* @param title title of the map
|
||||||
|
* @param description description of the map
|
||||||
|
* @param isPublic true if the map can be public
|
||||||
|
* @param mapsClient the maps client
|
||||||
|
* @param authToken the auth token
|
||||||
|
* @return map id of the created map if successful.
|
||||||
|
*/
|
||||||
|
public static String createNewMap(
|
||||||
|
String title, String description, boolean isPublic, MapsClient mapsClient, String authToken)
|
||||||
|
throws ParseException, HttpException, IOException {
|
||||||
|
String mapFeed = MapsClient.getMapsFeed();
|
||||||
|
MapsMapMetadata metaData = new MapsMapMetadata();
|
||||||
|
metaData.setTitle(title);
|
||||||
|
metaData.setDescription(description);
|
||||||
|
metaData.setSearchable(isPublic);
|
||||||
|
Entry entry = MapsGDataConverter.getMapEntryForMetadata(metaData);
|
||||||
|
Entry result = mapsClient.createEntry(mapFeed, authToken, entry);
|
||||||
|
if (result == null) {
|
||||||
|
Log.d(TAG, "No result when creating a new map");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return MapsClient.getMapIdFromMapEntryId(result.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uploads a start/end marker to Google Maps.
|
||||||
|
*
|
||||||
|
* @param mapId the map id
|
||||||
|
* @param title the marker title
|
||||||
|
* @param description the marker description
|
||||||
|
* @param iconUrl the marker icon URL
|
||||||
|
* @param location the marker location
|
||||||
|
* @param mapsClient the maps client
|
||||||
|
* @param authToken the auth token
|
||||||
|
* @param mapsGDataConverter the maps gdata converter
|
||||||
|
* @return true if success.
|
||||||
|
*/
|
||||||
|
public static boolean uploadMarker(String mapId, String title, String description, String iconUrl,
|
||||||
|
Location location, MapsClient mapsClient, String authToken,
|
||||||
|
MapsGDataConverter mapsGDataConverter) throws ParseException, HttpException, IOException {
|
||||||
|
String featuresFeed = MapsClient.getFeaturesFeed(mapId);
|
||||||
|
MapsFeature mapsFeature = buildMapsMarkerFeature(
|
||||||
|
title, description, iconUrl, getGeoPoint(location));
|
||||||
|
Entry entry = mapsGDataConverter.getEntryForFeature(mapsFeature);
|
||||||
|
try {
|
||||||
|
mapsClient.createEntry(featuresFeed, authToken, entry);
|
||||||
|
} catch (IOException e) {
|
||||||
|
// Retry once (often IOException is thrown on a timeout)
|
||||||
|
Log.d(TAG, "Retry upload marker", e);
|
||||||
|
mapsClient.createEntry(featuresFeed, authToken, entry);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uploads a waypoint as a marker feature to Google Maps.
|
||||||
|
*
|
||||||
|
* @param mapId the map id
|
||||||
|
* @param waypoint the waypoint
|
||||||
|
* @param mapsClient the maps client
|
||||||
|
* @param authToken the auth token
|
||||||
|
* @param mapsGDataConverter the maps gdata converter
|
||||||
|
* @return true if success.
|
||||||
|
*/
|
||||||
|
public static boolean uploadWaypoint(String mapId, Waypoint waypoint, MapsClient mapsClient,
|
||||||
|
String authToken, MapsGDataConverter mapsGDataConverter)
|
||||||
|
throws ParseException, HttpException, IOException {
|
||||||
|
String featuresFeed = MapsClient.getFeaturesFeed(mapId);
|
||||||
|
MapsFeature feature = buildMapsMarkerFeature(waypoint.getName(), waypoint.getDescription(),
|
||||||
|
waypoint.getIcon(), getGeoPoint(waypoint.getLocation()));
|
||||||
|
Entry entry = mapsGDataConverter.getEntryForFeature(feature);
|
||||||
|
try {
|
||||||
|
mapsClient.createEntry(featuresFeed, authToken, entry);
|
||||||
|
} catch (IOException e) {
|
||||||
|
// Retry once (often IOException is thrown on a timeout)
|
||||||
|
Log.d(TAG, "Retry upload waypoint", e);
|
||||||
|
mapsClient.createEntry(featuresFeed, authToken, entry);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uploads a segment as a line feature to Google Maps.
|
||||||
|
*
|
||||||
|
* @param mapId the map id
|
||||||
|
* @param title the segment title
|
||||||
|
* @param locations the segment locations
|
||||||
|
* @param mapsClient the maps client
|
||||||
|
* @param authToken the auth token
|
||||||
|
* @param mapsGDataConverter the maps gdata converter
|
||||||
|
* @return true if success.
|
||||||
|
*/
|
||||||
|
public static boolean uploadSegment(String mapId, String title, ArrayList<Location> locations,
|
||||||
|
MapsClient mapsClient, String authToken, MapsGDataConverter mapsGDataConverter)
|
||||||
|
throws ParseException, HttpException, IOException {
|
||||||
|
String featuresFeed = MapsClient.getFeaturesFeed(mapId);
|
||||||
|
Entry entry = mapsGDataConverter.getEntryForFeature(buildMapsLineFeature(title, locations));
|
||||||
|
try {
|
||||||
|
mapsClient.createEntry(featuresFeed, authToken, entry);
|
||||||
|
} catch (IOException e) {
|
||||||
|
// Retry once (often IOException is thrown on a timeout)
|
||||||
|
Log.d(TAG, "Retry upload track points", e);
|
||||||
|
mapsClient.createEntry(featuresFeed, authToken, entry);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a map marker feature.
|
||||||
|
*
|
||||||
|
* @param title feature title
|
||||||
|
* @param description the feature description
|
||||||
|
* @param iconUrl the feature icon URL
|
||||||
|
* @param geoPoint the marker
|
||||||
|
*/
|
||||||
|
@VisibleForTesting
|
||||||
|
static MapsFeature buildMapsMarkerFeature(
|
||||||
|
String title, String description, String iconUrl, GeoPoint geoPoint) {
|
||||||
|
MapsFeature mapsFeature = new MapsFeature();
|
||||||
|
mapsFeature.setType(MapsFeature.MARKER);
|
||||||
|
mapsFeature.generateAndroidId();
|
||||||
|
// Feature must have a name (otherwise GData upload may fail)
|
||||||
|
mapsFeature.setTitle(TextUtils.isEmpty(title) ? EMPTY_TITLE : title);
|
||||||
|
mapsFeature.setDescription(description.replaceAll("\n", "<br>"));
|
||||||
|
mapsFeature.setIconUrl(iconUrl);
|
||||||
|
mapsFeature.addPoint(geoPoint);
|
||||||
|
return mapsFeature;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a maps line feature from a set of locations.
|
||||||
|
*
|
||||||
|
* @param title the feature title
|
||||||
|
* @param locations set of locations
|
||||||
|
*/
|
||||||
|
@VisibleForTesting
|
||||||
|
static MapsFeature buildMapsLineFeature(String title, ArrayList<Location> locations) {
|
||||||
|
MapsFeature mapsFeature = new MapsFeature();
|
||||||
|
mapsFeature.setType(MapsFeature.LINE);
|
||||||
|
mapsFeature.generateAndroidId();
|
||||||
|
// Feature must have a name (otherwise GData upload may fail)
|
||||||
|
mapsFeature.setTitle(TextUtils.isEmpty(title) ? EMPTY_TITLE : title);
|
||||||
|
mapsFeature.setColor(LINE_COLOR);
|
||||||
|
for (Location location : locations) {
|
||||||
|
mapsFeature.addPoint(getGeoPoint(location));
|
||||||
|
}
|
||||||
|
return mapsFeature;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a {@link GeoPoint} from a {@link Location}.
|
||||||
|
*
|
||||||
|
* @param location the location
|
||||||
|
*/
|
||||||
|
@VisibleForTesting
|
||||||
|
static GeoPoint getGeoPoint(Location location) {
|
||||||
|
return new GeoPoint(
|
||||||
|
(int) (location.getLatitude() * 1E6), (int) (location.getLongitude() * 1E6));
|
||||||
|
}
|
||||||
|
}
|
||||||
+52
-38
@@ -16,7 +16,11 @@
|
|||||||
package com.google.android.apps.mytracks.io.sendtogoogle;
|
package com.google.android.apps.mytracks.io.sendtogoogle;
|
||||||
|
|
||||||
import com.google.android.apps.mytracks.Constants;
|
import com.google.android.apps.mytracks.Constants;
|
||||||
import com.google.android.apps.mytracks.util.ApiFeatures;
|
import com.google.android.apps.mytracks.io.docs.SendDocsActivity;
|
||||||
|
import com.google.android.apps.mytracks.io.fusiontables.SendFusionTablesActivity;
|
||||||
|
import com.google.android.apps.mytracks.io.maps.ChooseMapActivity;
|
||||||
|
import com.google.android.apps.mytracks.io.maps.SendMapsActivity;
|
||||||
|
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
|
||||||
import com.google.android.maps.mytracks.R;
|
import com.google.android.maps.mytracks.R;
|
||||||
|
|
||||||
import android.accounts.Account;
|
import android.accounts.Account;
|
||||||
@@ -32,46 +36,34 @@ import android.content.SharedPreferences.Editor;
|
|||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A chooser to select an account. To be called with
|
* A chooser to select an account.
|
||||||
* {@link Activity#startActivityForResult(Intent, int)}. Returns
|
|
||||||
* {@link Activity#RESULT_CANCELED} if the user cancels the activity. Otherwise,
|
|
||||||
* returns {@link Activity#RESULT_OK} with an intent containing the selected
|
|
||||||
* account. The selected account is stored as an {@link Account} in the
|
|
||||||
* {@link AccountChooserActivity#ACCOUNT} item of the intent. The selected
|
|
||||||
* account can be null.
|
|
||||||
*
|
*
|
||||||
* @author jshih@google.com (Jimmy Shih)
|
* @author jshih@google.com (Jimmy Shih)
|
||||||
*/
|
*/
|
||||||
public class AccountChooserActivity extends Activity {
|
public class AccountChooserActivity extends Activity {
|
||||||
|
|
||||||
/**
|
|
||||||
* Key for storing an {@link Account} in an {@link Intent}.
|
|
||||||
*/
|
|
||||||
public static final String ACCOUNT = "account";
|
|
||||||
|
|
||||||
private static final int NO_ACCOUNT_DIALOG = 1;
|
private static final int NO_ACCOUNT_DIALOG = 1;
|
||||||
private static final int CHOOSE_ACCOUNT_DIALOG = 2;
|
private static final int CHOOSE_ACCOUNT_DIALOG = 2;
|
||||||
|
|
||||||
|
private SendRequest sendRequest;
|
||||||
private Account[] accounts;
|
private Account[] accounts;
|
||||||
private int selectedAccountIndex;
|
private int selectedAccountIndex;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
sendRequest = getIntent().getParcelableExtra(SendRequest.SEND_REQUEST_KEY);
|
||||||
accounts = AccountManager.get(this).getAccountsByType(Constants.ACCOUNT_TYPE);
|
accounts = AccountManager.get(this).getAccountsByType(Constants.ACCOUNT_TYPE);
|
||||||
|
|
||||||
if (accounts.length == 1) {
|
if (accounts.length == 1) {
|
||||||
Intent intent = new Intent();
|
startNextActivity(accounts[0]);
|
||||||
intent.putExtra(ACCOUNT, accounts[0]);
|
|
||||||
setResult(RESULT_OK, intent);
|
|
||||||
finish();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
SharedPreferences prefs = getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
|
SharedPreferences prefs = getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
|
||||||
String preferredAccount = prefs.getString(getString(R.string.preferred_account_key), "");
|
String preferredAccount = prefs.getString(getString(R.string.preferred_account_key), "");
|
||||||
|
|
||||||
selectedAccountIndex = -1;
|
selectedAccountIndex = 0;
|
||||||
for (int i = 0; i < accounts.length; i++) {
|
for (int i = 0; i < accounts.length; i++) {
|
||||||
if (accounts[i].name.equals(preferredAccount)) {
|
if (accounts[i].name.equals(preferredAccount)) {
|
||||||
selectedAccountIndex = i;
|
selectedAccountIndex = i;
|
||||||
@@ -102,15 +94,11 @@ public class AccountChooserActivity extends Activity {
|
|||||||
builder.setOnCancelListener(new DialogInterface.OnCancelListener() {
|
builder.setOnCancelListener(new DialogInterface.OnCancelListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onCancel(DialogInterface dialog) {
|
public void onCancel(DialogInterface dialog) {
|
||||||
setResult(RESULT_CANCELED);
|
|
||||||
finish();
|
finish();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
builder.setPositiveButton(R.string.generic_ok, new DialogInterface.OnClickListener() {
|
builder.setPositiveButton(R.string.generic_ok, new DialogInterface.OnClickListener() {
|
||||||
public void onClick(DialogInterface dialog, int which) {
|
public void onClick(DialogInterface dialog, int which) {
|
||||||
Intent intent = new Intent();
|
|
||||||
intent.putExtra(ACCOUNT, (Account) null);
|
|
||||||
setResult(RESULT_OK, intent);
|
|
||||||
finish();
|
finish();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -134,31 +122,23 @@ public class AccountChooserActivity extends Activity {
|
|||||||
builder.setOnCancelListener(new DialogInterface.OnCancelListener() {
|
builder.setOnCancelListener(new DialogInterface.OnCancelListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onCancel(DialogInterface dialog) {
|
public void onCancel(DialogInterface dialog) {
|
||||||
setResult(RESULT_CANCELED);
|
|
||||||
finish();
|
finish();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
builder.setNegativeButton(R.string.generic_cancel, new DialogInterface.OnClickListener() {
|
builder.setNegativeButton(R.string.generic_cancel, new DialogInterface.OnClickListener() {
|
||||||
public void onClick(DialogInterface dialog, int which) {
|
public void onClick(DialogInterface dialog, int which) {
|
||||||
setResult(RESULT_CANCELED);
|
|
||||||
finish();
|
finish();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
builder.setPositiveButton(R.string.generic_ok, new DialogInterface.OnClickListener() {
|
builder.setPositiveButton(R.string.generic_ok, new DialogInterface.OnClickListener() {
|
||||||
public void onClick(DialogInterface dialog, int which) {
|
public void onClick(DialogInterface dialog, int which) {
|
||||||
Account account = null;
|
Account account = accounts[selectedAccountIndex];
|
||||||
if (selectedAccountIndex != -1) {
|
SharedPreferences prefs = getSharedPreferences(
|
||||||
account = accounts[selectedAccountIndex];
|
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
|
||||||
SharedPreferences prefs = getSharedPreferences(
|
Editor editor = prefs.edit();
|
||||||
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
|
editor.putString(getString(R.string.preferred_account_key), account.name);
|
||||||
Editor editor = prefs.edit();
|
ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor);
|
||||||
editor.putString(getString(R.string.preferred_account_key), account.name);
|
startNextActivity(account);
|
||||||
ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges(editor);
|
|
||||||
}
|
|
||||||
Intent intent = new Intent();
|
|
||||||
intent.putExtra(ACCOUNT, account);
|
|
||||||
setResult(RESULT_OK, intent);
|
|
||||||
finish();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return builder.create();
|
return builder.create();
|
||||||
@@ -166,4 +146,38 @@ public class AccountChooserActivity extends Activity {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
/**
|
||||||
|
* Starts the next activity. If
|
||||||
|
* <p>
|
||||||
|
* sendMaps and newMap -> {@link SendMapsActivity}
|
||||||
|
* <p>
|
||||||
|
* sendMaps and !newMap -> {@link ChooseMapActivity}
|
||||||
|
* <p>
|
||||||
|
* !sendMaps && sendFusionTables -> {@link SendFusionTablesActivity}
|
||||||
|
* <p>
|
||||||
|
* !sendMaps && !sendFusionTables && sendDocs -> {@link SendDocsActivity}
|
||||||
|
* <p>
|
||||||
|
* !sendMaps && !sendFusionTables && !sendDocs -> {@link UploadResultActivity}
|
||||||
|
*
|
||||||
|
* @param account the chosen account
|
||||||
|
*/
|
||||||
|
private void startNextActivity(Account account) {
|
||||||
|
sendRequest.setAccount(account);
|
||||||
|
|
||||||
|
Class<?> next;
|
||||||
|
if (sendRequest.isSendMaps()) {
|
||||||
|
next = sendRequest.isNewMap() ? SendMapsActivity.class : ChooseMapActivity.class;
|
||||||
|
} else if (sendRequest.isSendFusionTables()) {
|
||||||
|
next = SendFusionTablesActivity.class;
|
||||||
|
} else if (sendRequest.isSendDocs()) {
|
||||||
|
next = SendDocsActivity.class;
|
||||||
|
} else {
|
||||||
|
next = UploadResultActivity.class;
|
||||||
|
}
|
||||||
|
Intent intent = new Intent(this, next)
|
||||||
|
.putExtra(SendRequest.SEND_REQUEST_KEY, sendRequest);
|
||||||
|
startActivity(intent);
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
-109
@@ -1,109 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2011 Google Inc.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
|
||||||
* use this file except in compliance with the License. You may obtain a copy of
|
|
||||||
* the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
* License for the specific language governing permissions and limitations under
|
|
||||||
* the License.
|
|
||||||
*/
|
|
||||||
package com.google.android.apps.mytracks.io.sendtogoogle;
|
|
||||||
|
|
||||||
import com.google.android.maps.mytracks.R;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.app.AlertDialog;
|
|
||||||
import android.app.Dialog;
|
|
||||||
import android.content.DialogInterface;
|
|
||||||
import android.view.View;
|
|
||||||
import android.widget.ListView;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates the dialog used to display the results of sending track data to
|
|
||||||
* Google. The dialog lists the services to which data was uploaded, along
|
|
||||||
* with an indicator of success/failure. If possible, a button is displayed
|
|
||||||
* offering to share the uploaded track.
|
|
||||||
*
|
|
||||||
* Implementation note: This class is a factory, rather than a {@link Dialog}
|
|
||||||
* subclass, because alert dialogs have to be created with
|
|
||||||
* {@link AlertDialog.Builder}. Attempts to subclass {@link AlertDialog}
|
|
||||||
* directly have been unsuccessful, as {@link AlertDialog.Builder} uses a
|
|
||||||
* private subclass to implement most of the interesting behavior.
|
|
||||||
*
|
|
||||||
* @author Matthew Simmons
|
|
||||||
*/
|
|
||||||
public class ResultDialogFactory {
|
|
||||||
private ResultDialogFactory() {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a send-to-Google result dialog. The caller is responsible for
|
|
||||||
* showing it.
|
|
||||||
*
|
|
||||||
* @param activity the activity associated with the dialog
|
|
||||||
* @param results the results to be displayed in the dialog
|
|
||||||
* @param onOkClickListener the listener to invoke if the OK button is
|
|
||||||
* clicked
|
|
||||||
* @param onShareClickListener the listener to invoke if the Share button is
|
|
||||||
* clicked. If no share listener is provided, the Share button will not
|
|
||||||
* be displayed.
|
|
||||||
* @return the created dialog
|
|
||||||
*/
|
|
||||||
public static AlertDialog makeDialog(Activity activity, List<SendResult> results,
|
|
||||||
DialogInterface.OnClickListener onOkClickListener,
|
|
||||||
DialogInterface.OnClickListener onShareClickListener,
|
|
||||||
DialogInterface.OnCancelListener onCancelListener) {
|
|
||||||
boolean success = true;
|
|
||||||
for (SendResult result : results) {
|
|
||||||
if (!result.isSuccess()) {
|
|
||||||
success = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AlertDialog.Builder builder = new AlertDialog.Builder(activity)
|
|
||||||
.setView(makeDialogContent(activity, results, success));
|
|
||||||
|
|
||||||
if (success) {
|
|
||||||
builder.setTitle(R.string.generic_success_title);
|
|
||||||
builder.setIcon(android.R.drawable.ic_dialog_info);
|
|
||||||
} else {
|
|
||||||
builder.setTitle(R.string.generic_error_title);
|
|
||||||
builder.setIcon(android.R.drawable.ic_dialog_alert);
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.setPositiveButton(activity.getString(R.string.generic_ok), onOkClickListener);
|
|
||||||
if (onShareClickListener != null) {
|
|
||||||
builder.setNegativeButton(activity.getString(R.string.send_google_result_share_url),
|
|
||||||
onShareClickListener);
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.setOnCancelListener(onCancelListener);
|
|
||||||
|
|
||||||
return builder.create();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static View makeDialogContent(Activity activity, List<SendResult> results,
|
|
||||||
boolean success) {
|
|
||||||
ResultListAdapter resultListAdapter = new ResultListAdapter(activity,
|
|
||||||
R.layout.send_to_google_result_list_item, results);
|
|
||||||
|
|
||||||
View content = activity.getLayoutInflater().inflate(R.layout.send_to_google_result, null);
|
|
||||||
ListView resultList = (ListView) content.findViewById(R.id.send_to_google_result_list);
|
|
||||||
resultList.setAdapter(resultListAdapter);
|
|
||||||
|
|
||||||
content.findViewById(R.id.send_to_google_result_comment)
|
|
||||||
.setVisibility(success ? View.VISIBLE : View.GONE);
|
|
||||||
content.findViewById(R.id.send_to_google_result_error)
|
|
||||||
.setVisibility(success ? View.GONE : View.VISIBLE);
|
|
||||||
|
|
||||||
return content;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2011 Google Inc.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
|
||||||
* use this file except in compliance with the License. You may obtain a copy of
|
|
||||||
* the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
* License for the specific language governing permissions and limitations under
|
|
||||||
* the License.
|
|
||||||
*/
|
|
||||||
package com.google.android.apps.mytracks.io.sendtogoogle;
|
|
||||||
|
|
||||||
import com.google.android.maps.mytracks.R;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.ArrayAdapter;
|
|
||||||
import android.widget.ImageView;
|
|
||||||
import android.widget.ListAdapter;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This class implements the {@link ListAdapter} used by the send-to-Google
|
|
||||||
* result dialog created by {@link ResultDialogFactory}. It generates views
|
|
||||||
* for each entry in an array of {@link SendResult} instances.
|
|
||||||
* @author Matthew Simmons
|
|
||||||
*/
|
|
||||||
class ResultListAdapter extends ArrayAdapter<SendResult> {
|
|
||||||
public ResultListAdapter(Context context, int textViewResourceId, List<SendResult> results) {
|
|
||||||
super(context, textViewResourceId, results);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public View getView(int position, View convertView, ViewGroup parent) {
|
|
||||||
View view = convertView;
|
|
||||||
if (view == null) {
|
|
||||||
LayoutInflater inflater
|
|
||||||
= (LayoutInflater) getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
|
||||||
view = inflater.inflate(R.layout.send_to_google_result_list_item, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
SendResult result = getItem(position);
|
|
||||||
setImage(view, result.isSuccess() ? R.drawable.success : R.drawable.failure);
|
|
||||||
setName(view, result.getType().getServiceName());
|
|
||||||
setUrl(view, result.getType().getServiceUrl());
|
|
||||||
return view;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean areAllItemsEnabled() {
|
|
||||||
// We don't want the displayed items to be clickable
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The following protected methods exist to be overridden for testing
|
|
||||||
// purposes. Doing so insulates the test class from the details of the
|
|
||||||
// layout.
|
|
||||||
|
|
||||||
protected void setImage(View content, int drawableId) {
|
|
||||||
ImageView imageView = (ImageView) content.findViewById(R.id.send_to_google_result_icon);
|
|
||||||
imageView.setImageDrawable(getContext().getResources().getDrawable(drawableId));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void setName(View content, int nameId) {
|
|
||||||
setTextViewText(content, R.id.send_to_google_result_name, nameId);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void setUrl(View content, int urlId) {
|
|
||||||
setTextViewText(content, R.id.send_to_google_result_url, urlId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setTextViewText(View content, int viewId, int textId) {
|
|
||||||
TextView textView = (TextView) content.findViewById(viewId);
|
|
||||||
textView.setText(getContext().getString(textId));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,814 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2011 Google Inc.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
|
||||||
* use this file except in compliance with the License. You may obtain a copy of
|
|
||||||
* the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
* License for the specific language governing permissions and limitations under
|
|
||||||
* the License.
|
|
||||||
*/
|
|
||||||
package com.google.android.apps.mytracks.io.sendtogoogle;
|
|
||||||
|
|
||||||
import static com.google.android.apps.mytracks.Constants.TAG;
|
|
||||||
|
|
||||||
import com.google.android.apps.analytics.GoogleAnalyticsTracker;
|
|
||||||
import com.google.android.apps.mytracks.Constants;
|
|
||||||
import com.google.android.apps.mytracks.MapsList;
|
|
||||||
import com.google.android.apps.mytracks.ProgressIndicator;
|
|
||||||
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
|
|
||||||
import com.google.android.apps.mytracks.content.Track;
|
|
||||||
import com.google.android.apps.mytracks.io.AuthManager;
|
|
||||||
import com.google.android.apps.mytracks.io.AuthManager.AuthCallback;
|
|
||||||
import com.google.android.apps.mytracks.io.AuthManagerFactory;
|
|
||||||
import com.google.android.apps.mytracks.io.SendToDocs;
|
|
||||||
import com.google.android.apps.mytracks.io.SendToFusionTables;
|
|
||||||
import com.google.android.apps.mytracks.io.SendToFusionTables.OnSendCompletedListener;
|
|
||||||
import com.google.android.apps.mytracks.io.SendToMaps;
|
|
||||||
import com.google.android.apps.mytracks.io.maps.MapsConstants;
|
|
||||||
import com.google.android.apps.mytracks.io.maps.MapsFacade;
|
|
||||||
import com.google.android.apps.mytracks.util.SystemUtils;
|
|
||||||
import com.google.android.maps.mytracks.R;
|
|
||||||
|
|
||||||
import android.accounts.Account;
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.app.Dialog;
|
|
||||||
import android.app.ProgressDialog;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.DialogInterface;
|
|
||||||
import android.content.DialogInterface.OnCancelListener;
|
|
||||||
import android.content.DialogInterface.OnClickListener;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper activity for managing the sending of tracks to Google services.
|
|
||||||
*
|
|
||||||
* @author Rodrigo Damazio
|
|
||||||
*/
|
|
||||||
public class SendActivity extends Activity implements ProgressIndicator {
|
|
||||||
|
|
||||||
// Items in the intent that starts the activity.
|
|
||||||
public static final String TRACK_ID = "trackId";
|
|
||||||
public static final String SHARE_URL = "shareUrl";
|
|
||||||
public static final String SEND_MAPS = "sendMaps";
|
|
||||||
public static final String SEND_FUSION_TABLES = "sendFusionTables";
|
|
||||||
public static final String SEND_DOCS = "sendDocs";
|
|
||||||
public static final String CREATE_MAP = "createMap";
|
|
||||||
|
|
||||||
// Keys for saved state variables.
|
|
||||||
private static final String STATE_ACCOUNT = "account";
|
|
||||||
private static final String STATE_STATE = "state";
|
|
||||||
private static final String STATE_DOCS_SUCCESS = "docsSuccess";
|
|
||||||
private static final String STATE_FUSION_SUCCESS = "fusionSuccess";
|
|
||||||
private static final String STATE_MAPS_SUCCESS = "mapsSuccess";
|
|
||||||
private static final String STATE_TABLE_ID = "tableId";
|
|
||||||
private static final String STATE_MAP_ID = "mapId";
|
|
||||||
|
|
||||||
/** States for the state machine that defines the upload process. */
|
|
||||||
private enum SendState {
|
|
||||||
CHOOSE_ACCOUNT,
|
|
||||||
START,
|
|
||||||
AUTHENTICATE_MAPS,
|
|
||||||
PICK_MAP,
|
|
||||||
SEND_TO_MAPS,
|
|
||||||
SEND_TO_MAPS_DONE,
|
|
||||||
AUTHENTICATE_FUSION_TABLES,
|
|
||||||
SEND_TO_FUSION_TABLES,
|
|
||||||
SEND_TO_FUSION_TABLES_DONE,
|
|
||||||
AUTHENTICATE_DOCS,
|
|
||||||
AUTHENTICATE_TRIX,
|
|
||||||
SEND_TO_DOCS,
|
|
||||||
SEND_TO_DOCS_DONE,
|
|
||||||
SHOW_RESULTS,
|
|
||||||
SHARE_LINK,
|
|
||||||
FINISH,
|
|
||||||
DONE,
|
|
||||||
NOT_READY
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final int PROGRESS_DIALOG = 1;
|
|
||||||
/* @VisibleForTesting */
|
|
||||||
static final int DONE_DIALOG = 2;
|
|
||||||
|
|
||||||
// UI
|
|
||||||
private ProgressDialog progressDialog;
|
|
||||||
|
|
||||||
// Set in Activity.onCreate
|
|
||||||
private MyTracksProviderUtils providerUtils;
|
|
||||||
private SharedPreferences sharedPreferences;
|
|
||||||
private GoogleAnalyticsTracker tracker;
|
|
||||||
|
|
||||||
// Send request information. Set by the intent that starts the activity.
|
|
||||||
private long sendTrackId;
|
|
||||||
private boolean shareRequest;
|
|
||||||
private boolean sendToMaps;
|
|
||||||
private boolean sendToMapsNewMap;
|
|
||||||
private boolean sendToFusionTables;
|
|
||||||
private boolean sendToDocs;
|
|
||||||
|
|
||||||
// Authentication information.
|
|
||||||
private Account account;
|
|
||||||
private HashMap<String, AuthManager> authMap = new HashMap<String, AuthManager>();
|
|
||||||
|
|
||||||
// Current sending state.
|
|
||||||
private SendState currentState;
|
|
||||||
|
|
||||||
// Send result information. Used by the results dialog.
|
|
||||||
private boolean sendToMapsSuccess;
|
|
||||||
private boolean sendToFusionTablesSuccess;
|
|
||||||
private boolean sendToDocsSuccess;
|
|
||||||
private String sendToMapsMapId;
|
|
||||||
private String sendToFusionTablesTableId;
|
|
||||||
|
|
||||||
private final OnCancelListener finishOnCancelListener = new OnCancelListener() {
|
|
||||||
@Override
|
|
||||||
public void onCancel(DialogInterface arg0) {
|
|
||||||
onAllDone();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
|
||||||
Log.d(TAG, "SendActivity.onCreate");
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
|
|
||||||
providerUtils = MyTracksProviderUtils.Factory.get(this);
|
|
||||||
sharedPreferences = getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
|
|
||||||
|
|
||||||
tracker = GoogleAnalyticsTracker.getInstance();
|
|
||||||
// Start the tracker in manual dispatch mode...
|
|
||||||
tracker.start(getString(R.string.my_tracks_analytics_id),
|
|
||||||
getApplicationContext());
|
|
||||||
tracker.setProductVersion("android-mytracks",
|
|
||||||
SystemUtils.getMyTracksVersion(this));
|
|
||||||
|
|
||||||
resetState();
|
|
||||||
|
|
||||||
if (savedInstanceState != null) {
|
|
||||||
restoreInstanceState(savedInstanceState);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we had the instance restored after it was done, reset it.
|
|
||||||
if (currentState == SendState.DONE) {
|
|
||||||
resetState();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only consider the intent if we're not restoring from a previous state.
|
|
||||||
if (currentState == SendState.CHOOSE_ACCOUNT) {
|
|
||||||
if (!handleIntent()) {
|
|
||||||
finish();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Execute the state machine, at the start or restored state.
|
|
||||||
Log.w(TAG, "Starting at state " + currentState);
|
|
||||||
executeStateMachine(currentState);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean handleIntent() {
|
|
||||||
Intent intent = getIntent();
|
|
||||||
sendTrackId = intent.getLongExtra(TRACK_ID, -1L);
|
|
||||||
if (sendTrackId == -1L) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
sendToMaps = intent.getBooleanExtra(SEND_MAPS, false);
|
|
||||||
sendToFusionTables = intent.getBooleanExtra(SEND_FUSION_TABLES, false);
|
|
||||||
sendToDocs = intent.getBooleanExtra(SEND_DOCS, false);
|
|
||||||
if (!sendToMaps && !sendToFusionTables && !sendToDocs) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
sendToMapsNewMap = intent.getBooleanExtra(CREATE_MAP, true);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Dialog onCreateDialog(int id) {
|
|
||||||
switch (id) {
|
|
||||||
case PROGRESS_DIALOG:
|
|
||||||
return createProgressDialog();
|
|
||||||
case DONE_DIALOG:
|
|
||||||
return createDoneDialog();
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void restoreInstanceState(Bundle savedInstanceState) {
|
|
||||||
account = savedInstanceState.getParcelable(STATE_ACCOUNT);
|
|
||||||
|
|
||||||
currentState = SendState.values()[savedInstanceState.getInt(STATE_STATE)];
|
|
||||||
|
|
||||||
sendToMapsSuccess = savedInstanceState.getBoolean(STATE_MAPS_SUCCESS);
|
|
||||||
sendToFusionTablesSuccess = savedInstanceState.getBoolean(STATE_FUSION_SUCCESS);
|
|
||||||
sendToDocsSuccess = savedInstanceState.getBoolean(STATE_DOCS_SUCCESS);
|
|
||||||
sendToMapsMapId = savedInstanceState.getString(STATE_MAP_ID);
|
|
||||||
sendToFusionTablesTableId = savedInstanceState.getString(STATE_TABLE_ID);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onSaveInstanceState(Bundle outState) {
|
|
||||||
super.onSaveInstanceState(outState);
|
|
||||||
outState.putParcelable(STATE_ACCOUNT, account);
|
|
||||||
|
|
||||||
outState.putInt(STATE_STATE, currentState.ordinal());
|
|
||||||
|
|
||||||
outState.putBoolean(STATE_MAPS_SUCCESS, sendToMapsSuccess);
|
|
||||||
outState.putBoolean(STATE_FUSION_SUCCESS, sendToFusionTablesSuccess);
|
|
||||||
outState.putBoolean(STATE_DOCS_SUCCESS, sendToDocsSuccess);
|
|
||||||
outState.putString(STATE_MAP_ID, sendToMapsMapId);
|
|
||||||
outState.putString(STATE_TABLE_ID, sendToFusionTablesTableId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onStop() {
|
|
||||||
Log.d(TAG, "SendActivity.onStop, state=" + currentState);
|
|
||||||
tracker.dispatch();
|
|
||||||
tracker.stop();
|
|
||||||
|
|
||||||
super.onStop();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onDestroy() {
|
|
||||||
Log.d(TAG, "SendActivity.onDestroy, state=" + currentState);
|
|
||||||
super.onDestroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void executeStateMachine(SendState startState) {
|
|
||||||
currentState = startState;
|
|
||||||
|
|
||||||
// If a state handler returns NOT_READY, it means it's waiting for some
|
|
||||||
// event, and will call this method again when it happens.
|
|
||||||
while (currentState != SendState.DONE &&
|
|
||||||
currentState != SendState.NOT_READY) {
|
|
||||||
Log.d(TAG, "Executing state " + currentState);
|
|
||||||
currentState = executeState(currentState);
|
|
||||||
Log.d(TAG, "New state is " + currentState);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private SendState executeState(SendState state) {
|
|
||||||
switch (state) {
|
|
||||||
case CHOOSE_ACCOUNT:
|
|
||||||
return chooseAccount();
|
|
||||||
case START:
|
|
||||||
return startSend();
|
|
||||||
case AUTHENTICATE_MAPS:
|
|
||||||
return authenticateToGoogleMaps();
|
|
||||||
case PICK_MAP:
|
|
||||||
return pickMap();
|
|
||||||
case SEND_TO_MAPS:
|
|
||||||
return sendToGoogleMaps();
|
|
||||||
case SEND_TO_MAPS_DONE:
|
|
||||||
return onSendToGoogleMapsDone();
|
|
||||||
case AUTHENTICATE_FUSION_TABLES:
|
|
||||||
return authenticateToFusionTables();
|
|
||||||
case SEND_TO_FUSION_TABLES:
|
|
||||||
return sendToFusionTables();
|
|
||||||
case SEND_TO_FUSION_TABLES_DONE:
|
|
||||||
return onSendToFusionTablesDone();
|
|
||||||
case AUTHENTICATE_DOCS:
|
|
||||||
return authenticateToGoogleDocs();
|
|
||||||
case AUTHENTICATE_TRIX:
|
|
||||||
return authenticateToGoogleTrix();
|
|
||||||
case SEND_TO_DOCS:
|
|
||||||
return sendToGoogleDocs();
|
|
||||||
case SEND_TO_DOCS_DONE:
|
|
||||||
return onSendToGoogleDocsDone();
|
|
||||||
case SHOW_RESULTS:
|
|
||||||
return onSendToGoogleDone();
|
|
||||||
case SHARE_LINK:
|
|
||||||
return shareLink();
|
|
||||||
case FINISH:
|
|
||||||
return onAllDone();
|
|
||||||
default:
|
|
||||||
Log.e(TAG, "Reached a non-executable state");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private SendState chooseAccount() {
|
|
||||||
Intent intent = new Intent(this, AccountChooserActivity.class);
|
|
||||||
startActivityForResult(intent, Constants.CHOOSE_ACCOUNT);
|
|
||||||
return SendState.NOT_READY;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initiates the process to send tracks to google.
|
|
||||||
* This is called once the user has selected sending options via the
|
|
||||||
* SendToGoogleDialog.
|
|
||||||
*/
|
|
||||||
private SendState startSend() {
|
|
||||||
showDialog(PROGRESS_DIALOG);
|
|
||||||
|
|
||||||
if (sendToMaps) {
|
|
||||||
return SendState.AUTHENTICATE_MAPS;
|
|
||||||
} else if (sendToFusionTables) {
|
|
||||||
return SendState.AUTHENTICATE_FUSION_TABLES;
|
|
||||||
} else if (sendToDocs) {
|
|
||||||
return SendState.AUTHENTICATE_DOCS;
|
|
||||||
} else {
|
|
||||||
Log.w(TAG, "Nowhere to upload to");
|
|
||||||
return SendState.FINISH;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Dialog createProgressDialog() {
|
|
||||||
progressDialog = new ProgressDialog(this);
|
|
||||||
progressDialog.setCancelable(false);
|
|
||||||
progressDialog.setIcon(android.R.drawable.ic_dialog_info);
|
|
||||||
progressDialog.setTitle(R.string.generic_progress_title);
|
|
||||||
progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
|
|
||||||
progressDialog.setMax(100);
|
|
||||||
progressDialog.setProgress(0);
|
|
||||||
|
|
||||||
return progressDialog;
|
|
||||||
}
|
|
||||||
|
|
||||||
private SendState authenticateToGoogleMaps() {
|
|
||||||
Log.d(TAG, "SendActivity.authenticateToGoogleMaps");
|
|
||||||
progressDialog.setProgress(0);
|
|
||||||
progressDialog.setMessage(getAuthenticatingProgressMessage(SendType.MAPS));
|
|
||||||
authenticate(Constants.AUTHENTICATE_TO_MAPS, MapsConstants.SERVICE_NAME);
|
|
||||||
// AUTHENTICATE_TO_MAPS callback calls sendToGoogleMaps
|
|
||||||
return SendState.NOT_READY;
|
|
||||||
}
|
|
||||||
|
|
||||||
private SendState pickMap() {
|
|
||||||
if (!sendToMapsNewMap) {
|
|
||||||
// Ask the user to choose a map to upload into
|
|
||||||
Intent listIntent = new Intent(this, MapsList.class);
|
|
||||||
listIntent.putExtra(MapsList.EXTRA_ACCOUNT_NAME, account.name);
|
|
||||||
listIntent.putExtra(MapsList.EXTRA_ACCOUNT_TYPE, account.type);
|
|
||||||
startActivityForResult(listIntent, Constants.GET_MAP);
|
|
||||||
// The callback for GET_MAP calls authenticateToGoogleMaps
|
|
||||||
return SendState.NOT_READY;
|
|
||||||
} else {
|
|
||||||
return SendState.SEND_TO_MAPS;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private SendState sendToGoogleMaps() {
|
|
||||||
tracker.trackPageView("/send/maps");
|
|
||||||
|
|
||||||
SendToMaps.OnSendCompletedListener onCompletion = new SendToMaps.OnSendCompletedListener() {
|
|
||||||
@Override
|
|
||||||
public void onSendCompleted(String mapId, boolean success) {
|
|
||||||
sendToMapsSuccess = success;
|
|
||||||
if (sendToMapsSuccess) {
|
|
||||||
sendToMapsMapId = mapId;
|
|
||||||
// Update the map id for this track:
|
|
||||||
try {
|
|
||||||
Track track = providerUtils.getTrack(sendTrackId);
|
|
||||||
if (track != null) {
|
|
||||||
track.setMapId(mapId);
|
|
||||||
providerUtils.updateTrack(track);
|
|
||||||
} else {
|
|
||||||
Log.w(TAG, "Updating map id failed.");
|
|
||||||
}
|
|
||||||
} catch (RuntimeException e) {
|
|
||||||
// If that fails whatever reasons we'll just log an error, but
|
|
||||||
// continue.
|
|
||||||
Log.w(TAG, "Updating map id failed.", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
executeStateMachine(SendState.SEND_TO_MAPS_DONE);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (sendToMapsMapId == null) {
|
|
||||||
sendToMapsMapId = SendToMaps.NEW_MAP_ID;
|
|
||||||
}
|
|
||||||
|
|
||||||
final SendToMaps sender = new SendToMaps(this, sendToMapsMapId,
|
|
||||||
getAuthManager(MapsConstants.SERVICE_NAME), sendTrackId, this /*progressIndicator*/,
|
|
||||||
onCompletion);
|
|
||||||
|
|
||||||
new Thread(sender, "SendToMaps").start();
|
|
||||||
|
|
||||||
return SendState.NOT_READY;
|
|
||||||
}
|
|
||||||
|
|
||||||
private SendState onSendToGoogleMapsDone() {
|
|
||||||
if (sendToFusionTables) {
|
|
||||||
return SendState.AUTHENTICATE_FUSION_TABLES;
|
|
||||||
} else if (sendToDocs) {
|
|
||||||
return SendState.AUTHENTICATE_DOCS;
|
|
||||||
} else {
|
|
||||||
return SendState.SHOW_RESULTS;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private SendState authenticateToFusionTables() {
|
|
||||||
progressDialog.setProgress(0);
|
|
||||||
progressDialog.setMessage(getAuthenticatingProgressMessage(SendType.FUSION_TABLES));
|
|
||||||
authenticate(Constants.AUTHENTICATE_TO_FUSION_TABLES, SendToFusionTables.SERVICE_ID);
|
|
||||||
// AUTHENTICATE_TO_FUSION_TABLES callback calls sendToFusionTables
|
|
||||||
return SendState.NOT_READY;
|
|
||||||
}
|
|
||||||
|
|
||||||
private SendState sendToFusionTables() {
|
|
||||||
tracker.trackPageView("/send/fusion_tables");
|
|
||||||
|
|
||||||
OnSendCompletedListener onCompletion = new OnSendCompletedListener() {
|
|
||||||
@Override
|
|
||||||
public void onSendCompleted(String tableId, boolean success) {
|
|
||||||
sendToFusionTablesSuccess = success;
|
|
||||||
if (sendToFusionTablesSuccess) {
|
|
||||||
sendToFusionTablesTableId = tableId;
|
|
||||||
// Update the table id for this track:
|
|
||||||
try {
|
|
||||||
Track track = providerUtils.getTrack(sendTrackId);
|
|
||||||
if (track != null) {
|
|
||||||
track.setTableId(tableId);
|
|
||||||
providerUtils.updateTrack(track);
|
|
||||||
} else {
|
|
||||||
Log.w(TAG, "Updating table id failed.");
|
|
||||||
}
|
|
||||||
} catch (RuntimeException e) {
|
|
||||||
// If that fails whatever reasons we'll just log an error, but
|
|
||||||
// continue.
|
|
||||||
Log.w(TAG, "Updating table id failed.", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
executeStateMachine(SendState.SEND_TO_FUSION_TABLES_DONE);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
final SendToFusionTables sender = new SendToFusionTables(
|
|
||||||
this, getAuthManager(SendToFusionTables.SERVICE_ID), sendTrackId,
|
|
||||||
this /* progressIndicator */, onCompletion);
|
|
||||||
|
|
||||||
new Thread(sender, "SendToFusionTables").start();
|
|
||||||
|
|
||||||
return SendState.NOT_READY;
|
|
||||||
}
|
|
||||||
|
|
||||||
private SendState onSendToFusionTablesDone() {
|
|
||||||
if (sendToDocs) {
|
|
||||||
return SendState.AUTHENTICATE_DOCS;
|
|
||||||
} else {
|
|
||||||
return SendState.SHOW_RESULTS;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private SendState authenticateToGoogleDocs() {
|
|
||||||
setProgressValue(0);
|
|
||||||
setProgressMessage(getAuthenticatingProgressMessage(SendType.DOCS));
|
|
||||||
authenticate(Constants.AUTHENTICATE_TO_DOCLIST, SendToDocs.GDATA_SERVICE_NAME_DOCLIST);
|
|
||||||
// AUTHENTICATE_TO_DOCLIST callback calls authenticateToGoogleTrix
|
|
||||||
return SendState.NOT_READY;
|
|
||||||
}
|
|
||||||
|
|
||||||
private SendState authenticateToGoogleTrix() {
|
|
||||||
setProgressValue(30);
|
|
||||||
setProgressMessage(getAuthenticatingProgressMessage(SendType.DOCS));
|
|
||||||
authenticate(Constants.AUTHENTICATE_TO_TRIX, SendToDocs.GDATA_SERVICE_NAME_TRIX);
|
|
||||||
// AUTHENTICATE_TO_TRIX callback calls sendToGoogleDocs
|
|
||||||
return SendState.NOT_READY;
|
|
||||||
}
|
|
||||||
|
|
||||||
private SendState sendToGoogleDocs() {
|
|
||||||
Log.d(TAG, "Sending to Docs....");
|
|
||||||
tracker.trackPageView("/send/docs");
|
|
||||||
|
|
||||||
setProgressValue(50);
|
|
||||||
String format = getString(R.string.send_google_progress_sending);
|
|
||||||
String serviceName = getString(SendType.DOCS.getServiceName());
|
|
||||||
setProgressMessage(String.format(format, serviceName));
|
|
||||||
final SendToDocs sender = new SendToDocs(this,
|
|
||||||
getAuthManager(SendToDocs.GDATA_SERVICE_NAME_TRIX),
|
|
||||||
getAuthManager(SendToDocs.GDATA_SERVICE_NAME_DOCLIST),
|
|
||||||
this);
|
|
||||||
Runnable onCompletion = new Runnable() {
|
|
||||||
public void run() {
|
|
||||||
setProgressValue(100);
|
|
||||||
sendToDocsSuccess = sender.wasSuccess();
|
|
||||||
executeStateMachine(SendState.SEND_TO_DOCS_DONE);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
sender.setOnCompletion(onCompletion);
|
|
||||||
sender.sendToDocs(sendTrackId);
|
|
||||||
|
|
||||||
return SendState.NOT_READY;
|
|
||||||
}
|
|
||||||
|
|
||||||
private SendState onSendToGoogleDocsDone() {
|
|
||||||
return SendState.SHOW_RESULTS;
|
|
||||||
}
|
|
||||||
|
|
||||||
private SendState onSendToGoogleDone() {
|
|
||||||
tracker.dispatch();
|
|
||||||
|
|
||||||
runOnUiThread(new Runnable() {
|
|
||||||
@Override
|
|
||||||
public void run() {
|
|
||||||
Log.d(TAG, "Sending to Google done.");
|
|
||||||
dismissDialog(PROGRESS_DIALOG);
|
|
||||||
// Ensure a new done dialog is created each time.
|
|
||||||
// This is required because the send results must be available at the
|
|
||||||
// time the dialog is created.
|
|
||||||
removeDialog(DONE_DIALOG);
|
|
||||||
showDialog(DONE_DIALOG);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return SendState.NOT_READY;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Dialog createDoneDialog() {
|
|
||||||
Log.d(TAG, "Creating done dialog");
|
|
||||||
// We've finished sending the track to the user-selected services. Now
|
|
||||||
// we tell them the results of the upload, and optionally share the track.
|
|
||||||
// There are a few different paths through this code:
|
|
||||||
//
|
|
||||||
// 1. The user pre-requested a share (shareRequested == true). We're going
|
|
||||||
// to display the result dialog *without* the share button (the share
|
|
||||||
// listener will be null). The OK button listener will initiate the
|
|
||||||
// share.
|
|
||||||
//
|
|
||||||
// 2. The user did not pre-request a share, and the set of services to
|
|
||||||
// which we succeeded in uploading the track are compatible with
|
|
||||||
// sharing. We'll display a share button (the share listener will be
|
|
||||||
// non-null), and will share the link if the user clicks it.
|
|
||||||
//
|
|
||||||
// 3. The user did not pre-request a share, and the set of services to
|
|
||||||
// which we succeeded in uploading the track are incompatible with
|
|
||||||
// sharing. We won't display a share button.
|
|
||||||
|
|
||||||
List<SendResult> results = makeSendToGoogleResults();
|
|
||||||
final boolean canShare = sendToFusionTablesTableId != null || sendToMapsMapId != null;
|
|
||||||
|
|
||||||
final OnClickListener finishListener = new DialogInterface.OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(DialogInterface dialog, int which) {
|
|
||||||
dialog.dismiss();
|
|
||||||
executeStateMachine(SendState.FINISH);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
DialogInterface.OnClickListener doShareListener = null;
|
|
||||||
if (canShare) {
|
|
||||||
doShareListener = new DialogInterface.OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(DialogInterface dialog, int which) {
|
|
||||||
dialog.dismiss();
|
|
||||||
|
|
||||||
executeStateMachine(SendState.SHARE_LINK);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
DialogInterface.OnClickListener onOkListener = (canShare && shareRequest)
|
|
||||||
? doShareListener : finishListener;
|
|
||||||
DialogInterface.OnClickListener onShareListener = (canShare && !shareRequest)
|
|
||||||
? doShareListener : null;
|
|
||||||
|
|
||||||
return ResultDialogFactory.makeDialog(
|
|
||||||
this, results, onOkListener, onShareListener, finishOnCancelListener);
|
|
||||||
}
|
|
||||||
|
|
||||||
private SendState onAllDone() {
|
|
||||||
Log.d(TAG, "All sending done.");
|
|
||||||
removeDialog(PROGRESS_DIALOG);
|
|
||||||
removeDialog(DONE_DIALOG);
|
|
||||||
progressDialog = null;
|
|
||||||
finish();
|
|
||||||
return SendState.DONE;
|
|
||||||
}
|
|
||||||
|
|
||||||
private SendState shareLink() {
|
|
||||||
String url = null;
|
|
||||||
if (sendToMaps && sendToMapsSuccess) {
|
|
||||||
// Prefer a link to Maps
|
|
||||||
url = MapsFacade.buildMapUrl(sendToMapsMapId);
|
|
||||||
} else if (sendToFusionTables && sendToFusionTablesSuccess) {
|
|
||||||
// Otherwise try using the link to fusion tables
|
|
||||||
url = getFusionTablesUrl(sendTrackId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (url != null) {
|
|
||||||
shareLinkToMap(url);
|
|
||||||
} else {
|
|
||||||
Log.w(TAG, "Failed to share link");
|
|
||||||
}
|
|
||||||
|
|
||||||
return SendState.FINISH;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shares a link to a Map or Fusion Table via external app (email, gmail, ...)
|
|
||||||
* A chooser with apps that support text/plain will be shown to the user.
|
|
||||||
*/
|
|
||||||
private void shareLinkToMap(String url) {
|
|
||||||
boolean shareUrlOnly = sharedPreferences.getBoolean(
|
|
||||||
getString(R.string.share_url_only_key), false);
|
|
||||||
String msg = shareUrlOnly ? url : String.format(
|
|
||||||
getResources().getText(R.string.share_track_url_body_format).toString(), url);
|
|
||||||
|
|
||||||
Intent shareIntent = new Intent(Intent.ACTION_SEND);
|
|
||||||
shareIntent.setType("text/plain");
|
|
||||||
shareIntent.putExtra(Intent.EXTRA_SUBJECT,
|
|
||||||
getResources().getText(R.string.share_track_subject).toString());
|
|
||||||
shareIntent.putExtra(Intent.EXTRA_TEXT, msg);
|
|
||||||
startActivity(Intent.createChooser(shareIntent,
|
|
||||||
getResources().getText(R.string.share_track_picker_title).toString()));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected String getFusionTablesUrl(long trackId) {
|
|
||||||
Track track = providerUtils.getTrack(trackId);
|
|
||||||
return SendToFusionTables.getMapVisualizationUrl(track);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a list of {@link SendResult} instances based on the set of
|
|
||||||
* services selected in {@link SendDialog} and the results as known to
|
|
||||||
* this class.
|
|
||||||
*/
|
|
||||||
private List<SendResult> makeSendToGoogleResults() {
|
|
||||||
List<SendResult> results = new ArrayList<SendResult>();
|
|
||||||
if (sendToMaps) {
|
|
||||||
results.add(new SendResult(SendType.MAPS, sendToMapsSuccess));
|
|
||||||
}
|
|
||||||
if (sendToFusionTables) {
|
|
||||||
results.add(new SendResult(SendType.FUSION_TABLES,
|
|
||||||
sendToFusionTablesSuccess));
|
|
||||||
}
|
|
||||||
if (sendToDocs) {
|
|
||||||
results.add(new SendResult(SendType.DOCS, sendToDocsSuccess));
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the authentication manager which obtains an authentication
|
|
||||||
* token, prompting the user for a login and password if needed.
|
|
||||||
*/
|
|
||||||
private void authenticate(final int requestCode, final String service) {
|
|
||||||
AuthManager authManager = getAuthManager(service);
|
|
||||||
authManager.doLogin(new AuthCallback() {
|
|
||||||
@Override
|
|
||||||
public void onAuthResult(boolean success) {
|
|
||||||
Log.i(TAG, "Login success for " + service + ": " + success);
|
|
||||||
if (!success) {
|
|
||||||
executeStateMachine(SendState.SHOW_RESULTS);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
onLoginSuccess(requestCode);
|
|
||||||
}
|
|
||||||
}, account);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onActivityResult(int requestCode, int resultCode,
|
|
||||||
final Intent results) {
|
|
||||||
SendState nextState = null;
|
|
||||||
switch (requestCode) {
|
|
||||||
case Constants.GET_MAP: {
|
|
||||||
// User picked a map to upload to
|
|
||||||
Log.d(TAG, "Get map result: " + resultCode);
|
|
||||||
if (resultCode == RESULT_OK) {
|
|
||||||
if (results.hasExtra("mapid")) {
|
|
||||||
sendToMapsMapId = results.getStringExtra("mapid");
|
|
||||||
}
|
|
||||||
nextState = SendState.SEND_TO_MAPS;
|
|
||||||
} else {
|
|
||||||
nextState = SendState.FINISH;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case Constants.CHOOSE_ACCOUNT: {
|
|
||||||
if (resultCode == RESULT_CANCELED) {
|
|
||||||
nextState = SendState.FINISH;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
account = results.getParcelableExtra(AccountChooserActivity.ACCOUNT);
|
|
||||||
if (account == null) {
|
|
||||||
nextState = SendState.FINISH;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
nextState = SendState.START;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
Log.e(TAG, "Unrequested result: " + requestCode);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextState != null) {
|
|
||||||
executeStateMachine(nextState);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onLoginSuccess(int requestCode) {
|
|
||||||
SendState nextState;
|
|
||||||
switch (requestCode) {
|
|
||||||
case Constants.AUTHENTICATE_TO_MAPS:
|
|
||||||
// Authenticated with Google Maps
|
|
||||||
nextState = SendState.PICK_MAP;
|
|
||||||
break;
|
|
||||||
case Constants.AUTHENTICATE_TO_FUSION_TABLES:
|
|
||||||
// Authenticated with Google Fusion Tables
|
|
||||||
nextState = SendState.SEND_TO_FUSION_TABLES;
|
|
||||||
break;
|
|
||||||
case Constants.AUTHENTICATE_TO_DOCLIST:
|
|
||||||
// Authenticated with Google Docs
|
|
||||||
nextState = SendState.AUTHENTICATE_TRIX;
|
|
||||||
break;
|
|
||||||
case Constants.AUTHENTICATE_TO_TRIX:
|
|
||||||
// Authenticated with Trix
|
|
||||||
nextState = SendState.SEND_TO_DOCS;
|
|
||||||
break;
|
|
||||||
|
|
||||||
default: {
|
|
||||||
Log.e(TAG, "Unrequested login code: " + requestCode);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
executeStateMachine(nextState);
|
|
||||||
}
|
|
||||||
|
|
||||||
private AuthManager getAuthManager(String service) {
|
|
||||||
AuthManager authManager = authMap.get(service);
|
|
||||||
if (authManager == null) {
|
|
||||||
authManager = AuthManagerFactory.getAuthManager(this,
|
|
||||||
Constants.GET_LOGIN,
|
|
||||||
null,
|
|
||||||
true,
|
|
||||||
service);
|
|
||||||
authMap.put(service, authManager);
|
|
||||||
}
|
|
||||||
return authManager;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Resets status information for sending to Maps/Fusion Tables/Docs.
|
|
||||||
*/
|
|
||||||
private void resetState() {
|
|
||||||
account = null;
|
|
||||||
authMap.clear();
|
|
||||||
currentState = SendState.CHOOSE_ACCOUNT;
|
|
||||||
sendToMapsSuccess = false;
|
|
||||||
sendToFusionTablesSuccess = false;
|
|
||||||
sendToDocsSuccess = false;
|
|
||||||
sendToMapsMapId = null;
|
|
||||||
sendToFusionTablesTableId = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets a progress message indicating My Tracks is authenticating to a
|
|
||||||
* service.
|
|
||||||
*
|
|
||||||
* @param type the type of service
|
|
||||||
*/
|
|
||||||
private String getAuthenticatingProgressMessage(SendType type) {
|
|
||||||
String format = getString(R.string.send_google_progress_authenticating);
|
|
||||||
String serviceName = getString(type.getServiceName());
|
|
||||||
return String.format(format, serviceName);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setProgressMessage(final String message) {
|
|
||||||
runOnUiThread(new Runnable() {
|
|
||||||
public void run() {
|
|
||||||
if (progressDialog != null) {
|
|
||||||
progressDialog.setMessage(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setProgressValue(final int percent) {
|
|
||||||
runOnUiThread(new Runnable() {
|
|
||||||
public void run() {
|
|
||||||
if (progressDialog != null) {
|
|
||||||
progressDialog.setProgress(percent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void clearProgressMessage() {
|
|
||||||
progressDialog.setMessage("");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,294 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012 Google Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||||
|
* use this file except in compliance with the License. You may obtain a copy of
|
||||||
|
* the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations under
|
||||||
|
* the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.google.android.apps.mytracks.io.sendtogoogle;
|
||||||
|
|
||||||
|
import android.accounts.Account;
|
||||||
|
import android.os.Parcel;
|
||||||
|
import android.os.Parcelable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send request states for sending a track to Google Maps, Google Fusion Tables,
|
||||||
|
* and Google Docs.
|
||||||
|
*
|
||||||
|
* @author Jimmy Shih
|
||||||
|
*/
|
||||||
|
public class SendRequest implements Parcelable {
|
||||||
|
|
||||||
|
public static final String SEND_REQUEST_KEY = "sendRequest";
|
||||||
|
|
||||||
|
private long trackId = -1L;
|
||||||
|
private boolean showMaps = false;
|
||||||
|
private boolean showFusionTables = false;
|
||||||
|
private boolean showDocs = false;
|
||||||
|
private boolean sendMaps = false;
|
||||||
|
private boolean sendFusionTables = false;
|
||||||
|
private boolean sendDocs = false;
|
||||||
|
private boolean newMap = false;
|
||||||
|
private Account account = null;
|
||||||
|
private String mapId = null;
|
||||||
|
private boolean mapsSuccess = false;
|
||||||
|
private boolean docsSuccess = false;
|
||||||
|
private boolean fusionTablesSuccess = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new send request.
|
||||||
|
*
|
||||||
|
* @param trackId the track id
|
||||||
|
* @param showMaps true to show the Google Maps option
|
||||||
|
* @param showFusionTables true to show the Google Fusion Tables option
|
||||||
|
* @param showDocs true to show the Google Docs option
|
||||||
|
*/
|
||||||
|
public SendRequest(long trackId, boolean showMaps, boolean showFusionTables, boolean showDocs) {
|
||||||
|
this.trackId = trackId;
|
||||||
|
this.showMaps = showMaps;
|
||||||
|
this.showFusionTables = showFusionTables;
|
||||||
|
this.showDocs = showDocs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the track id.
|
||||||
|
*/
|
||||||
|
public long getTrackId() {
|
||||||
|
return trackId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if showing the send to Google Maps option.
|
||||||
|
*/
|
||||||
|
public boolean isShowMaps() {
|
||||||
|
return showMaps;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if showing the send to Google Fusion Tables option.
|
||||||
|
*/
|
||||||
|
public boolean isShowFusionTables() {
|
||||||
|
return showFusionTables;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if showing the send to Google Docs option.
|
||||||
|
*/
|
||||||
|
public boolean isShowDocs() {
|
||||||
|
return showDocs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if showing all the send options.
|
||||||
|
*/
|
||||||
|
public boolean isShowAll() {
|
||||||
|
return showMaps && showFusionTables && showDocs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if the user has selected the send to Google Maps option.
|
||||||
|
*/
|
||||||
|
public boolean isSendMaps() {
|
||||||
|
return sendMaps;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the send to Google Maps option.
|
||||||
|
*
|
||||||
|
* @param sendMaps true if the user has selected the send to Google Maps
|
||||||
|
* option
|
||||||
|
*/
|
||||||
|
public void setSendMaps(boolean sendMaps) {
|
||||||
|
this.sendMaps = sendMaps;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if the user has selected the send to Google Fusion Tables option.
|
||||||
|
*/
|
||||||
|
public boolean isSendFusionTables() {
|
||||||
|
return sendFusionTables;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the send to Google Fusion Tables option.
|
||||||
|
*
|
||||||
|
* @param sendFusionTables true if the user has selected the send to Google
|
||||||
|
* Fusion Tables option
|
||||||
|
*/
|
||||||
|
public void setSendFusionTables(boolean sendFusionTables) {
|
||||||
|
this.sendFusionTables = sendFusionTables;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if the user has selected the send to Google Docs option.
|
||||||
|
*/
|
||||||
|
public boolean isSendDocs() {
|
||||||
|
return sendDocs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the send to Google Docs option.
|
||||||
|
*
|
||||||
|
* @param sendDocs true if the user has selected the send to Google Docs
|
||||||
|
* option
|
||||||
|
*/
|
||||||
|
public void setSendDocs(boolean sendDocs) {
|
||||||
|
this.sendDocs = sendDocs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if the user has selected to create a new Google Maps.
|
||||||
|
*/
|
||||||
|
public boolean isNewMap() {
|
||||||
|
return newMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the new map option.
|
||||||
|
*
|
||||||
|
* @param newMap true if the user has selected to create a new Google Maps.
|
||||||
|
*/
|
||||||
|
public void setNewMap(boolean newMap) {
|
||||||
|
this.newMap = newMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the account.
|
||||||
|
*/
|
||||||
|
public Account getAccount() {
|
||||||
|
return account;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the account.
|
||||||
|
*
|
||||||
|
* @param account the account
|
||||||
|
*/
|
||||||
|
public void setAccount(Account account) {
|
||||||
|
this.account = account;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the selected map id if the user has selected to send a track to an
|
||||||
|
* existing Google Maps.
|
||||||
|
*/
|
||||||
|
public String getMapId() {
|
||||||
|
return mapId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the map id.
|
||||||
|
*
|
||||||
|
* @param mapId the map id
|
||||||
|
*/
|
||||||
|
public void setMapId(String mapId) {
|
||||||
|
this.mapId = mapId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if sending to Google Maps is success.
|
||||||
|
*/
|
||||||
|
public boolean isMapsSuccess() {
|
||||||
|
return mapsSuccess;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the Google Maps result.
|
||||||
|
*
|
||||||
|
* @param mapsSuccess true if sending to Google Maps is success
|
||||||
|
*/
|
||||||
|
public void setMapsSuccess(boolean mapsSuccess) {
|
||||||
|
this.mapsSuccess = mapsSuccess;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if sending to Google Fusion Tables is success.
|
||||||
|
*/
|
||||||
|
public boolean isFusionTablesSuccess() {
|
||||||
|
return fusionTablesSuccess;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the Google Fusion Tables result.
|
||||||
|
*
|
||||||
|
* @param fusionTablesSuccess true if sending to Google Fusion Tables is
|
||||||
|
* success
|
||||||
|
*/
|
||||||
|
public void setFusionTablesSuccess(boolean fusionTablesSuccess) {
|
||||||
|
this.fusionTablesSuccess = fusionTablesSuccess;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if sending to Google Docs is success.
|
||||||
|
*/
|
||||||
|
public boolean isDocsSuccess() {
|
||||||
|
return docsSuccess;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the Google Docs result.
|
||||||
|
*
|
||||||
|
* @param docsSuccess true if sending to Google Docs is success
|
||||||
|
*/
|
||||||
|
public void setDocsSuccess(boolean docsSuccess) {
|
||||||
|
this.docsSuccess = docsSuccess;
|
||||||
|
}
|
||||||
|
|
||||||
|
private SendRequest(Parcel in) {
|
||||||
|
trackId = in.readLong();
|
||||||
|
showMaps = in.readByte() == 1;
|
||||||
|
showFusionTables = in.readByte() == 1;
|
||||||
|
showDocs = in.readByte() == 1;
|
||||||
|
sendMaps = in.readByte() == 1;
|
||||||
|
sendFusionTables = in.readByte() == 1;
|
||||||
|
sendDocs = in.readByte() == 1;
|
||||||
|
newMap = in.readByte() == 1;
|
||||||
|
account = in.readParcelable(null);
|
||||||
|
mapId = in.readString();
|
||||||
|
mapsSuccess = in.readByte() == 1;
|
||||||
|
fusionTablesSuccess = in.readByte() == 1;
|
||||||
|
docsSuccess = in.readByte() == 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int describeContents() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeToParcel(Parcel out, int flags) {
|
||||||
|
out.writeLong(trackId);
|
||||||
|
out.writeByte((byte) (showMaps ? 1 : 0));
|
||||||
|
out.writeByte((byte) (showFusionTables ? 1 : 0));
|
||||||
|
out.writeByte((byte) (showDocs ? 1 : 0));
|
||||||
|
out.writeByte((byte) (sendMaps ? 1 : 0));
|
||||||
|
out.writeByte((byte) (sendFusionTables ? 1 : 0));
|
||||||
|
out.writeByte((byte) (sendDocs ? 1 : 0));
|
||||||
|
out.writeByte((byte) (newMap ? 1 : 0));
|
||||||
|
out.writeParcelable(account, 0);
|
||||||
|
out.writeString(mapId);
|
||||||
|
out.writeByte((byte) (mapsSuccess ? 1 : 0));
|
||||||
|
out.writeByte((byte) (fusionTablesSuccess ? 1 : 0));
|
||||||
|
out.writeByte((byte) (docsSuccess ? 1 : 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final Parcelable.Creator<SendRequest> CREATOR = new Parcelable.Creator<
|
||||||
|
SendRequest>() {
|
||||||
|
public SendRequest createFromParcel(Parcel in) {
|
||||||
|
return new SendRequest(in);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SendRequest[] newArray(int size) {
|
||||||
|
return new SendRequest[size];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2011 Google Inc.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
|
||||||
* use this file except in compliance with the License. You may obtain a copy of
|
|
||||||
* the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
* License for the specific language governing permissions and limitations under
|
|
||||||
* the License.
|
|
||||||
*/
|
|
||||||
package com.google.android.apps.mytracks.io.sendtogoogle;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This class represents the the result of the uploading of track data to a
|
|
||||||
* single service.
|
|
||||||
*
|
|
||||||
* @author Matthew Simmons
|
|
||||||
*/
|
|
||||||
public class SendResult {
|
|
||||||
private final SendType type;
|
|
||||||
private final boolean success;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param type the service to which track data was uploaded
|
|
||||||
* @param success true if the uploading succeeded
|
|
||||||
*/
|
|
||||||
public SendResult(SendType type, boolean success) {
|
|
||||||
this.type = type;
|
|
||||||
this.success = success;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns the service to which the track data was uploaded */
|
|
||||||
public SendType getType() {
|
|
||||||
return type;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns true if the uploading succeeded */
|
|
||||||
public boolean isSuccess() {
|
|
||||||
return success;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012 Google Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||||
|
* use this file except in compliance with the License. You may obtain a copy of
|
||||||
|
* the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations under
|
||||||
|
* the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.google.android.apps.mytracks.io.sendtogoogle;
|
||||||
|
|
||||||
|
import com.google.android.apps.mytracks.content.Track;
|
||||||
|
import com.google.android.apps.mytracks.stats.TripStatistics;
|
||||||
|
import com.google.android.apps.mytracks.util.LocationUtils;
|
||||||
|
|
||||||
|
import android.location.Location;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Commons utilities for sending a track to Google.
|
||||||
|
*
|
||||||
|
* @author Jimmy Shih
|
||||||
|
*/
|
||||||
|
public class SendToGoogleUtils {
|
||||||
|
|
||||||
|
private static final String TAG = SendToGoogleUtils.class.getSimpleName();
|
||||||
|
|
||||||
|
private SendToGoogleUtils() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepares a list of locations to send to Google Maps or Google Fusion
|
||||||
|
* Tables. Splits the locations into segments if necessary.
|
||||||
|
*
|
||||||
|
* @param track the track
|
||||||
|
* @param locations the list of locations
|
||||||
|
* @return an array of split segments.
|
||||||
|
*/
|
||||||
|
public static ArrayList<Track> prepareLocations(Track track, List<Location> locations) {
|
||||||
|
ArrayList<Track> splitTracks = new ArrayList<Track>();
|
||||||
|
|
||||||
|
// Create a new segment
|
||||||
|
Track segment = new Track();
|
||||||
|
segment.setId(track.getId());
|
||||||
|
segment.setName(track.getName());
|
||||||
|
segment.setDescription("");
|
||||||
|
segment.setCategory(track.getCategory());
|
||||||
|
|
||||||
|
TripStatistics segmentStats = segment.getStatistics();
|
||||||
|
TripStatistics trackStats = track.getStatistics();
|
||||||
|
segmentStats.setStartTime(trackStats.getStartTime());
|
||||||
|
segmentStats.setStopTime(trackStats.getStopTime());
|
||||||
|
boolean startNewTrackSegment = false;
|
||||||
|
for (Location loc : locations) {
|
||||||
|
// Latitude is greater than 90 if the location is invalid. Do not add to
|
||||||
|
// the segment.
|
||||||
|
if (loc.getLatitude() > 90) {
|
||||||
|
startNewTrackSegment = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startNewTrackSegment) {
|
||||||
|
// Close the last segment
|
||||||
|
prepareTrackSegment(segment, splitTracks);
|
||||||
|
|
||||||
|
startNewTrackSegment = false;
|
||||||
|
segment = new Track();
|
||||||
|
segment.setId(track.getId());
|
||||||
|
segment.setName(track.getName());
|
||||||
|
segment.setDescription("");
|
||||||
|
segment.setCategory(track.getCategory());
|
||||||
|
segmentStats = segment.getStatistics();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loc.getLatitude() <= 90) {
|
||||||
|
segment.addLocation(loc);
|
||||||
|
|
||||||
|
// For a new segment, sets its start time using the first available
|
||||||
|
// location time.
|
||||||
|
if (segmentStats.getStartTime() < 0) {
|
||||||
|
segmentStats.setStartTime(loc.getTime());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
prepareTrackSegment(segment, splitTracks);
|
||||||
|
|
||||||
|
return splitTracks;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepares a track segment for sending to Google Maps or Google Fusion
|
||||||
|
* Tables. The main steps are:
|
||||||
|
* <ul>
|
||||||
|
* <li>make sure the segment has at least 2 points</li>
|
||||||
|
* <li>set the segment stop time if necessary</li>
|
||||||
|
* <li>decimate locations precision</li>
|
||||||
|
* </ul>
|
||||||
|
* The prepared track will be added to the splitTracks.
|
||||||
|
*
|
||||||
|
* @param segment the track segment
|
||||||
|
* @param splitTracks an array of track segments
|
||||||
|
*/
|
||||||
|
private static void prepareTrackSegment(Track segment, ArrayList<Track> splitTracks) {
|
||||||
|
// Make sure the segment has at least 2 points
|
||||||
|
if (segment.getLocations().size() < 2) {
|
||||||
|
Log.d(TAG, "segment has less than 2 points");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For a new segment, sets it stop time
|
||||||
|
TripStatistics segmentStats = segment.getStatistics();
|
||||||
|
if (segmentStats.getStopTime() < 0) {
|
||||||
|
Location lastLocation = segment.getLocations().get(segment.getLocations().size() - 1);
|
||||||
|
segmentStats.setStopTime(lastLocation.getTime());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decimate to 2 meter precision. Google Maps and Google Fusion Tables do
|
||||||
|
// not like the locations to be too precise.
|
||||||
|
LocationUtils.decimate(segment, 2.0);
|
||||||
|
|
||||||
|
splitTracks.add(segment);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2011 Google Inc.
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
|
||||||
* use this file except in compliance with the License. You may obtain a copy of
|
|
||||||
* the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
* License for the specific language governing permissions and limitations under
|
|
||||||
* the License.
|
|
||||||
*/
|
|
||||||
package com.google.android.apps.mytracks.io.sendtogoogle;
|
|
||||||
|
|
||||||
import com.google.android.maps.mytracks.R;
|
|
||||||
|
|
||||||
import android.os.Parcel;
|
|
||||||
import android.os.Parcelable;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enumerates the services to which we can upload track data.
|
|
||||||
*
|
|
||||||
* @author Matthew Simmons
|
|
||||||
*/
|
|
||||||
public enum SendType implements Parcelable {
|
|
||||||
MAPS(R.string.send_google_maps,
|
|
||||||
R.string.send_google_maps_url),
|
|
||||||
FUSION_TABLES(R.string.send_google_fusion_tables,
|
|
||||||
R.string.send_google_fusion_tables_url),
|
|
||||||
DOCS(R.string.send_google_docs,
|
|
||||||
R.string.send_google_docs_url);
|
|
||||||
|
|
||||||
private int serviceName;
|
|
||||||
private int serviceUrl;
|
|
||||||
|
|
||||||
private SendType(int serviceName, int serviceUrl) {
|
|
||||||
this.serviceName = serviceName;
|
|
||||||
this.serviceUrl = serviceUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns the resource ID for the printable (short) name of the service */
|
|
||||||
public int getServiceName() {
|
|
||||||
return serviceName;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns the resource ID for the service's URL */
|
|
||||||
public int getServiceUrl() {
|
|
||||||
return serviceUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int describeContents() {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void writeToParcel(Parcel dest, int flags) {
|
|
||||||
dest.writeInt(ordinal());
|
|
||||||
}
|
|
||||||
|
|
||||||
public static final Creator<SendType> CREATOR = new Creator<SendType>() {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public SendType createFromParcel(Parcel source) {
|
|
||||||
return SendType.values()[source.readInt()];
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public SendType[] newArray(int size) {
|
|
||||||
return new SendType[size];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
+194
@@ -0,0 +1,194 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012 Google Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||||
|
* use this file except in compliance with the License. You may obtain a copy of
|
||||||
|
* the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations under
|
||||||
|
* the License.
|
||||||
|
*/
|
||||||
|
package com.google.android.apps.mytracks.io.sendtogoogle;
|
||||||
|
|
||||||
|
import com.google.android.apps.mytracks.Constants;
|
||||||
|
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
|
||||||
|
import com.google.android.apps.mytracks.content.Track;
|
||||||
|
import com.google.android.apps.mytracks.io.fusiontables.SendFusionTablesUtils;
|
||||||
|
import com.google.android.apps.mytracks.io.maps.SendMapsUtils;
|
||||||
|
import com.google.android.maps.mytracks.R;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.app.AlertDialog;
|
||||||
|
import android.app.Dialog;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.DialogInterface;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A dialog to show the result of uploading to Google services.
|
||||||
|
*
|
||||||
|
* @author jshih@google.com (Jimmy Shih)
|
||||||
|
*/
|
||||||
|
public class UploadResultActivity extends Activity {
|
||||||
|
|
||||||
|
private static final String TEXT_PLAIN_TYPE = "text/plain";
|
||||||
|
private static final int RESULT_DIALOG = 1;
|
||||||
|
|
||||||
|
private SendRequest sendRequest;
|
||||||
|
private Track track;
|
||||||
|
private String shareUrl;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
sendRequest = getIntent().getParcelableExtra(SendRequest.SEND_REQUEST_KEY);
|
||||||
|
track = null;
|
||||||
|
shareUrl = null;
|
||||||
|
|
||||||
|
if (sendRequest.isSendMaps() && sendRequest.isMapsSuccess()) {
|
||||||
|
shareUrl = SendMapsUtils.getMapUrl(getTrack());
|
||||||
|
}
|
||||||
|
if (shareUrl == null && sendRequest.isSendFusionTables()
|
||||||
|
&& sendRequest.isFusionTablesSuccess()) {
|
||||||
|
shareUrl = SendFusionTablesUtils.getMapUrl(getTrack());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Track getTrack() {
|
||||||
|
if (track == null) {
|
||||||
|
track = MyTracksProviderUtils.Factory.get(this).getTrack(sendRequest.getTrackId());
|
||||||
|
}
|
||||||
|
return track;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onResume() {
|
||||||
|
super.onResume();
|
||||||
|
showDialog(RESULT_DIALOG);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Dialog onCreateDialog(int id) {
|
||||||
|
switch (id) {
|
||||||
|
case RESULT_DIALOG:
|
||||||
|
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||||
|
View view = getLayoutInflater().inflate(R.layout.upload_result, null);
|
||||||
|
builder.setView(view);
|
||||||
|
|
||||||
|
LinearLayout mapsResult = (LinearLayout) view.findViewById(R.id.upload_result_maps_result);
|
||||||
|
LinearLayout fusionTablesResult = (LinearLayout) view.findViewById(
|
||||||
|
R.id.upload_result_fusion_tables_result);
|
||||||
|
LinearLayout docsResult = (LinearLayout) view.findViewById(R.id.upload_result_docs_result);
|
||||||
|
|
||||||
|
ImageView mapsResultIcon = (ImageView) view.findViewById(
|
||||||
|
R.id.upload_result_maps_result_icon);
|
||||||
|
ImageView fusionTablesResultIcon = (ImageView) view.findViewById(
|
||||||
|
R.id.upload_result_fusion_tables_result_icon);
|
||||||
|
ImageView docsResultIcon = (ImageView) view.findViewById(
|
||||||
|
R.id.upload_result_docs_result_icon);
|
||||||
|
|
||||||
|
TextView successFooter = (TextView) view.findViewById(R.id.upload_result_success_footer);
|
||||||
|
TextView errorFooter = (TextView) view.findViewById(R.id.upload_result_error_footer);
|
||||||
|
|
||||||
|
boolean hasError = false;
|
||||||
|
if (!sendRequest.isSendMaps()) {
|
||||||
|
mapsResult.setVisibility(View.GONE);
|
||||||
|
} else {
|
||||||
|
if (!sendRequest.isMapsSuccess()) {
|
||||||
|
mapsResultIcon.setImageResource(R.drawable.failure);
|
||||||
|
hasError = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sendRequest.isSendFusionTables()) {
|
||||||
|
fusionTablesResult.setVisibility(View.GONE);
|
||||||
|
} else {
|
||||||
|
if (!sendRequest.isFusionTablesSuccess()) {
|
||||||
|
fusionTablesResultIcon.setImageResource(R.drawable.failure);
|
||||||
|
hasError = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sendRequest.isSendDocs()) {
|
||||||
|
docsResult.setVisibility(View.GONE);
|
||||||
|
} else {
|
||||||
|
if (!sendRequest.isDocsSuccess()) {
|
||||||
|
docsResultIcon.setImageResource(R.drawable.failure);
|
||||||
|
hasError = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasError) {
|
||||||
|
builder.setTitle(R.string.generic_error_title);
|
||||||
|
builder.setIcon(android.R.drawable.ic_dialog_alert);
|
||||||
|
successFooter.setVisibility(View.GONE);
|
||||||
|
} else {
|
||||||
|
builder.setTitle(R.string.generic_success_title);
|
||||||
|
builder.setIcon(android.R.drawable.ic_dialog_info);
|
||||||
|
errorFooter.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.setCancelable(true);
|
||||||
|
builder.setOnCancelListener(new DialogInterface.OnCancelListener() {
|
||||||
|
@Override
|
||||||
|
public void onCancel(DialogInterface dialog) {
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
builder.setPositiveButton(
|
||||||
|
getString(R.string.generic_ok), new DialogInterface.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(DialogInterface dialog, int which) {
|
||||||
|
if (!sendRequest.isShowAll() && shareUrl != null) {
|
||||||
|
startShareUrlActivity(shareUrl);
|
||||||
|
}
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add a Share URL button if showing all the options and a shareUrl
|
||||||
|
// exists
|
||||||
|
if (sendRequest.isShowAll() && shareUrl != null) {
|
||||||
|
builder.setNegativeButton(getString(R.string.send_google_result_share_url),
|
||||||
|
new DialogInterface.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(DialogInterface dialog, int which) {
|
||||||
|
startShareUrlActivity(shareUrl);
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return builder.create();
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts an activity to share the url.
|
||||||
|
*
|
||||||
|
* @param url the url
|
||||||
|
*/
|
||||||
|
private void startShareUrlActivity(String url) {
|
||||||
|
SharedPreferences prefs = getSharedPreferences(Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
|
||||||
|
boolean shareUrlOnly = prefs.getBoolean(getString(R.string.share_url_only_key), false);
|
||||||
|
|
||||||
|
Intent intent = new Intent(Intent.ACTION_SEND);
|
||||||
|
intent.setType(TEXT_PLAIN_TYPE);
|
||||||
|
intent.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.share_track_subject));
|
||||||
|
intent.putExtra(Intent.EXTRA_TEXT,
|
||||||
|
shareUrlOnly ? url : getString(R.string.share_track_url_body_format, url));
|
||||||
|
startActivity(Intent.createChooser(intent, getString(R.string.share_track_picker_title)));
|
||||||
|
}
|
||||||
|
}
|
||||||
+61
-67
@@ -15,8 +15,10 @@
|
|||||||
*/
|
*/
|
||||||
package com.google.android.apps.mytracks.io.sendtogoogle;
|
package com.google.android.apps.mytracks.io.sendtogoogle;
|
||||||
|
|
||||||
|
import com.google.android.apps.analytics.GoogleAnalyticsTracker;
|
||||||
import com.google.android.apps.mytracks.Constants;
|
import com.google.android.apps.mytracks.Constants;
|
||||||
import com.google.android.apps.mytracks.util.ApiFeatures;
|
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
|
||||||
|
import com.google.android.apps.mytracks.util.SystemUtils;
|
||||||
import com.google.android.maps.mytracks.R;
|
import com.google.android.maps.mytracks.R;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
@@ -43,20 +45,9 @@ import android.widget.TableRow;
|
|||||||
*/
|
*/
|
||||||
public class UploadServiceChooserActivity extends Activity {
|
public class UploadServiceChooserActivity extends Activity {
|
||||||
|
|
||||||
/**
|
|
||||||
* The track id.
|
|
||||||
*/
|
|
||||||
public static final String TRACK_ID = "trackId";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The send type. Null to send to all Google services.
|
|
||||||
*/
|
|
||||||
public static final String SEND_TYPE = "sendType";
|
|
||||||
|
|
||||||
private static final int SERVICE_PICKER_DIALOG = 1;
|
private static final int SERVICE_PICKER_DIALOG = 1;
|
||||||
|
|
||||||
private long trackId;
|
private SendRequest sendRequest;
|
||||||
private SendType sendType;
|
|
||||||
|
|
||||||
private TableRow mapsTableRow;
|
private TableRow mapsTableRow;
|
||||||
private TableRow fusionTablesTableRow;
|
private TableRow fusionTablesTableRow;
|
||||||
@@ -76,10 +67,7 @@ public class UploadServiceChooserActivity extends Activity {
|
|||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
sendRequest = getIntent().getParcelableExtra(SendRequest.SEND_REQUEST_KEY);
|
||||||
Intent intent = getIntent();
|
|
||||||
trackId = intent.getLongExtra(TRACK_ID, -1L);
|
|
||||||
sendType = intent.getParcelableExtra(SEND_TYPE);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -136,23 +124,15 @@ public class UploadServiceChooserActivity extends Activity {
|
|||||||
send.setOnClickListener(new View.OnClickListener() {
|
send.setOnClickListener(new View.OnClickListener() {
|
||||||
public void onClick(View v) {
|
public void onClick(View v) {
|
||||||
saveState();
|
saveState();
|
||||||
Intent intent = new Intent(UploadServiceChooserActivity.this, SendActivity.class);
|
startNextActivity();
|
||||||
intent.putExtra(SendActivity.TRACK_ID, trackId);
|
|
||||||
intent.putExtra(SendActivity.SHARE_URL, sendType != null);
|
|
||||||
intent.putExtra(SendActivity.SEND_MAPS, sendMaps());
|
|
||||||
intent.putExtra(SendActivity.SEND_FUSION_TABLES, sendFusionTables());
|
|
||||||
intent.putExtra(SendActivity.SEND_DOCS, sendDocs());
|
|
||||||
intent.putExtra(SendActivity.CREATE_MAP, !existingMapRadioButton.isChecked());
|
|
||||||
startActivity(intent);
|
|
||||||
finish();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Setup initial state
|
// Setup initial state
|
||||||
initState();
|
initState();
|
||||||
|
|
||||||
// Update state based on sendType
|
// Update state based on sendRequest
|
||||||
updateStateBySendType();
|
updateStateBySendRequest();
|
||||||
|
|
||||||
// Update state based on current user selection
|
// Update state based on current user selection
|
||||||
updateStateBySelection();
|
updateStateBySelection();
|
||||||
@@ -180,21 +160,21 @@ public class UploadServiceChooserActivity extends Activity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the UI state based on the sendType.
|
* Updates the UI state based on sendRequest.
|
||||||
*/
|
*/
|
||||||
private void updateStateBySendType() {
|
private void updateStateBySendRequest() {
|
||||||
if (sendType == SendType.MAPS) {
|
if (!sendRequest.isShowAll()) {
|
||||||
mapsCheckBox.setChecked(true);
|
if (sendRequest.isShowMaps()) {
|
||||||
} else if (sendType == SendType.FUSION_TABLES) {
|
mapsCheckBox.setChecked(true);
|
||||||
fusionTablesCheckBox.setChecked(true);
|
} else if (sendRequest.isShowFusionTables()) {
|
||||||
} else if (sendType == SendType.DOCS) {
|
fusionTablesCheckBox.setChecked(true);
|
||||||
docsCheckBox.setChecked(true);
|
} else if (sendRequest.isShowDocs()) {
|
||||||
} else {
|
docsCheckBox.setChecked(true);
|
||||||
// sendType == null
|
}
|
||||||
}
|
}
|
||||||
mapsTableRow.setVisibility(showMaps() ? View.VISIBLE : View.GONE);
|
mapsTableRow.setVisibility(sendRequest.isShowMaps() ? View.VISIBLE : View.GONE);
|
||||||
fusionTablesTableRow.setVisibility(showFusionTables() ? View.VISIBLE : View.GONE);
|
fusionTablesTableRow.setVisibility(sendRequest.isShowFusionTables() ? View.VISIBLE : View.GONE);
|
||||||
docsTableRow.setVisibility(showDocs() ? View.VISIBLE : View.GONE);
|
docsTableRow.setVisibility(sendRequest.isShowDocs() ? View.VISIBLE : View.GONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -213,53 +193,67 @@ public class UploadServiceChooserActivity extends Activity {
|
|||||||
Editor editor = prefs.edit();
|
Editor editor = prefs.edit();
|
||||||
editor.putBoolean(
|
editor.putBoolean(
|
||||||
getString(R.string.pick_existing_map_key), existingMapRadioButton.isChecked());
|
getString(R.string.pick_existing_map_key), existingMapRadioButton.isChecked());
|
||||||
if (sendType == null) {
|
if (sendRequest.isShowAll()) {
|
||||||
editor.putBoolean(getString(R.string.send_to_maps_key), sendMaps());
|
editor.putBoolean(getString(R.string.send_to_maps_key), sendMaps());
|
||||||
editor.putBoolean(getString(R.string.send_to_fusion_tables_key), sendFusionTables());
|
editor.putBoolean(getString(R.string.send_to_fusion_tables_key), sendFusionTables());
|
||||||
editor.putBoolean(getString(R.string.send_to_docs_key), sendDocs());
|
editor.putBoolean(getString(R.string.send_to_docs_key), sendDocs());
|
||||||
}
|
}
|
||||||
ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges(editor);
|
ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor);
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true to show the Google Maps option.
|
|
||||||
*/
|
|
||||||
private boolean showMaps() {
|
|
||||||
return sendType == null || sendType == SendType.MAPS;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true to show the Google Fusion Tables option.
|
|
||||||
*/
|
|
||||||
private boolean showFusionTables() {
|
|
||||||
return sendType == null || sendType == SendType.FUSION_TABLES;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true to show the Google Docs option.
|
|
||||||
*/
|
|
||||||
private boolean showDocs() {
|
|
||||||
return sendType == null || sendType == SendType.DOCS;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true to send to Google Maps.
|
* Returns true to send to Google Maps.
|
||||||
*/
|
*/
|
||||||
private boolean sendMaps() {
|
private boolean sendMaps() {
|
||||||
return showMaps() && mapsCheckBox.isChecked();
|
return sendRequest.isShowMaps() && mapsCheckBox.isChecked();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true to send to Google Fusion Tables.
|
* Returns true to send to Google Fusion Tables.
|
||||||
*/
|
*/
|
||||||
private boolean sendFusionTables() {
|
private boolean sendFusionTables() {
|
||||||
return showFusionTables() && fusionTablesCheckBox.isChecked();
|
return sendRequest.isShowFusionTables() && fusionTablesCheckBox.isChecked();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true to send to Google Docs.
|
* Returns true to send to Google Docs.
|
||||||
*/
|
*/
|
||||||
private boolean sendDocs() {
|
private boolean sendDocs() {
|
||||||
return showDocs() && docsCheckBox.isChecked();
|
return sendRequest.isShowDocs() && docsCheckBox.isChecked();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the next activity, {@link AccountChooserActivity}.
|
||||||
|
*/
|
||||||
|
private void startNextActivity() {
|
||||||
|
sendStats();
|
||||||
|
sendRequest.setSendMaps(sendMaps());
|
||||||
|
sendRequest.setSendFusionTables(sendFusionTables());
|
||||||
|
sendRequest.setSendDocs(sendDocs());
|
||||||
|
sendRequest.setNewMap(!existingMapRadioButton.isChecked());
|
||||||
|
Intent intent = new Intent(this, AccountChooserActivity.class)
|
||||||
|
.putExtra(SendRequest.SEND_REQUEST_KEY, sendRequest);
|
||||||
|
startActivity(intent);
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends stats to Google Analytics.
|
||||||
|
*/
|
||||||
|
private void sendStats() {
|
||||||
|
GoogleAnalyticsTracker tracker = GoogleAnalyticsTracker.getInstance();
|
||||||
|
tracker.start(getString(R.string.my_tracks_analytics_id), getApplicationContext());
|
||||||
|
tracker.setProductVersion("android-mytracks", SystemUtils.getMyTracksVersion(this));
|
||||||
|
if (sendRequest.isSendMaps()) {
|
||||||
|
tracker.trackPageView("/send/maps");
|
||||||
|
}
|
||||||
|
if (sendRequest.isSendFusionTables()) {
|
||||||
|
tracker.trackPageView("/send/fusion_tables");
|
||||||
|
}
|
||||||
|
if (sendRequest.isSendDocs()) {
|
||||||
|
tracker.trackPageView("/send/docs");
|
||||||
|
}
|
||||||
|
tracker.dispatch();
|
||||||
|
tracker.stop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
package com.google.android.apps.mytracks.services;
|
package com.google.android.apps.mytracks.services;
|
||||||
|
|
||||||
import com.google.android.apps.mytracks.Constants;
|
import com.google.android.apps.mytracks.Constants;
|
||||||
import com.google.android.apps.mytracks.util.ApiFeatures;
|
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
|
||||||
import com.google.android.maps.mytracks.R;
|
import com.google.android.maps.mytracks.R;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
@@ -174,19 +174,19 @@ public class PreferenceManager implements OnSharedPreferenceChangeListener {
|
|||||||
public void setAutoResumeTrackCurrentRetry(int retryAttempts) {
|
public void setAutoResumeTrackCurrentRetry(int retryAttempts) {
|
||||||
Editor editor = sharedPreferences.edit();
|
Editor editor = sharedPreferences.edit();
|
||||||
editor.putInt(autoResumeTrackCurrentRetryKey, retryAttempts);
|
editor.putInt(autoResumeTrackCurrentRetryKey, retryAttempts);
|
||||||
ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges(editor);
|
ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setRecordingTrack(long id) {
|
public void setRecordingTrack(long id) {
|
||||||
Editor editor = sharedPreferences.edit();
|
Editor editor = sharedPreferences.edit();
|
||||||
editor.putLong(recordingTrackKey, id);
|
editor.putLong(recordingTrackKey, id);
|
||||||
ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges(editor);
|
ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setSelectedTrack(long id) {
|
public void setSelectedTrack(long id) {
|
||||||
Editor editor = sharedPreferences.edit();
|
Editor editor = sharedPreferences.edit();
|
||||||
editor.putLong(selectedTrackKey, id);
|
editor.putLong(selectedTrackKey, id);
|
||||||
ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges(editor);
|
ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void shutdown() {
|
public void shutdown() {
|
||||||
|
|||||||
@@ -36,14 +36,12 @@ import com.google.android.apps.mytracks.services.tasks.SplitTask;
|
|||||||
import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerFactory;
|
import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerFactory;
|
||||||
import com.google.android.apps.mytracks.stats.TripStatistics;
|
import com.google.android.apps.mytracks.stats.TripStatistics;
|
||||||
import com.google.android.apps.mytracks.stats.TripStatisticsBuilder;
|
import com.google.android.apps.mytracks.stats.TripStatisticsBuilder;
|
||||||
import com.google.android.apps.mytracks.util.ApiFeatures;
|
|
||||||
import com.google.android.apps.mytracks.util.ApiLevelAdapter;
|
|
||||||
import com.google.android.apps.mytracks.util.LocationUtils;
|
import com.google.android.apps.mytracks.util.LocationUtils;
|
||||||
import com.google.android.apps.mytracks.util.StringUtils;
|
import com.google.android.apps.mytracks.util.StringUtils;
|
||||||
import com.google.android.maps.mytracks.R;
|
import com.google.android.maps.mytracks.R;
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
|
||||||
import android.app.Notification;
|
import android.app.Notification;
|
||||||
import android.app.NotificationManager;
|
|
||||||
import android.app.PendingIntent;
|
import android.app.PendingIntent;
|
||||||
import android.app.Service;
|
import android.app.Service;
|
||||||
import android.content.ContentValues;
|
import android.content.ContentValues;
|
||||||
@@ -80,7 +78,6 @@ public class TrackRecordingService extends Service {
|
|||||||
|
|
||||||
static final int MAX_AUTO_RESUME_TRACK_RETRY_ATTEMPTS = 3;
|
static final int MAX_AUTO_RESUME_TRACK_RETRY_ATTEMPTS = 3;
|
||||||
|
|
||||||
private NotificationManager notificationManager;
|
|
||||||
private LocationManager locationManager;
|
private LocationManager locationManager;
|
||||||
private WakeLock wakeLock;
|
private WakeLock wakeLock;
|
||||||
|
|
||||||
@@ -243,8 +240,6 @@ public class TrackRecordingService extends Service {
|
|||||||
super.onCreate();
|
super.onCreate();
|
||||||
Log.d(TAG, "TrackRecordingService.onCreate");
|
Log.d(TAG, "TrackRecordingService.onCreate");
|
||||||
providerUtils = MyTracksProviderUtils.Factory.get(this);
|
providerUtils = MyTracksProviderUtils.Factory.get(this);
|
||||||
notificationManager =
|
|
||||||
(NotificationManager) getSystemService(NOTIFICATION_SERVICE);
|
|
||||||
locationManager = (LocationManager) getSystemService(LOCATION_SERVICE);
|
locationManager = (LocationManager) getSystemService(LOCATION_SERVICE);
|
||||||
|
|
||||||
setUpTaskExecutors();
|
setUpTaskExecutors();
|
||||||
@@ -369,7 +364,6 @@ public class TrackRecordingService extends Service {
|
|||||||
|
|
||||||
// Make sure we have no indirect references to this service.
|
// Make sure we have no indirect references to this service.
|
||||||
locationManager = null;
|
locationManager = null;
|
||||||
notificationManager = null;
|
|
||||||
providerUtils = null;
|
providerUtils = null;
|
||||||
binder.detachFromService();
|
binder.detachFromService();
|
||||||
binder = null;
|
binder = null;
|
||||||
@@ -485,8 +479,6 @@ public class TrackRecordingService extends Service {
|
|||||||
* Shows the notification message and icon in the notification bar.
|
* Shows the notification message and icon in the notification bar.
|
||||||
*/
|
*/
|
||||||
private void showNotification() {
|
private void showNotification() {
|
||||||
final ApiLevelAdapter apiLevelAdapter =
|
|
||||||
ApiFeatures.getInstance().getApiAdapter();
|
|
||||||
if (isRecording) {
|
if (isRecording) {
|
||||||
Notification notification = new Notification(
|
Notification notification = new Notification(
|
||||||
R.drawable.arrow_320, null /* tickerText */,
|
R.drawable.arrow_320, null /* tickerText */,
|
||||||
@@ -497,16 +489,24 @@ public class TrackRecordingService extends Service {
|
|||||||
notification.setLatestEventInfo(this, getString(R.string.my_tracks_app_name),
|
notification.setLatestEventInfo(this, getString(R.string.my_tracks_app_name),
|
||||||
getString(R.string.track_record_notification), contentIntent);
|
getString(R.string.track_record_notification), contentIntent);
|
||||||
notification.flags += Notification.FLAG_NO_CLEAR;
|
notification.flags += Notification.FLAG_NO_CLEAR;
|
||||||
apiLevelAdapter.startForeground(this, notificationManager, 1,
|
startForegroundService(notification);
|
||||||
notification);
|
|
||||||
} else {
|
} else {
|
||||||
apiLevelAdapter.stopForeground(this, notificationManager, 1);
|
stopForegroundService();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
protected void startForegroundService(Notification notification) {
|
||||||
|
startForeground(1, notification);
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
protected void stopForegroundService() {
|
||||||
|
stopForeground(true);
|
||||||
|
}
|
||||||
|
|
||||||
private void setUpTaskExecutors() {
|
private void setUpTaskExecutors() {
|
||||||
announcementExecutor = new PeriodicTaskExecutor(
|
announcementExecutor = new PeriodicTaskExecutor(this, new StatusAnnouncerFactory());
|
||||||
this, new StatusAnnouncerFactory(ApiFeatures.getInstance()));
|
|
||||||
splitExecutor = new PeriodicTaskExecutor(this, new SplitTask.Factory());
|
splitExecutor = new PeriodicTaskExecutor(this, new SplitTask.Factory());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+5
-34
@@ -18,7 +18,7 @@ package com.google.android.apps.mytracks.services.sensors;
|
|||||||
|
|
||||||
import com.google.android.apps.mytracks.Constants;
|
import com.google.android.apps.mytracks.Constants;
|
||||||
import com.google.android.apps.mytracks.content.Sensor;
|
import com.google.android.apps.mytracks.content.Sensor;
|
||||||
import com.google.android.apps.mytracks.util.ApiFeatures;
|
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
|
||||||
|
|
||||||
import android.bluetooth.BluetoothAdapter;
|
import android.bluetooth.BluetoothAdapter;
|
||||||
import android.bluetooth.BluetoothDevice;
|
import android.bluetooth.BluetoothDevice;
|
||||||
@@ -31,8 +31,6 @@ import android.util.Log;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.lang.reflect.InvocationTargetException;
|
|
||||||
import java.lang.reflect.Method;
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -44,8 +42,9 @@ import java.util.UUID;
|
|||||||
* @author Sandor Dornbush
|
* @author Sandor Dornbush
|
||||||
*/
|
*/
|
||||||
public class BluetoothConnectionManager {
|
public class BluetoothConnectionManager {
|
||||||
// Unique UUID for this application
|
|
||||||
private static final UUID SPP_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");
|
// Unique Bluetooth UUID for My Tracks
|
||||||
|
public static final UUID SPP_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");
|
||||||
|
|
||||||
private MessageParser parser;
|
private MessageParser parser;
|
||||||
|
|
||||||
@@ -254,41 +253,13 @@ public class BluetoothConnectionManager {
|
|||||||
// Get a BluetoothSocket for a connection with the
|
// Get a BluetoothSocket for a connection with the
|
||||||
// given BluetoothDevice
|
// given BluetoothDevice
|
||||||
try {
|
try {
|
||||||
tmp = getSocket();
|
tmp = ApiAdapterFactory.getApiAdapter().getBluetoothSocket(device);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Log.e(Constants.TAG, "create() failed", e);
|
Log.e(Constants.TAG, "create() failed", e);
|
||||||
}
|
}
|
||||||
socket = tmp;
|
socket = tmp;
|
||||||
}
|
}
|
||||||
|
|
||||||
private BluetoothSocket getSocket() throws IOException {
|
|
||||||
if (ApiFeatures.getInstance().hasBluetoothDeviceCreateInsecureRfcommSocketToServiceRecord()) {
|
|
||||||
try {
|
|
||||||
return device.createInsecureRfcommSocketToServiceRecord(SPP_UUID);
|
|
||||||
} catch (IOException e) {
|
|
||||||
Log.e(Constants.TAG, "Unable to get insecure connect.", e);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
Class<? extends BluetoothDevice> c = device.getClass();
|
|
||||||
Method insecure = c.getMethod("createInsecureRfcommSocket", Integer.class);
|
|
||||||
insecure.setAccessible(true);
|
|
||||||
return (BluetoothSocket) insecure.invoke(device, 1);
|
|
||||||
} catch (SecurityException e) {
|
|
||||||
Log.e(Constants.TAG, "Unable to get insecure connect.", e);
|
|
||||||
} catch (NoSuchMethodException e) {
|
|
||||||
Log.e(Constants.TAG, "Unable to get insecure connect.", e);
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
Log.e(Constants.TAG, "Unable to get insecure connect.", e);
|
|
||||||
} catch (IllegalAccessException e) {
|
|
||||||
Log.e(Constants.TAG, "Unable to get insecure connect.", e);
|
|
||||||
} catch (InvocationTargetException e) {
|
|
||||||
Log.e(Constants.TAG, "Unable to get insecure connect.", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return device.createRfcommSocketToServiceRecord(SPP_UUID);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
Log.d(Constants.TAG, "BEGIN mConnectThread");
|
Log.d(Constants.TAG, "BEGIN mConnectThread");
|
||||||
|
|||||||
+2
-3
@@ -16,7 +16,7 @@
|
|||||||
package com.google.android.apps.mytracks.services.sensors;
|
package com.google.android.apps.mytracks.services.sensors;
|
||||||
|
|
||||||
import com.google.android.apps.mytracks.content.Sensor;
|
import com.google.android.apps.mytracks.content.Sensor;
|
||||||
import com.google.android.apps.mytracks.util.ApiFeatures;
|
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
|
||||||
@@ -62,8 +62,7 @@ public class ZephyrMessageParser implements MessageParser {
|
|||||||
// 0x1A00316550003162 produces erroneous values for Cadence and needs
|
// 0x1A00316550003162 produces erroneous values for Cadence and needs
|
||||||
// a workaround based on the stride counter.
|
// a workaround based on the stride counter.
|
||||||
// Firmware values range from field 3 to 10 (inclusive) of the byte buffer.
|
// Firmware values range from field 3 to 10 (inclusive) of the byte buffer.
|
||||||
byte[] hardwareFirmwareId = ApiFeatures.getInstance().getApiAdapter()
|
byte[] hardwareFirmwareId = ApiAdapterFactory.getApiAdapter().copyByteArray(buffer, 3, 11);
|
||||||
.copyByteArray(buffer, 3, 11);
|
|
||||||
Sensor.SensorData.Builder cadence = Sensor.SensorData.newBuilder();
|
Sensor.SensorData.Builder cadence = Sensor.SensorData.newBuilder();
|
||||||
|
|
||||||
if (Arrays.equals(hardwareFirmwareId, CADENCE_BUG_FW_ID)) {
|
if (Arrays.equals(hardwareFirmwareId, CADENCE_BUG_FW_ID)) {
|
||||||
|
|||||||
+2
-2
@@ -22,7 +22,7 @@ import com.dsi.ant.AntMesg;
|
|||||||
import com.dsi.ant.exception.AntInterfaceException;
|
import com.dsi.ant.exception.AntInterfaceException;
|
||||||
import com.google.android.apps.mytracks.Constants;
|
import com.google.android.apps.mytracks.Constants;
|
||||||
import com.google.android.apps.mytracks.content.Sensor;
|
import com.google.android.apps.mytracks.content.Sensor;
|
||||||
import com.google.android.apps.mytracks.util.ApiFeatures;
|
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
|
||||||
import com.google.android.maps.mytracks.R;
|
import com.google.android.maps.mytracks.R;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
@@ -135,7 +135,7 @@ public class AntDirectSensorManager extends AntSensorManager {
|
|||||||
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
|
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
|
||||||
SharedPreferences.Editor editor = prefs.edit();
|
SharedPreferences.Editor editor = prefs.edit();
|
||||||
editor.putInt(context.getString(R.string.ant_heart_rate_sensor_id_key), deviceNumberHRM);
|
editor.putInt(context.getString(R.string.ant_heart_rate_sensor_id_key), deviceNumberHRM);
|
||||||
ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges(editor);
|
ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleMessageResponse(byte[] rawMessage) {
|
private void handleMessageResponse(byte[] rawMessage) {
|
||||||
|
|||||||
+2
-2
@@ -22,7 +22,7 @@ import com.dsi.ant.AntMesg;
|
|||||||
import com.dsi.ant.exception.AntInterfaceException;
|
import com.dsi.ant.exception.AntInterfaceException;
|
||||||
import com.google.android.apps.mytracks.Constants;
|
import com.google.android.apps.mytracks.Constants;
|
||||||
import com.google.android.apps.mytracks.content.Sensor;
|
import com.google.android.apps.mytracks.content.Sensor;
|
||||||
import com.google.android.apps.mytracks.util.ApiFeatures;
|
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
|
||||||
import com.google.android.apps.mytracks.util.SystemUtils;
|
import com.google.android.apps.mytracks.util.SystemUtils;
|
||||||
import com.google.android.maps.mytracks.R;
|
import com.google.android.maps.mytracks.R;
|
||||||
|
|
||||||
@@ -199,7 +199,7 @@ public class AntSrmBridgeSensorManager extends AntSensorManager {
|
|||||||
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
|
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
|
||||||
SharedPreferences.Editor editor = prefs.edit();
|
SharedPreferences.Editor editor = prefs.edit();
|
||||||
editor.putInt(context.getString(R.string.ant_srm_bridge_sensor_id_key), deviceNumber);
|
editor.putInt(context.getString(R.string.ant_srm_bridge_sensor_id_key), deviceNumber);
|
||||||
ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges(editor);
|
ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleMessageResponse(byte[] rawMessage) {
|
private void handleMessageResponse(byte[] rawMessage) {
|
||||||
|
|||||||
+3
-23
@@ -15,10 +15,9 @@
|
|||||||
*/
|
*/
|
||||||
package com.google.android.apps.mytracks.services.tasks;
|
package com.google.android.apps.mytracks.services.tasks;
|
||||||
|
|
||||||
import com.google.android.apps.mytracks.util.ApiFeatures;
|
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.media.AudioManager;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Factory which wraps construction and setup of text-to-speech announcements in
|
* Factory which wraps construction and setup of text-to-speech announcements in
|
||||||
@@ -28,30 +27,11 @@ import android.media.AudioManager;
|
|||||||
*/
|
*/
|
||||||
public class StatusAnnouncerFactory implements PeriodicTaskFactory {
|
public class StatusAnnouncerFactory implements PeriodicTaskFactory {
|
||||||
|
|
||||||
private final boolean hasTts;
|
public StatusAnnouncerFactory() {
|
||||||
|
|
||||||
public StatusAnnouncerFactory(ApiFeatures apiFeatures) {
|
|
||||||
this.hasTts = apiFeatures.hasTextToSpeech();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PeriodicTask create(Context context) {
|
public PeriodicTask create(Context context) {
|
||||||
if (hasTts) {
|
return ApiAdapterFactory.getApiAdapter().getStatusAnnouncerTask(context);
|
||||||
return ApiFeatures.getInstance().getApiAdapter().getStatusAnnouncerTask(context);
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the appropriate volume stream for controlling announcement
|
|
||||||
* volume.
|
|
||||||
*/
|
|
||||||
public int getVolumeStream() {
|
|
||||||
if (hasTts) {
|
|
||||||
return StatusAnnouncerTask.getVolumeStream();
|
|
||||||
} else {
|
|
||||||
return AudioManager.USE_DEFAULT_STREAM_TYPE;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -331,13 +331,6 @@ public class StatusAnnouncerTask implements PeriodicTask {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the volume stream to use for controlling announcement volume.
|
|
||||||
*/
|
|
||||||
public static int getVolumeStream() {
|
|
||||||
return TextToSpeech.Engine.DEFAULT_STREAM;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets a string to announce the time.
|
* Gets a string to announce the time.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012 Google Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||||
|
* use this file except in compliance with the License. You may obtain a copy of
|
||||||
|
* the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations under
|
||||||
|
* the License.
|
||||||
|
*/
|
||||||
|
package com.google.android.apps.mytracks.util;
|
||||||
|
|
||||||
|
import com.google.android.apps.mytracks.Constants;
|
||||||
|
import com.google.android.apps.mytracks.services.sensors.BluetoothConnectionManager;
|
||||||
|
|
||||||
|
import android.bluetooth.BluetoothDevice;
|
||||||
|
import android.bluetooth.BluetoothSocket;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API level 10 specific implementation of the {@link ApiAdapter}.
|
||||||
|
*
|
||||||
|
* @author Jimmy Shih
|
||||||
|
*/
|
||||||
|
public class Api10Adapter extends Api9Adapter {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BluetoothSocket getBluetoothSocket(BluetoothDevice bluetoothDevice) throws IOException {
|
||||||
|
try {
|
||||||
|
return bluetoothDevice.createInsecureRfcommSocketToServiceRecord(
|
||||||
|
BluetoothConnectionManager.SPP_UUID);
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.d(Constants.TAG, "Unable to create insecure connection", e);
|
||||||
|
}
|
||||||
|
return bluetoothDevice.createRfcommSocketToServiceRecord(BluetoothConnectionManager.SPP_UUID);
|
||||||
|
};
|
||||||
|
}
|
||||||
+28
-43
@@ -15,50 +15,32 @@
|
|||||||
*/
|
*/
|
||||||
package com.google.android.apps.mytracks.util;
|
package com.google.android.apps.mytracks.util;
|
||||||
|
|
||||||
import static com.google.android.apps.mytracks.Constants.TAG;
|
import com.google.android.apps.mytracks.Constants;
|
||||||
|
|
||||||
import com.google.android.apps.mytracks.io.backup.BackupPreferencesListener;
|
import com.google.android.apps.mytracks.io.backup.BackupPreferencesListener;
|
||||||
|
import com.google.android.apps.mytracks.services.sensors.BluetoothConnectionManager;
|
||||||
import com.google.android.apps.mytracks.services.tasks.PeriodicTask;
|
import com.google.android.apps.mytracks.services.tasks.PeriodicTask;
|
||||||
import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerTask;
|
import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerTask;
|
||||||
import com.google.api.client.http.HttpTransport;
|
import com.google.api.client.http.HttpTransport;
|
||||||
import com.google.api.client.http.apache.ApacheHttpTransport;
|
import com.google.api.client.http.apache.ApacheHttpTransport;
|
||||||
|
|
||||||
import android.app.Notification;
|
import android.bluetooth.BluetoothDevice;
|
||||||
import android.app.NotificationManager;
|
import android.bluetooth.BluetoothSocket;
|
||||||
import android.app.Service;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.content.SharedPreferences.Editor;
|
import android.content.SharedPreferences.Editor;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.lang.reflect.InvocationTargetException;
|
import java.lang.reflect.InvocationTargetException;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* API level 3 specific implementation of the {@link ApiLevelAdapter}.
|
* API level 7 specific implementation of the {@link ApiAdapter}.
|
||||||
*
|
*
|
||||||
* @author Bartlomiej Niechwiej
|
* @author Bartlomiej Niechwiej
|
||||||
*/
|
*/
|
||||||
public class ApiLevel3Adapter implements ApiLevelAdapter {
|
public class Api7Adapter implements ApiAdapter {
|
||||||
|
|
||||||
@Override
|
|
||||||
public void startForeground(Service service,
|
|
||||||
NotificationManager notificationManager, int id,
|
|
||||||
Notification notification) {
|
|
||||||
setServiceForeground(service, true);
|
|
||||||
|
|
||||||
notificationManager.notify(id, notification);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void stopForeground(Service service,
|
|
||||||
NotificationManager notificationManager, int id) {
|
|
||||||
setServiceForeground(service, false);
|
|
||||||
if (id != -1) {
|
|
||||||
notificationManager.cancel(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PeriodicTask getStatusAnnouncerTask(Context context) {
|
public PeriodicTask getStatusAnnouncerTask(Context context) {
|
||||||
return new StatusAnnouncerTask(context);
|
return new StatusAnnouncerTask(context);
|
||||||
@@ -74,24 +56,6 @@ public class ApiLevel3Adapter implements ApiLevelAdapter {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setServiceForeground(Service service, boolean foreground) {
|
|
||||||
// setForeground has been completely removed in API level 11, so we use reflection.
|
|
||||||
try {
|
|
||||||
Method setForegroundMethod = Service.class.getMethod("setForeground", boolean.class);
|
|
||||||
setForegroundMethod.invoke(service, foreground);
|
|
||||||
} catch (SecurityException e) {
|
|
||||||
Log.e(TAG, "Unable to set service foreground state", e);
|
|
||||||
} catch (NoSuchMethodException e) {
|
|
||||||
Log.e(TAG, "Unable to set service foreground state", e);
|
|
||||||
} catch (IllegalArgumentException e) {
|
|
||||||
Log.e(TAG, "Unable to set service foreground state", e);
|
|
||||||
} catch (IllegalAccessException e) {
|
|
||||||
Log.e(TAG, "Unable to set service foreground state", e);
|
|
||||||
} catch (InvocationTargetException e) {
|
|
||||||
Log.e(TAG, "Unable to set service foreground state", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void applyPreferenceChanges(Editor editor) {
|
public void applyPreferenceChanges(Editor editor) {
|
||||||
editor.commit();
|
editor.commit();
|
||||||
@@ -114,4 +78,25 @@ public class ApiLevel3Adapter implements ApiLevelAdapter {
|
|||||||
public HttpTransport getHttpTransport() {
|
public HttpTransport getHttpTransport() {
|
||||||
return new ApacheHttpTransport();
|
return new ApacheHttpTransport();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BluetoothSocket getBluetoothSocket(BluetoothDevice bluetoothDevice) throws IOException {
|
||||||
|
try {
|
||||||
|
Class<? extends BluetoothDevice> c = bluetoothDevice.getClass();
|
||||||
|
Method insecure = c.getMethod("createInsecureRfcommSocket", Integer.class);
|
||||||
|
insecure.setAccessible(true);
|
||||||
|
return (BluetoothSocket) insecure.invoke(bluetoothDevice, 1);
|
||||||
|
} catch (SecurityException e) {
|
||||||
|
Log.d(Constants.TAG, "Unable to create insecure connection", e);
|
||||||
|
} catch (NoSuchMethodException e) {
|
||||||
|
Log.d(Constants.TAG, "Unable to create insecure connection", e);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
Log.d(Constants.TAG, "Unable to create insecure connection", e);
|
||||||
|
} catch (IllegalAccessException e) {
|
||||||
|
Log.d(Constants.TAG, "Unable to create insecure connection", e);
|
||||||
|
} catch (InvocationTargetException e) {
|
||||||
|
Log.d(Constants.TAG, "Unable to create insecure connection", e);
|
||||||
|
}
|
||||||
|
return bluetoothDevice.createRfcommSocketToServiceRecord(BluetoothConnectionManager.SPP_UUID);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+17
-2
@@ -1,3 +1,18 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012 Google Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||||
|
* use this file except in compliance with the License. You may obtain a copy of
|
||||||
|
* the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations under
|
||||||
|
* the License.
|
||||||
|
*/
|
||||||
package com.google.android.apps.mytracks.util;
|
package com.google.android.apps.mytracks.util;
|
||||||
|
|
||||||
import com.google.android.apps.mytracks.io.backup.BackupPreferencesListener;
|
import com.google.android.apps.mytracks.io.backup.BackupPreferencesListener;
|
||||||
@@ -8,11 +23,11 @@ import com.google.android.apps.mytracks.services.tasks.PeriodicTask;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* API level 8 specific implementation of the {@link ApiLevelAdapter}.
|
* API level 8 specific implementation of the {@link ApiAdapter}.
|
||||||
*
|
*
|
||||||
* @author Jimmy Shih
|
* @author Jimmy Shih
|
||||||
*/
|
*/
|
||||||
public class ApiLevel8Adapter extends ApiLevel5Adapter {
|
public class Api8Adapter extends Api7Adapter {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PeriodicTask getStatusAnnouncerTask(Context context) {
|
public PeriodicTask getStatusAnnouncerTask(Context context) {
|
||||||
+17
-2
@@ -1,3 +1,18 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012 Google Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||||
|
* use this file except in compliance with the License. You may obtain a copy of
|
||||||
|
* the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations under
|
||||||
|
* the License.
|
||||||
|
*/
|
||||||
package com.google.android.apps.mytracks.util;
|
package com.google.android.apps.mytracks.util;
|
||||||
|
|
||||||
import com.google.android.apps.mytracks.Constants;
|
import com.google.android.apps.mytracks.Constants;
|
||||||
@@ -11,11 +26,11 @@ import android.util.Log;
|
|||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* API level 9 specific implementation of the {@link ApiLevelAdapter}.
|
* API level 9 specific implementation of the {@link ApiAdapter}.
|
||||||
*
|
*
|
||||||
* @author Rodrigo Damazio
|
* @author Rodrigo Damazio
|
||||||
*/
|
*/
|
||||||
public class ApiLevel9Adapter extends ApiLevel8Adapter {
|
public class Api9Adapter extends Api8Adapter {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void applyPreferenceChanges(Editor editor) {
|
public void applyPreferenceChanges(Editor editor) {
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
/*
|
||||||
|
* 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.util;
|
||||||
|
|
||||||
|
import com.google.android.apps.mytracks.io.backup.BackupPreferencesListener;
|
||||||
|
import com.google.android.apps.mytracks.services.tasks.PeriodicTask;
|
||||||
|
import com.google.api.client.http.HttpTransport;
|
||||||
|
|
||||||
|
import android.bluetooth.BluetoothDevice;
|
||||||
|
import android.bluetooth.BluetoothSocket;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A set of methods that may be implemented differently depending on the Android
|
||||||
|
* API level.
|
||||||
|
*
|
||||||
|
* @author Bartlomiej Niechwiej
|
||||||
|
*/
|
||||||
|
public interface ApiAdapter {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a status announcer task.
|
||||||
|
* <p>
|
||||||
|
* Due to changes in API level 8.
|
||||||
|
*
|
||||||
|
* @param context the context
|
||||||
|
*/
|
||||||
|
public PeriodicTask getStatusAnnouncerTask(Context context);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a {@link BackupPreferencesListener}.
|
||||||
|
* <p>
|
||||||
|
* Due to changes in API level 8.
|
||||||
|
*
|
||||||
|
* @param context the context
|
||||||
|
*/
|
||||||
|
public BackupPreferencesListener getBackupPreferencesListener(Context context);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies all the changes done to a given preferences editor. Changes may or
|
||||||
|
* may not be applied immediately.
|
||||||
|
* <p>
|
||||||
|
* Due to changes in API level 9.
|
||||||
|
*
|
||||||
|
* @param editor the editor
|
||||||
|
*/
|
||||||
|
public void applyPreferenceChanges(SharedPreferences.Editor editor);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enables strict mode where supported, only if this is a development build.
|
||||||
|
* <p>
|
||||||
|
* Due to changes in API level 9.
|
||||||
|
*/
|
||||||
|
public void enableStrictMode();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copies elements from an input byte array into a new byte array, from
|
||||||
|
* indexes start (inclusive) to end (exclusive). The end index must be less
|
||||||
|
* than or equal to the input length.
|
||||||
|
* <p>
|
||||||
|
* Due to changes in API level 9.
|
||||||
|
*
|
||||||
|
* @param input the input byte array
|
||||||
|
* @param start the start index
|
||||||
|
* @param end the end index
|
||||||
|
* @return a new array containing elements from the input byte array.
|
||||||
|
*/
|
||||||
|
public byte[] copyByteArray(byte[] input, int start, int end);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a {@link HttpTransport}.
|
||||||
|
* <p>
|
||||||
|
* Due to changes in API level 9.
|
||||||
|
*/
|
||||||
|
public HttpTransport getHttpTransport();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a {@link BluetoothSocket}.
|
||||||
|
* <p>
|
||||||
|
* Due to changes in API level 10.
|
||||||
|
*
|
||||||
|
* @param bluetoothDevice
|
||||||
|
*/
|
||||||
|
public BluetoothSocket getBluetoothSocket(BluetoothDevice bluetoothDevice) throws IOException;
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2010 Google Inc.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||||
|
* use this file except in compliance with the License. You may obtain a copy of
|
||||||
|
* the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
* License for the specific language governing permissions and limitations under
|
||||||
|
* the License.
|
||||||
|
*/
|
||||||
|
package com.google.android.apps.mytracks.util;
|
||||||
|
|
||||||
|
import android.os.Build;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A factory to get the {@link ApiAdapter} for the current device.
|
||||||
|
*
|
||||||
|
* @author Rodrigo Damazio
|
||||||
|
*/
|
||||||
|
public class ApiAdapterFactory {
|
||||||
|
|
||||||
|
private static ApiAdapter apiAdapter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the {@link ApiAdapter} for the current device.
|
||||||
|
*/
|
||||||
|
public static ApiAdapter getApiAdapter() {
|
||||||
|
if (apiAdapter == null) {
|
||||||
|
if (Build.VERSION.SDK_INT >= 10) {
|
||||||
|
apiAdapter = new Api10Adapter();
|
||||||
|
return apiAdapter;
|
||||||
|
} else if (Build.VERSION.SDK_INT >= 9) {
|
||||||
|
apiAdapter = new Api9Adapter();
|
||||||
|
return apiAdapter;
|
||||||
|
} else if (Build.VERSION.SDK_INT >= 8) {
|
||||||
|
apiAdapter = new Api8Adapter();
|
||||||
|
return apiAdapter;
|
||||||
|
} else {
|
||||||
|
apiAdapter = new Api7Adapter();
|
||||||
|
return apiAdapter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return apiAdapter;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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.util;
|
|
||||||
|
|
||||||
import com.google.android.apps.mytracks.Constants;
|
|
||||||
|
|
||||||
import android.os.Build;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility class for determining if newer-API features are available on the
|
|
||||||
* current device.
|
|
||||||
*
|
|
||||||
* @author Rodrigo Damazio
|
|
||||||
*/
|
|
||||||
public class ApiFeatures {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The API level of the Android version we are being run under.
|
|
||||||
*/
|
|
||||||
private static final int ANDROID_API_LEVEL = Integer.parseInt(
|
|
||||||
Build.VERSION.SDK);
|
|
||||||
|
|
||||||
private static ApiFeatures instance;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The API level adapter for the Android version we are being run under.
|
|
||||||
*/
|
|
||||||
private ApiLevelAdapter apiLevelAdapter;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the singleton instance of this class.
|
|
||||||
*/
|
|
||||||
public static ApiFeatures getInstance() {
|
|
||||||
if (instance == null) {
|
|
||||||
instance = new ApiFeatures();
|
|
||||||
}
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Injects a specific singleton instance, to be used for unit tests.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("hiding")
|
|
||||||
public static void injectInstance(ApiFeatures instance) {
|
|
||||||
ApiFeatures.instance = instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Allow subclasses for mocking, but no direct instantiation.
|
|
||||||
*/
|
|
||||||
protected ApiFeatures() {
|
|
||||||
// It is safe to import unsupported classes as long as we only actually
|
|
||||||
// load the class when supported.
|
|
||||||
if (getApiLevel() >= 9) {
|
|
||||||
apiLevelAdapter = new ApiLevel9Adapter();
|
|
||||||
} else if (getApiLevel() >= 8) {
|
|
||||||
apiLevelAdapter = new ApiLevel8Adapter();
|
|
||||||
} else if (getApiLevel() >= 5) {
|
|
||||||
apiLevelAdapter = new ApiLevel5Adapter();
|
|
||||||
} else {
|
|
||||||
apiLevelAdapter = new ApiLevel3Adapter();
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.i(Constants.TAG, "Using API level adapter " + apiLevelAdapter.getClass());
|
|
||||||
}
|
|
||||||
|
|
||||||
public ApiLevelAdapter getApiAdapter() {
|
|
||||||
return apiLevelAdapter;
|
|
||||||
}
|
|
||||||
|
|
||||||
// API Level 4 Changes
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns whether text-to-speech is available.
|
|
||||||
*/
|
|
||||||
public boolean hasTextToSpeech() {
|
|
||||||
return getApiLevel() >= 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
// API Level 5 Changes
|
|
||||||
|
|
||||||
/**
|
|
||||||
* There's a bug (#1587) in Cupcake and Donut which prevents you from
|
|
||||||
* using a SQLiteQueryBuilder twice. That is, if you call buildQuery
|
|
||||||
* on a given instance (to log the statement for debugging), and then
|
|
||||||
* call query on the same instance to make it actually do the query,
|
|
||||||
* it'll regenerate the query for the second call, and will screw it
|
|
||||||
* up. Specifically, it'll add extra parens which don't belong.
|
|
||||||
*/
|
|
||||||
public boolean canReuseSQLiteQueryBuilder() {
|
|
||||||
return getApiLevel() >= 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
// API Level 10 changes
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if BluetoothDevice.createInsecureRfcommSocketToServiceRecord
|
|
||||||
* is available.
|
|
||||||
*/
|
|
||||||
public boolean hasBluetoothDeviceCreateInsecureRfcommSocketToServiceRecord() {
|
|
||||||
return getApiLevel() >= 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Visible for testing.
|
|
||||||
protected int getApiLevel() {
|
|
||||||
return ANDROID_API_LEVEL;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user