summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJames Lemieux <jplemieux@google.com>2015-09-18 16:45:45 -0700
committerJames Lemieux <jplemieux@google.com>2015-09-29 11:17:27 -0700
commit24a54fc16fdf95ee3f76ab99978c3401473dc516 (patch)
tree4d677ebad703dcdb4c92b2d5d1849dff07bbfbb1
parent548c0d1813d06e69833b0835d0103b13c0dfb2cc (diff)
downloadandroid_packages_apps_DeskClock-24a54fc16fdf95ee3f76ab99978c3401473dc516.tar.gz
android_packages_apps_DeskClock-24a54fc16fdf95ee3f76ab99978c3401473dc516.tar.bz2
android_packages_apps_DeskClock-24a54fc16fdf95ee3f76ab99978c3401473dc516.zip
Refactor Stopwatch to use new unified DataModel
This refactoring introduces Stopwatch and Lap domain objects. Instances of these domain objects are fetched via a DataModel class that is the single point of reference for all stopwatch data. This allows the data to be shared across the entire application. In particular, the stopwatch notification and StopwatchFragment all draw upon the same model to determine what the state of the stopwatch is and what laps have been recorded. Change-Id: I6bda1fda73568cb312b167f1f6b10bd1abc99d62
-rw-r--r--AndroidManifest.xml11
-rw-r--r--res/drawable-v21/ic_add_24dp.xml2
-rw-r--r--res/drawable-v21/ic_pause_24dp.xml2
-rw-r--r--res/drawable-v21/ic_reset_24dp.xml2
-rw-r--r--res/drawable-v21/ic_snooze_24dp.xml2
-rw-r--r--res/drawable-v21/ic_start_24dp.xml2
-rw-r--r--res/drawable-v21/ic_stop_24dp.xml2
-rw-r--r--res/drawable-v21/ic_sw_lap_24dp.xml (renamed from res/drawable-v21/ic_lap_24dp.xml)0
-rw-r--r--res/drawable-v21/ic_sw_stop_24dp.xml4
-rw-r--r--res/drawable/ic_sw_lap_24dp.xml (renamed from res/drawable/ic_lap_24dp.xml)0
-rw-r--r--res/drawable/ic_sw_stop_24dp.xml3
-rw-r--r--res/layout-land/stopwatch_fragment.xml57
-rw-r--r--res/layout/alarm_empty_view.xml2
-rw-r--r--res/layout/lap_view.xml1
-rw-r--r--res/layout/stopwatch_fragment.xml50
-rw-r--r--res/layout/stopwatch_notif_collapsed.xml4
-rw-r--r--res/values-v21/dimens.xml4
-rw-r--r--res/values/dimens.xml5
-rw-r--r--res/values/donottranslate_events.xml2
-rw-r--r--res/values/strings.xml57
-rw-r--r--src/com/android/deskclock/AlarmInitReceiver.java9
-rw-r--r--src/com/android/deskclock/CircleTimerView.java61
-rw-r--r--src/com/android/deskclock/ClockFragment.java16
-rw-r--r--src/com/android/deskclock/DeskClock.java21
-rw-r--r--src/com/android/deskclock/HandleDeskClockApiCalls.java355
-rw-r--r--src/com/android/deskclock/Utils.java26
-rw-r--r--src/com/android/deskclock/data/City.java2
-rw-r--r--src/com/android/deskclock/data/CityDAO.java4
-rw-r--r--src/com/android/deskclock/data/CityModel.java3
-rw-r--r--src/com/android/deskclock/data/DataModel.java121
-rw-r--r--src/com/android/deskclock/data/Lap.java42
-rw-r--r--src/com/android/deskclock/data/NotificationModel.java46
-rw-r--r--src/com/android/deskclock/data/Stopwatch.java107
-rw-r--r--src/com/android/deskclock/data/StopwatchDAO.java157
-rw-r--r--src/com/android/deskclock/data/StopwatchModel.java342
-rw-r--r--src/com/android/deskclock/stopwatch/LapsAdapter.java321
-rw-r--r--src/com/android/deskclock/stopwatch/StopwatchFragment.java1133
-rw-r--r--src/com/android/deskclock/stopwatch/StopwatchService.java481
-rw-r--r--src/com/android/deskclock/stopwatch/StopwatchTimer.java188
-rw-r--r--src/com/android/deskclock/stopwatch/Stopwatches.java160
-rw-r--r--src/com/android/deskclock/worldclock/CitySelectionActivity.java10
41 files changed, 2033 insertions, 1784 deletions
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 497c9a95f..76acc6dad 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -214,7 +214,7 @@
<category android:name="android.intent.category.VOICE" />
</intent-filter>
<intent-filter>
- <action android:name="com.android.deskclock.action.STOP_STOPWATCH" />
+ <action android:name="com.android.deskclock.action.PAUSE_STOPWATCH" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.VOICE" />
</intent-filter>
@@ -361,11 +361,10 @@
android:exported="false"
android:description="@string/stopwatch_service_desc">
<intent-filter>
- <action android:name="start_stopwatch" />
- <action android:name="lap_stopwatch" />
- <action android:name="stop_stopwatch" />
- <action android:name="reset_stopwatch" />
- <action android:name="share_stopwatch" />
+ <action android:name="com.android.deskclock.action.START_STOPWATCH" />
+ <action android:name="com.android.deskclock.action.PAUSE_STOPWATCH" />
+ <action android:name="com.android.deskclock.action.LAP_STOPWATCH" />
+ <action android:name="com.android.deskclock.action.RESET_STOPWATCH" />
</intent-filter>
</service>
</application>
diff --git a/res/drawable-v21/ic_add_24dp.xml b/res/drawable-v21/ic_add_24dp.xml
index 85532d013..61ef28501 100644
--- a/res/drawable-v21/ic_add_24dp.xml
+++ b/res/drawable-v21/ic_add_24dp.xml
@@ -16,4 +16,4 @@
<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
android:src="@drawable/ic_add_white_24dp"
- android:tint="@color/black" />
+ android:tint="@color/black_54p" />
diff --git a/res/drawable-v21/ic_pause_24dp.xml b/res/drawable-v21/ic_pause_24dp.xml
index 899672f7e..92c5e93b1 100644
--- a/res/drawable-v21/ic_pause_24dp.xml
+++ b/res/drawable-v21/ic_pause_24dp.xml
@@ -15,4 +15,4 @@
-->
<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
android:src="@drawable/ic_pause_white_24dp"
- android:tint="@color/black" />
+ android:tint="@color/black_54p" />
diff --git a/res/drawable-v21/ic_reset_24dp.xml b/res/drawable-v21/ic_reset_24dp.xml
index a754bd849..b88c04fa4 100644
--- a/res/drawable-v21/ic_reset_24dp.xml
+++ b/res/drawable-v21/ic_reset_24dp.xml
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
android:src="@drawable/ic_reset_white_24dp"
- android:tint="@color/black" />
+ android:tint="@color/black_54p" />
diff --git a/res/drawable-v21/ic_snooze_24dp.xml b/res/drawable-v21/ic_snooze_24dp.xml
index 29da44dd1..1968c6d7d 100644
--- a/res/drawable-v21/ic_snooze_24dp.xml
+++ b/res/drawable-v21/ic_snooze_24dp.xml
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
android:src="@drawable/ic_snooze_white_24dp"
- android:tint="@color/black" /> \ No newline at end of file
+ android:tint="@color/black_54p" /> \ No newline at end of file
diff --git a/res/drawable-v21/ic_start_24dp.xml b/res/drawable-v21/ic_start_24dp.xml
index 655e66d70..ebf308368 100644
--- a/res/drawable-v21/ic_start_24dp.xml
+++ b/res/drawable-v21/ic_start_24dp.xml
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
android:src="@drawable/ic_start_white_24dp"
- android:tint="@color/black" />
+ android:tint="@color/black_54p" />
diff --git a/res/drawable-v21/ic_stop_24dp.xml b/res/drawable-v21/ic_stop_24dp.xml
index 0d4c891b0..e3e3431f8 100644
--- a/res/drawable-v21/ic_stop_24dp.xml
+++ b/res/drawable-v21/ic_stop_24dp.xml
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
android:src="@drawable/ic_stop_white_24dp"
- android:tint="@color/black" />
+ android:tint="@color/black_54p" />
diff --git a/res/drawable-v21/ic_lap_24dp.xml b/res/drawable-v21/ic_sw_lap_24dp.xml
index 25b0e5095..25b0e5095 100644
--- a/res/drawable-v21/ic_lap_24dp.xml
+++ b/res/drawable-v21/ic_sw_lap_24dp.xml
diff --git a/res/drawable-v21/ic_sw_stop_24dp.xml b/res/drawable-v21/ic_sw_stop_24dp.xml
deleted file mode 100644
index e3e3431f8..000000000
--- a/res/drawable-v21/ic_sw_stop_24dp.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
- android:src="@drawable/ic_stop_white_24dp"
- android:tint="@color/black_54p" />
diff --git a/res/drawable/ic_lap_24dp.xml b/res/drawable/ic_sw_lap_24dp.xml
index 84ada73c6..84ada73c6 100644
--- a/res/drawable/ic_lap_24dp.xml
+++ b/res/drawable/ic_sw_lap_24dp.xml
diff --git a/res/drawable/ic_sw_stop_24dp.xml b/res/drawable/ic_sw_stop_24dp.xml
deleted file mode 100644
index 02a2cf185..000000000
--- a/res/drawable/ic_sw_stop_24dp.xml
+++ /dev/null
@@ -1,3 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
- android:src="@drawable/ic_stop_white_24dp" />
diff --git a/res/layout-land/stopwatch_fragment.xml b/res/layout-land/stopwatch_fragment.xml
index 5c862937d..f47202a75 100644
--- a/res/layout-land/stopwatch_fragment.xml
+++ b/res/layout-land/stopwatch_fragment.xml
@@ -13,62 +13,47 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
- android:layout_height="match_parent"
+ android:layout_height="wrap_content"
+ android:baselineAligned="false"
android:orientation="horizontal"
- android:baselineAligned="false">
+ android:paddingBottom="@dimen/fab_height">
- <Space
- android:id="@+id/start_space"
- android:visibility="gone"
- android:layout_weight="0.5"
- android:layout_width="0dip"
- android:layout_height="match_parent"/>
-
- <LinearLayout
- android:layout_width="wrap_content"
+ <!-- This FrameLayout reserves half the screen for the stopwatch. -->
+ <FrameLayout
+ android:layout_width="0dp"
android:layout_height="match_parent"
- android:layout_marginBottom="@dimen/stopwatch_circle_margin_bottom"
- android:gravity="center"
- android:orientation="vertical">
+ android:layout_weight="1">
- <com.android.deskclock.CircleButtonsLayout
- android:id="@+id/stopwatch_circle"
+ <!-- This FrameLayout draws the stopwatch centered within its half of the screen. -->
+ <FrameLayout
android:layout_width="@dimen/circle_size"
android:layout_height="@dimen/circle_size"
- android:layout_marginStart="32dip"
- android:layout_marginEnd="32dip"
- android:gravity="center">
+ android:layout_gravity="center"
+ android:gravity="center"
+ android:padding="8dp">
<com.android.deskclock.timer.CountingTimerView
android:id="@+id/stopwatch_time_text"
android:layout_width="match_parent"
android:layout_height="match_parent" />
- <com.android.deskclock.CircleTimerView
+ <com.android.deskclock.stopwatch.StopwatchTimer
android:id="@+id/stopwatch_time"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@null" />
+ </FrameLayout>
- </com.android.deskclock.CircleButtonsLayout>
-
- </LinearLayout>
+ </FrameLayout>
<ListView
android:id="@+id/laps_list"
- android:layout_width="0dip"
+ android:layout_width="0dp"
android:layout_height="wrap_content"
- android:layout_weight="2"
- android:layout_marginBottom="8dip"
- android:layout_gravity="center" />
-
- <Space
- android:id="@+id/end_space"
- android:visibility="gone"
- android:layout_weight="0.5"
- android:layout_width="0dip"
- android:layout_height="match_parent"/>
+ android:layout_gravity="center"
+ android:layout_weight="1" />
-</LinearLayout>
+</LinearLayout> \ No newline at end of file
diff --git a/res/layout/alarm_empty_view.xml b/res/layout/alarm_empty_view.xml
index 8f05d0ce5..fc03e830d 100644
--- a/res/layout/alarm_empty_view.xml
+++ b/res/layout/alarm_empty_view.xml
@@ -22,7 +22,7 @@
android:layout_gravity="center"
android:contentDescription="@string/no_alarms"
android:drawableTop="@drawable/ic_noalarms"
- android:paddingBottom="88dp"
+ android:paddingBottom="@dimen/fab_height"
android:text="@string/no_alarms"
android:gravity="center_horizontal"
android:textColor="#4cffffff"
diff --git a/res/layout/lap_view.xml b/res/layout/lap_view.xml
index 5130c115e..a79a8f4a1 100644
--- a/res/layout/lap_view.xml
+++ b/res/layout/lap_view.xml
@@ -16,6 +16,7 @@
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/lap_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layoutDirection="ltr"
diff --git a/res/layout/stopwatch_fragment.xml b/res/layout/stopwatch_fragment.xml
index 1dca235d3..2ad694595 100644
--- a/res/layout/stopwatch_fragment.xml
+++ b/res/layout/stopwatch_fragment.xml
@@ -13,58 +13,38 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:orientation="vertical">
+ android:baselineAligned="false"
+ android:gravity="center"
+ android:orientation="vertical"
+ android:paddingBottom="@dimen/fab_height">
- <Space
- android:id="@+id/start_space"
- android:visibility="gone"
- android:layout_weight="1"
- android:layout_width="match_parent"
- android:layout_height="0dip"/>
-
- <com.android.deskclock.CircleButtonsLayout
- android:id="@+id/stopwatch_circle"
+ <FrameLayout
android:layout_width="@dimen/circle_size"
android:layout_height="@dimen/circle_size"
android:layout_gravity="center"
- android:layout_marginLeft="@dimen/circle_margin"
- android:layout_marginStart="@dimen/circle_margin"
- android:layout_marginRight="@dimen/circle_margin"
- android:layout_marginEnd="@dimen/circle_margin">
+ android:padding="8dp">
<com.android.deskclock.timer.CountingTimerView
android:id="@+id/stopwatch_time_text"
android:layout_width="match_parent"
android:layout_height="match_parent" />
- <com.android.deskclock.CircleTimerView
+
+ <com.android.deskclock.stopwatch.StopwatchTimer
android:id="@+id/stopwatch_time"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@null" />
-
- </com.android.deskclock.CircleButtonsLayout>
-
- <Space
- android:id="@+id/end_space"
- android:visibility="gone"
- android:layout_weight="1"
- android:layout_width="match_parent"
- android:layout_height="0dip"/>
+ </FrameLayout>
<ListView
android:id="@+id/laps_list"
- android:layout_weight="2"
- android:layout_width="match_parent"
- android:layout_height="0dip" />
-
- <Space
- android:id="@+id/bottom_space"
- android:visibility="gone"
android:layout_width="match_parent"
- android:layout_height="@dimen/footer_button_size"
- android:layout_margin="@dimen/footer_button_layout_margin" />
+ android:layout_height="0dp"
+ android:layout_gravity="center"
+ android:layout_weight="1" />
-</LinearLayout>
+</LinearLayout> \ No newline at end of file
diff --git a/res/layout/stopwatch_notif_collapsed.xml b/res/layout/stopwatch_notif_collapsed.xml
index dade139f2..a8ec1934e 100644
--- a/res/layout/stopwatch_notif_collapsed.xml
+++ b/res/layout/stopwatch_notif_collapsed.xml
@@ -39,7 +39,7 @@
android:id="@+id/line1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:layout_marginStart="8dp"
+ android:layout_marginStart="@dimen/notification_content_margin_start"
android:orientation="horizontal">
<Chronometer
@@ -55,7 +55,7 @@
android:id="@+id/swn_collapsed_laps"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:layout_marginStart="8dp"
+ android:layout_marginStart="@dimen/notification_content_margin_start"
android:ellipsize="marquee"
android:fadingEdge="horizontal"
android:visibility="gone"
diff --git a/res/values-v21/dimens.xml b/res/values-v21/dimens.xml
index f3f5427c5..f320981e0 100644
--- a/res/values-v21/dimens.xml
+++ b/res/values-v21/dimens.xml
@@ -18,4 +18,8 @@
<resources>
<!-- Floating action button style -->
<dimen name="fab_margin">16dp</dimen>
+
+ <!-- Size of margin between icon and text / title text. -->
+ <dimen name="notification_content_margin_start">0dp</dimen>
+
</resources>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 47a1b35dd..8ae2101a2 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -133,6 +133,8 @@
<dimen name="notification_text_size">14dp</dimen>
<!-- Size of notification text titles (see TextAppearance.StatusBar.EventContent.Title) -->
<dimen name="notification_title_text_size">18dp</dimen>
+ <!-- Size of margin between icon and text / title text. -->
+ <dimen name="notification_content_margin_start">8dp</dimen>
<!-- Width of the clock, for use with alarm buttons. -->
<dimen name="alarm_alert_display_width">304dip</dimen>
@@ -203,4 +205,7 @@
<!-- Floating action button style -->
<dimen name="fab_margin">0dp</dimen>
<dimen name="fab_elevation">8dp</dimen>
+
+ <!-- Height of the bar containing the floating action button -->
+ <dimen name="fab_height">88dp</dimen>
</resources>
diff --git a/res/values/donottranslate_events.xml b/res/values/donottranslate_events.xml
index 1b2e322c0..dcef70a85 100644
--- a/res/values/donottranslate_events.xml
+++ b/res/values/donottranslate_events.xml
@@ -25,10 +25,12 @@
<string name="action_hide">Hide</string>
<string name="action_snooze">Snooze</string>
<string name="action_create">Create</string>
+ <string name="action_add">Add</string>
<string name="action_delete">Delete</string>
<string name="action_update">Update</string>
<string name="action_start">Start</string>
<string name="action_stop">Stop</string>
+ <string name="action_pause">Pause</string>
<string name="action_fire">Fire</string>
<string name="action_reset">Reset</string>
<string name="action_show">Show</string>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 9bdb10923..f7a21abb8 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -439,7 +439,7 @@
<string name="selected_cities_label">Selected Cities</string>
<!-- Stopwatch and Timer shared strings -->
- <!-- Describes the purpose of the button to resume running a stopwatch or timer. [CHAR LIMIT=15] -->
+ <!-- Describes the purpose of the button to resume running timer. [CHAR LIMIT=15] -->
<string name="sw_resume_button">Resume</string>
<!--
Describes the purpose of the button
@@ -453,8 +453,8 @@
<!-- Describes the purpose of the button to begin running a stopwatch -->
<string name="sw_start_button">Start</string>
<!-- Describes the purpose of the button to pause a stopwatch. -->
- <string name="sw_stop_button">Stop</string>
- <!-- Describes the purpose of the button to record current the stopwatch value into the4 collection of lap times. -->
+ <string name="sw_pause_button">Pause</string>
+ <!-- Describes the purpose of the button to record current the stopwatch value into the collection of lap times. -->
<string name="sw_lap_button">Lap</string>
<!-- Describes the purpose of the button to share the stopwatch value.
Also used as title for chooser when sharing stopwatch results. -->
@@ -480,49 +480,6 @@
<string name="sw_share_laps">Lap times:</string>
<!-- Label to enumerate the number of laps in the notification the user has counted -->
<string name="sw_notification_lap_number">Lap <xliff:g id="number">%d</xliff:g></string>
- <!-- Formatted stopwatch time -->
- <string-array name="stopwatch_format_set" translatable="false">
- <item> <!-- <10 minutes - example "1 02.03" -->
- <xliff:g id="M">%2$d</xliff:g> &#x200E;<xliff:g id="S">%3$02d</xliff:g><xliff:g id="decimal_separator">%5$c</xliff:g><xliff:g id="D">%4$02d</xliff:g>
- </item>
- <item> <!-- <1 hour - example "12 03.04" -->
- <xliff:g id="M">%2$02d</xliff:g> &#x200E;<xliff:g id="S">%3$02d</xliff:g><xliff:g id="decimal_separator">%5$c</xliff:g><xliff:g id="D">%4$02d</xliff:g>
- </item>
- <item> <!-- <10 hours - example "1 02 03.04" -->
- <xliff:g id="H">%1$d</xliff:g> &#x200E;<xliff:g id="M">%2$02d</xliff:g> &#x200E;<xliff:g id="S">%3$02d</xliff:g><xliff:g id="decimal_separator">%5$c</xliff:g><xliff:g id="D">%4$02d</xliff:g>
- </item>
- <item> <!-- <100 hours - example "12 03 04.05" -->
- <xliff:g id="H">%1$02d</xliff:g> &#x200E;<xliff:g id="M">%2$02d</xliff:g> &#x200E;<xliff:g id="S">%3$02d</xliff:g><xliff:g id="decimal_separator">%5$c</xliff:g><xliff:g id="D">%4$02d</xliff:g>
- </item>
- <item> <!-- <1000 hours - example "123 04 05.06" -->
- <xliff:g id="H">%1$03d</xliff:g> &#x200E;<xliff:g id="M">%2$02d</xliff:g> &#x200E;<xliff:g id="S">%3$02d</xliff:g><xliff:g id="decimal_separator">%5$c</xliff:g><xliff:g id="D">%4$02d</xliff:g>
- </item>
- </string-array>
-
- <!-- Formatted stopwatch time for laps shared using the share button -->
- <string-array name="shared_laps_format_set" translatable="false">
- <item> <!-- <10 minutes - example "1 02.03" -->
- <xliff:g id="lap">%6$d</xliff:g><xliff:g id="decimal_separator">%5$c</xliff:g> &#x200E;<xliff:g id="M">%2$d</xliff:g> &#x200E;<xliff:g id="S">%3$02d</xliff:g><xliff:g id="decimal_separator">%5$c</xliff:g><xliff:g id="D">%4$02d</xliff:g>
- </item>
- <item> <!-- <1 hour - example "12 03.04" -->
- <xliff:g id="lap">%6$d</xliff:g><xliff:g id="decimal_separator">%5$c</xliff:g> &#x200E;<xliff:g id="M">%2$02d</xliff:g> &#x200E;<xliff:g id="S">%3$02d</xliff:g><xliff:g id="decimal_separator">%5$c</xliff:g><xliff:g id="D">%4$02d</xliff:g>
- </item>
- <item> <!-- <10 hours - example "1 02 03.04" -->
- <xliff:g id="lap">%6$d</xliff:g><xliff:g id="decimal_separator">%5$c</xliff:g> &#x200E;<xliff:g id="H">%1$d</xliff:g> &#x200E;<xliff:g id="M">%2$02d</xliff:g> &#x200E;<xliff:g id="S">%3$02d</xliff:g><xliff:g id="decimal_separator">%5$c</xliff:g><xliff:g id="D">%4$02d</xliff:g>
- </item>
- <item> <!-- <100 hours - example "12 03 04.05" -->
- <xliff:g id="lap">%6$d</xliff:g><xliff:g id="decimal_separator">%5$c</xliff:g> &#x200E;<xliff:g id="H">%1$02d</xliff:g> &#x200E;<xliff:g id="M">%2$02d</xliff:g> &#x200E;<xliff:g id="S">%3$02d</xliff:g><xliff:g id="decimal_separator">%5$c</xliff:g><xliff:g id="D">%4$02d</xliff:g>
- </item>
- <item> <!-- <1000 hours - example "123 04 05.06" -->
- <xliff:g id="lap">%6$d</xliff:g><xliff:g id="decimal_separator">%5$c</xliff:g> &#x200E;<xliff:g id="H">%1$03d</xliff:g> &#x200E;<xliff:g id="M">%2$02d</xliff:g> &#x200E;<xliff:g id="S">%3$02d</xliff:g><xliff:g id="decimal_separator">%5$c</xliff:g><xliff:g id="D">%4$02d</xliff:g>
- </item>
- </string-array>
-
- <!-- Label to enumerate the number of laps the user has counted -->
- <string-array name="sw_lap_number_set" translatable="true">
- <item># <xliff:g id="number">%d</xliff:g></item>
- <item># <xliff:g id="number">%02d</xliff:g></item>
- </string-array>
<!-- Stopwatch accessibility strings -->
<plurals name="Nhours_description">
@@ -829,8 +786,8 @@
<string name="alarm_settings">Alarms</string>
<!-- Describes the running service for the stopwatch -->
<string name="stopwatch_service_desc">Stopwatch service to run the notification.</string>
- <!-- Desription for the stopped stop watch -->
- <string name="swn_stopped">Stopped</string>
+ <!-- Description for the paused stop watch -->
+ <string name="swn_paused">Paused</string>
<!-- Text instruction for dismiss alarm on alarm lock screen. The dismiss button will still
be on the right even on RTL languages so please do not reverse this during
@@ -985,11 +942,11 @@
-->
<string name="stopwatch_cant_be_reset_because_is_running">Stopwatch cannot be reset because it is running</string>
- <!-- String that represents that the user has successfully sent a voice command stopping
+ <!-- String that represents that the user has successfully sent a voice command pausing
the stopwatch.
[CHAR LIMIT=NONE]
-->
- <string name="stopwatch_stopped">Stopwatch stopped</string>
+ <string name="stopwatch_paused">Stopwatch paused</string>
<!-- String that represents that the user has successfully sent a voice command resetting
the stopwatch.
diff --git a/src/com/android/deskclock/AlarmInitReceiver.java b/src/com/android/deskclock/AlarmInitReceiver.java
index 5df4da8c8..04e83bc65 100644
--- a/src/com/android/deskclock/AlarmInitReceiver.java
+++ b/src/com/android/deskclock/AlarmInitReceiver.java
@@ -24,6 +24,7 @@ import android.os.PowerManager.WakeLock;
import android.preference.PreferenceManager;
import com.android.deskclock.alarms.AlarmStateManager;
+import com.android.deskclock.data.DataModel;
import com.android.deskclock.settings.SettingsActivity;
import com.android.deskclock.timer.TimerObj;
@@ -56,6 +57,13 @@ public class AlarmInitReceiver extends BroadcastReceiver {
// We need to increment the global id out of the async task to prevent race conditions
AlarmStateManager.updateGlobalIntentId(context);
+ // Clear stopwatch data because stopwatch times are based on elapsed real-time values which
+ // are meaningless after a device reboot.
+ if (Intent.ACTION_BOOT_COMPLETED.equals(action)) {
+ DataModel.getDataModel().clearLaps();
+ DataModel.getDataModel().resetStopwatch();
+ }
+
AsyncHandler.post(new Runnable() {
@Override public void run() {
try {
@@ -65,7 +73,6 @@ public class AlarmInitReceiver extends BroadcastReceiver {
PreferenceManager.getDefaultSharedPreferences(context);
LogUtils.v("AlarmInitReceiver - Reset timers and clear stopwatch data");
TimerObj.resetTimersInSharedPrefs(prefs);
- Utils.clearSwSharedPref(prefs);
if (!prefs.getBoolean(PREF_VOLUME_DEF_DONE, false)) {
// Fix the default
diff --git a/src/com/android/deskclock/CircleTimerView.java b/src/com/android/deskclock/CircleTimerView.java
index 83d0cc76b..7d88dafbc 100644
--- a/src/com/android/deskclock/CircleTimerView.java
+++ b/src/com/android/deskclock/CircleTimerView.java
@@ -1,7 +1,6 @@
package com.android.deskclock;
import android.content.Context;
-import android.content.SharedPreferences;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Color;
@@ -10,8 +9,6 @@ import android.graphics.RectF;
import android.util.AttributeSet;
import android.view.View;
-import com.android.deskclock.stopwatch.Stopwatches;
-
/**
* Class to draw a circle for timers and stopwatches.
* These two usages require two different animation modes:
@@ -27,7 +24,6 @@ public class CircleTimerView extends View {
private long mMarkerTime = -1;
private long mCurrentIntervalTime = 0;
private long mAccumulatedTime = 0;
- private boolean mPaused = false;
private boolean mAnimate = false;
private static float mStrokeSize = 4;
private static float mDotRadius = 6;
@@ -56,11 +52,6 @@ public class CircleTimerView extends View {
postInvalidate();
}
- public void setMarkerTime(long t) {
- mMarkerTime = t;
- postInvalidate();
- }
-
public void reset() {
mIntervalStartTime = -1;
mMarkerTime = -1;
@@ -70,8 +61,8 @@ public class CircleTimerView extends View {
mIntervalStartTime = Utils.getTimeNow();
mAnimate = true;
invalidate();
- mPaused = false;
}
+
public void stopIntervalAnimation() {
mAnimate = false;
mIntervalStartTime = -1;
@@ -85,7 +76,6 @@ public class CircleTimerView extends View {
public void pauseIntervalAnimation() {
mAnimate = false;
mAccumulatedTime += Utils.getTimeNow() - mIntervalStartTime;
- mPaused = true;
}
public void abortIntervalAnimation() {
@@ -108,8 +98,6 @@ public class CircleTimerView extends View {
postInvalidate();
}
-
-
private void init(Context c) {
Resources resources = c.getResources();
@@ -211,51 +199,4 @@ public class CircleTimerView extends View {
canvas.drawCircle(xCenter + (float) (radius * Math.cos(dotRadians)),
yCenter + (float) (radius * Math.sin(dotRadians)), mDotRadius, mFill);
}
-
- public static final String PREF_CTV_PAUSED = "_ctv_paused";
- public static final String PREF_CTV_INTERVAL = "_ctv_interval";
- public static final String PREF_CTV_INTERVAL_START = "_ctv_interval_start";
- public static final String PREF_CTV_CURRENT_INTERVAL = "_ctv_current_interval";
- public static final String PREF_CTV_ACCUM_TIME = "_ctv_accum_time";
- public static final String PREF_CTV_TIMER_MODE = "_ctv_timer_mode";
- public static final String PREF_CTV_MARKER_TIME = "_ctv_marker_time";
-
- // Since this view is used in multiple places, use the key to save different instances
- public void writeToSharedPref(SharedPreferences prefs, String key) {
- SharedPreferences.Editor editor = prefs.edit();
- editor.putBoolean (key + PREF_CTV_PAUSED, mPaused);
- editor.putLong (key + PREF_CTV_INTERVAL, mIntervalTime);
- editor.putLong (key + PREF_CTV_INTERVAL_START, mIntervalStartTime);
- editor.putLong (key + PREF_CTV_CURRENT_INTERVAL, mCurrentIntervalTime);
- editor.putLong (key + PREF_CTV_ACCUM_TIME, mAccumulatedTime);
- editor.putLong (key + PREF_CTV_MARKER_TIME, mMarkerTime);
- editor.putBoolean (key + PREF_CTV_TIMER_MODE, mTimerMode);
- editor.apply();
- }
-
- public void readFromSharedPref(SharedPreferences prefs, String key) {
- mPaused = prefs.getBoolean(key + PREF_CTV_PAUSED, false);
- mIntervalTime = prefs.getLong(key + PREF_CTV_INTERVAL, 0);
- mIntervalStartTime = prefs.getLong(key + PREF_CTV_INTERVAL_START, -1);
- mCurrentIntervalTime = prefs.getLong(key + PREF_CTV_CURRENT_INTERVAL, 0);
- mAccumulatedTime = prefs.getLong(key + PREF_CTV_ACCUM_TIME, 0);
- mMarkerTime = prefs.getLong(key + PREF_CTV_MARKER_TIME, -1);
- mTimerMode = prefs.getBoolean(key + PREF_CTV_TIMER_MODE, false);
- mAnimate = (mIntervalStartTime != -1 && !mPaused);
- }
-
- public void clearSharedPref(SharedPreferences prefs, String key) {
- SharedPreferences.Editor editor = prefs.edit();
- editor.remove (Stopwatches.PREF_START_TIME);
- editor.remove (Stopwatches.PREF_ACCUM_TIME);
- editor.remove (Stopwatches.PREF_STATE);
- editor.remove (key + PREF_CTV_PAUSED);
- editor.remove (key + PREF_CTV_INTERVAL);
- editor.remove (key + PREF_CTV_INTERVAL_START);
- editor.remove (key + PREF_CTV_CURRENT_INTERVAL);
- editor.remove (key + PREF_CTV_ACCUM_TIME);
- editor.remove (key + PREF_CTV_MARKER_TIME);
- editor.remove (key + PREF_CTV_TIMER_MODE);
- editor.apply();
- }
}
diff --git a/src/com/android/deskclock/ClockFragment.java b/src/com/android/deskclock/ClockFragment.java
index f7ef1a9c6..b5739df03 100644
--- a/src/com/android/deskclock/ClockFragment.java
+++ b/src/com/android/deskclock/ClockFragment.java
@@ -52,7 +52,7 @@ import static java.util.Calendar.DAY_OF_WEEK;
/**
* Fragment that shows the clock (analog or digital), the next alarm info and the world clock.
*/
-public class ClockFragment extends DeskClockFragment {
+public final class ClockFragment extends DeskClockFragment {
private TextClock mDigitalClock;
private View mAnalogClock, mClockFrame;
@@ -61,8 +61,6 @@ public class ClockFragment extends DeskClockFragment {
private String mDateFormat;
private String mDateFormatForAccessibility;
- private final Handler mHandler = new Handler();
-
// Updates the UI in response to system setting changes that alter time values and time display.
private final BroadcastReceiver mBroadcastReceiver = new SystemBroadcastReceiver();
@@ -72,10 +70,19 @@ public class ClockFragment extends DeskClockFragment {
// Updates dates in the UI on every quarter-hour.
private final Runnable mQuarterHourUpdater = new QuarterHourRunnable();
+ private Handler mHandler;
+
/** The public no-arg constructor required by all fragments. */
public ClockFragment() {}
@Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ mHandler = new Handler();
+ }
+
+ @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle icicle) {
super.onCreateView(inflater, container, icicle);
@@ -401,6 +408,9 @@ public class ClockFragment extends DeskClockFragment {
return view;
}
+ /**
+ * @return {@code false} to prevent the cities from responding to touch
+ */
@Override
public boolean isEnabled(int position) {
return false;
diff --git a/src/com/android/deskclock/DeskClock.java b/src/com/android/deskclock/DeskClock.java
index cf7e37d84..d8fb223dd 100644
--- a/src/com/android/deskclock/DeskClock.java
+++ b/src/com/android/deskclock/DeskClock.java
@@ -47,12 +47,11 @@ import com.android.deskclock.actionbarmenu.MenuItemControllerFactory;
import com.android.deskclock.actionbarmenu.NightModeMenuItemController;
import com.android.deskclock.actionbarmenu.SettingMenuItemController;
import com.android.deskclock.alarms.AlarmStateManager;
+import com.android.deskclock.data.DataModel;
import com.android.deskclock.events.Events;
import com.android.deskclock.provider.Alarm;
import com.android.deskclock.settings.SettingsActivity;
import com.android.deskclock.stopwatch.StopwatchFragment;
-import com.android.deskclock.stopwatch.StopwatchService;
-import com.android.deskclock.stopwatch.Stopwatches;
import com.android.deskclock.timer.TimerFragment;
import com.android.deskclock.timer.TimerObj;
import com.android.deskclock.timer.Timers;
@@ -93,7 +92,6 @@ public class DeskClock extends BaseActivity
private TabsAdapter mTabsAdapter;
private int mSelectedTab;
- private boolean mActivityResumed;
@Override
public void onNewIntent(Intent newIntent) {
@@ -219,27 +217,18 @@ public class DeskClock extends BaseActivity
protected void onResume() {
super.onResume();
- // We only want to show notifications for stopwatch/timer when the app is closed so
- // that we don't have to worry about keeping the notifications in perfect sync with
- // the app.
- Intent stopwatchIntent = new Intent(getApplicationContext(), StopwatchService.class);
- stopwatchIntent.setAction(Stopwatches.KILL_NOTIF);
- startService(stopwatchIntent);
+ DataModel.getDataModel().setApplicationInForeground(true);
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
SharedPreferences.Editor editor = prefs.edit();
editor.putBoolean(Timers.NOTIF_APP_OPEN, true);
editor.apply();
sendBroadcast(new Intent(Timers.NOTIF_IN_USE_CANCEL));
- mActivityResumed = true;
}
@Override
public void onPause() {
- mActivityResumed = false;
- Intent intent = new Intent(getApplicationContext(), StopwatchService.class);
- intent.setAction(Stopwatches.SHOW_NOTIF);
- startService(intent);
+ DataModel.getDataModel().setApplicationInForeground(false);
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
SharedPreferences.Editor editor = prefs.edit();
@@ -406,7 +395,9 @@ public class DeskClock extends BaseActivity
mSelectedTab = position;
nightModeMenuItemController.setEnabled(mSelectedTab == CLOCK_TAB_INDEX);
- if (mActivityResumed) {
+ // Avoid sending events for the initial tab selection on launch and the reselecting a
+ // tab after a configuration change.
+ if (DataModel.getDataModel().isApplicationInForeground()) {
switch (mSelectedTab) {
case ALARM_TAB_INDEX:
Events.sendAlarmEvent(R.string.action_show, R.string.label_deskclock);
diff --git a/src/com/android/deskclock/HandleDeskClockApiCalls.java b/src/com/android/deskclock/HandleDeskClockApiCalls.java
index 2e59b0925..3a4243ca0 100644
--- a/src/com/android/deskclock/HandleDeskClockApiCalls.java
+++ b/src/com/android/deskclock/HandleDeskClockApiCalls.java
@@ -26,9 +26,9 @@ import android.preference.PreferenceManager;
import com.android.deskclock.data.City;
import com.android.deskclock.data.DataModel;
+import com.android.deskclock.data.Stopwatch;
import com.android.deskclock.events.Events;
import com.android.deskclock.stopwatch.StopwatchService;
-import com.android.deskclock.stopwatch.Stopwatches;
import com.android.deskclock.timer.TimerFullScreenFragment;
import com.android.deskclock.timer.TimerObj;
import com.android.deskclock.timer.Timers;
@@ -60,8 +60,8 @@ public class HandleDeskClockApiCalls extends Activity {
public static final String ACTION_SHOW_STOPWATCH = ACTION_PREFIX + "SHOW_STOPWATCH";
// starts the current stopwatch
public static final String ACTION_START_STOPWATCH = ACTION_PREFIX + "START_STOPWATCH";
- // stops the current stopwatch
- public static final String ACTION_STOP_STOPWATCH = ACTION_PREFIX + "STOP_STOPWATCH";
+ // pauses the current stopwatch that's currently running
+ public static final String ACTION_PAUSE_STOPWATCH = ACTION_PREFIX + "PAUSE_STOPWATCH";
// laps the stopwatch that's currently running
public static final String ACTION_LAP_STOPWATCH = ACTION_PREFIX + "LAP_STOPWATCH";
// resets the stopwatch if it's stopped
@@ -78,6 +78,10 @@ public class HandleDeskClockApiCalls extends Activity {
// resets the timer, works for both running and stopped
public static final String ACTION_RESET_TIMER = ACTION_PREFIX + "RESET_TIMER";
+ // extra for actions originating from the notifications
+ public static final String EXTRA_FROM_NOTIFICATION =
+ "com.android.deskclock.extra.FROM_NOTIFICATION";
+
@Override
protected void onCreate(Bundle icicle) {
try {
@@ -90,13 +94,15 @@ public class HandleDeskClockApiCalls extends Activity {
}
final String action = intent.getAction();
+ LogUtils.i("HandleDeskClockApiCalls " + action);
+
switch (action) {
case ACTION_START_STOPWATCH:
- case ACTION_STOP_STOPWATCH:
+ case ACTION_PAUSE_STOPWATCH:
case ACTION_LAP_STOPWATCH:
case ACTION_SHOW_STOPWATCH:
case ACTION_RESET_STOPWATCH:
- handleStopwatchIntent(action);
+ handleStopwatchIntent(intent);
break;
case ACTION_SHOW_TIMERS:
case ACTION_DELETE_TIMER:
@@ -108,7 +114,7 @@ public class HandleDeskClockApiCalls extends Activity {
case ACTION_SHOW_CLOCK:
case ACTION_ADD_CLOCK:
case ACTION_DELETE_CLOCK:
- handleClockIntent(action);
+ handleClockIntent(intent);
break;
}
} finally {
@@ -116,74 +122,83 @@ public class HandleDeskClockApiCalls extends Activity {
}
}
- private void handleStopwatchIntent(String action) {
- // Opens the UI for stopwatch
- final Intent stopwatchIntent = new Intent(mAppContext, DeskClock.class)
- .setAction(action)
- .putExtra(DeskClock.SELECT_TAB_INTENT_EXTRA, DeskClock.STOPWATCH_TAB_INDEX);
- startActivity(stopwatchIntent);
- LogUtils.i("HandleDeskClockApiCalls " + action);
+ private void handleStopwatchIntent(Intent intent) {
+ final String action = intent.getAction();
- if (action.equals(ACTION_SHOW_STOPWATCH)) {
- Events.sendStopwatchEvent(R.string.action_show, R.string.label_intent);
- return;
- }
+ // Determine where this intent originated.
+ final boolean fromNotif =
+ intent.getBooleanExtra(HandleDeskClockApiCalls.EXTRA_FROM_NOTIFICATION, false);
+ final int label = fromNotif ? R.string.label_notification : R.string.label_intent;
- // checking if the stopwatch is already running
- final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mAppContext);
- final boolean stopwatchAlreadyRunning =
- prefs.getBoolean(Stopwatches.NOTIF_CLOCK_RUNNING, false);
+ if (ACTION_SHOW_STOPWATCH.equals(action)) {
+ Events.sendStopwatchEvent(R.string.action_show, label);
+ } else {
+ final Stopwatch stopwatch = DataModel.getDataModel().getStopwatch();
- if (stopwatchAlreadyRunning) {
- // don't fire START_STOPWATCH or RESET_STOPWATCH if a stopwatch is already running
- if (ACTION_START_STOPWATCH.equals(action)) {
- final String reason = getString(R.string.stopwatch_already_running);
- Voice.notifyFailure(this, reason);
- LogUtils.i(reason);
- return;
- } else if (ACTION_RESET_STOPWATCH.equals(action)) { // RESET_STOPWATCH
- final String reason = getString(R.string.stopwatch_cant_be_reset_because_is_running);
- Voice.notifyFailure(this, reason);
- LogUtils.i(reason);
- return;
+ final String reason;
+ boolean fail = false;
+ switch (action) {
+ case ACTION_START_STOPWATCH: {
+ if (stopwatch.isRunning()) {
+ fail = true;
+ reason = getString(R.string.stopwatch_already_running);
+ } else {
+ Events.sendStopwatchEvent(R.string.action_start, label);
+ reason = getString(R.string.stopwatch_started);
+ }
+ break;
+ }
+ case ACTION_PAUSE_STOPWATCH: {
+ if (!stopwatch.isRunning()) {
+ fail = true;
+ reason = getString(R.string.stopwatch_isnt_running);
+ } else {
+ Events.sendStopwatchEvent(R.string.action_pause, label);
+ reason = getString(R.string.stopwatch_paused);
+ }
+ break;
+ }
+ case ACTION_LAP_STOPWATCH: {
+ if (!stopwatch.isRunning()) {
+ fail = true;
+ reason = getString(R.string.stopwatch_isnt_running);
+ } else {
+ Events.sendStopwatchEvent(R.string.action_lap, label);
+ reason = getString(R.string.stopwatch_lapped);
+ }
+ break;
+ }
+ case ACTION_RESET_STOPWATCH: {
+ if (stopwatch.isRunning()) {
+ fail = true;
+ reason = getString(R.string.stopwatch_cant_be_reset_because_is_running);
+ } else {
+ Events.sendStopwatchEvent(R.string.action_reset, label);
+ reason = getString(R.string.stopwatch_reset);
+ }
+ break;
+ }
+ default:
+ throw new IllegalArgumentException("unknown stopwatch action: " + action);
}
- } else {
- // if a stopwatch isn't running, don't try to stop or lap it
- if (ACTION_STOP_STOPWATCH.equals(action) ||
- ACTION_LAP_STOPWATCH.equals(action)) {
- final String reason = getString(R.string.stopwatch_isnt_running);
+
+ if (fail) {
Voice.notifyFailure(this, reason);
- LogUtils.i(reason);
- return;
+ } else {
+ // Perform the action on the stopwatch.
+ final Intent performActionIntent = new Intent(mAppContext, StopwatchService.class)
+ .setAction(action);
+ startService(performActionIntent);
+ Voice.notifySuccess(this, reason);
}
+ LogUtils.i(reason);
}
- final String reason;
- // Events and voice interactor setup
- switch (action) {
- case ACTION_START_STOPWATCH:
- Events.sendStopwatchEvent(R.string.action_start, R.string.label_intent);
- reason = getString(R.string.stopwatch_started);
- break;
- case ACTION_STOP_STOPWATCH:
- Events.sendStopwatchEvent(R.string.action_stop, R.string.label_intent);
- reason = getString(R.string.stopwatch_stopped);
- break;
- case ACTION_LAP_STOPWATCH:
- Events.sendStopwatchEvent(R.string.action_lap, R.string.label_intent);
- reason = getString(R.string.stopwatch_lapped);
- break;
- case ACTION_RESET_STOPWATCH:
- Events.sendStopwatchEvent(R.string.action_reset, R.string.label_intent);
- reason = getString(R.string.stopwatch_reset);
- break;
- default:
- return;
- }
- final Intent intent = new Intent(mAppContext, StopwatchService.class).setAction(action);
- startService(intent);
- Voice.notifySuccess(this, reason);
- LogUtils.i(reason);
+ // Open the UI to the stopwatch.
+ final Intent stopwatchIntent = new Intent(mAppContext, DeskClock.class)
+ .setAction(action)
+ .putExtra(DeskClock.SELECT_TAB_INTENT_EXTRA, DeskClock.STOPWATCH_TAB_INDEX);
+ startActivity(stopwatchIntent);
}
private void handleTimerIntent(final String action) {
@@ -192,7 +207,6 @@ public class HandleDeskClockApiCalls extends Activity {
.putExtra(DeskClock.SELECT_TAB_INTENT_EXTRA, DeskClock.TIMER_TAB_INDEX)
.putExtra(TimerFullScreenFragment.GOTO_SETUP_VIEW, false);
startActivity(timerIntent);
- LogUtils.i("HandleDeskClockApiCalls " + action);
if (ACTION_SHOW_TIMERS.equals(action)) {
Events.sendTimerEvent(R.string.action_show, R.string.label_intent);
@@ -201,15 +215,104 @@ public class HandleDeskClockApiCalls extends Activity {
new HandleTimersAsync(mAppContext, action, this).execute();
}
- private void handleClockIntent(final String action) {
+ private void handleClockIntent(Intent intent) {
+ final String action = intent.getAction();
+
+ if (ACTION_SHOW_CLOCK.equals(action)) {
+ final boolean fromWidget = intent.getBooleanExtra(EXTRA_FROM_WIDGET, false);
+ final int label = fromWidget ? R.string.label_widget : R.string.label_intent;
+ Events.sendClockEvent(R.string.action_show, label);
+ } else {
+ final String cityName = intent.getStringExtra(EXTRA_CITY);
+
+ final String reason;
+ boolean fail = false;
+
+ // If no city was given, start the city chooser.
+ if (cityName == null) {
+ reason = getString(R.string.no_city_selected);
+ LogUtils.i(reason);
+ Voice.notifySuccess(this, reason);
+ startActivity(new Intent(this, CitySelectionActivity.class)
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
+ switch (action) {
+ case ACTION_ADD_CLOCK:
+ Events.sendClockEvent(R.string.action_add, R.string.label_intent);
+ break;
+ case ACTION_DELETE_CLOCK:
+ Events.sendClockEvent(R.string.action_delete, R.string.label_intent);
+ break;
+ }
+ return;
+ }
+
+ // If a city was given, ensure it can be located.
+ final City city = DataModel.getDataModel().getCity(cityName);
+ if (city == null) {
+ reason = getString(R.string.the_city_you_specified_is_not_available);
+ LogUtils.i(reason);
+ Voice.notifyFailure(this, reason);
+ switch (action) {
+ case ACTION_ADD_CLOCK:
+ Events.sendClockEvent(R.string.action_add, R.string.label_intent);
+ break;
+ case ACTION_DELETE_CLOCK:
+ Events.sendClockEvent(R.string.action_delete, R.string.label_intent);
+ break;
+ }
+ return;
+ }
+
+ final Set<City> selectedCities =
+ Utils.newArraySet(DataModel.getDataModel().getSelectedCities());
+
+ switch (action) {
+ case ACTION_ADD_CLOCK: {
+ // Fail if the city is already present.
+ if (!selectedCities.add(city)) {
+ fail = true;
+ reason = getString(R.string.the_city_already_added);
+ break;
+ }
+
+ // Otherwise report the success.
+ DataModel.getDataModel().setSelectedCities(selectedCities);
+ reason = getString(R.string.city_added, city.getName());
+ Events.sendClockEvent(R.string.action_add, R.string.label_intent);
+ break;
+ }
+ case ACTION_DELETE_CLOCK: {
+ // Fail if the city is not present.
+ if (!selectedCities.remove(city)) {
+ fail = true;
+ reason = getString(R.string.the_city_you_specified_is_not_available);
+ break;
+ }
+
+ // Otherwise report the success.
+ DataModel.getDataModel().setSelectedCities(selectedCities);
+ reason = getString(R.string.city_deleted, city.getName());
+ Events.sendClockEvent(R.string.action_delete, R.string.label_intent);
+ break;
+ }
+ default:
+ throw new IllegalArgumentException("unknown clock action: " + action);
+ }
+
+ if (fail) {
+ Voice.notifyFailure(this, reason);
+ } else {
+ Voice.notifySuccess(this, reason);
+ }
+ LogUtils.i(reason);
+ }
+
// Opens the UI for clocks
- final Intent handleClock = new Intent(mAppContext, DeskClock.class)
+ final Intent clockIntent = new Intent(mAppContext, DeskClock.class)
.setAction(action)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
.putExtra(DeskClock.SELECT_TAB_INTENT_EXTRA, DeskClock.CLOCK_TAB_INDEX);
- startActivity(handleClock);
-
- new HandleClockAsync(mAppContext, getIntent(), this).execute();
+ startActivity(clockIntent);
}
private static class HandleTimersAsync extends AsyncTask<Void, Void, Void> {
@@ -402,112 +505,4 @@ public class HandleDeskClockApiCalls extends Activity {
return soleTimer;
}
}
-
- private static class HandleClockAsync extends AsyncTask<Void, Void, Void> {
- private final Context mContext;
- private final Intent mIntent;
- private final Activity mActivity;
-
- public HandleClockAsync(Context context, Intent intent, Activity activity) {
- mContext = context;
- mIntent = intent;
- mActivity = activity;
- }
-
- @Override
- protected Void doInBackground(Void... parameters) {
- final String cityName = mIntent.getStringExtra(EXTRA_CITY);
- switch (mIntent.getAction()) {
- case ACTION_ADD_CLOCK: {
- // if a city isn't specified open CitiesActivity to choose a city
- if (cityName == null) {
- final String reason = mContext.getString(R.string.no_city_selected);
- Voice.notifyFailure(mActivity, reason);
- LogUtils.i(reason);
- startSelectWorldClocksActivity();
- Events.sendClockEvent(R.string.action_create, R.string.label_intent);
- break;
- }
-
- // if a city is passed add that city to the list
- final City city = DataModel.getDataModel().getCity(cityName);
- // check if this city exists in the list of available cities
- if (city == null) {
- final String reason = mContext.getString(
- R.string.the_city_you_specified_is_not_available);
- Voice.notifyFailure(mActivity, reason);
- LogUtils.i(reason);
- break;
- }
-
- final Set<City> selectedCities =
- Utils.newArraySet(DataModel.getDataModel().getSelectedCities());
- // if this city is already added don't add it
- if (!selectedCities.add(city)) {
- final String reason = mContext.getString(R.string.the_city_already_added);
- Voice.notifyFailure(mActivity, reason);
- LogUtils.i(reason);
- break;
- }
-
- DataModel.getDataModel().setSelectedCities(selectedCities);
- final String reason = mContext.getString(R.string.city_added, city.getName());
- Voice.notifySuccess(mActivity, reason);
- LogUtils.i(reason);
- Events.sendClockEvent(R.string.action_start, R.string.label_intent);
- break;
- }
- case ACTION_DELETE_CLOCK: {
- if (cityName == null) {
- // if a city isn't specified open the activity that chooses a city
- final String reason = mContext.getString(R.string.no_city_selected);
- Voice.notifyFailure(mActivity, reason);
- LogUtils.i(reason);
- startSelectWorldClocksActivity();
- Events.sendClockEvent(R.string.action_create, R.string.label_intent);
- break;
- }
-
- // if a city is specified check if it's selected and if so delete it
- final City city = DataModel.getDataModel().getCity(cityName);
- if (city == null) {
- final String reason = mContext.getString(
- R.string.the_city_you_specified_is_not_available);
- Voice.notifyFailure(mActivity, reason);
- LogUtils.i(reason);
- break;
- }
-
- final Set<City> selectedCities =
- Utils.newArraySet(DataModel.getDataModel().getSelectedCities());
-
- // check if this city exists in the list of available cities
- if (!selectedCities.remove(city)) {
- // the specified city hasn't been added to the user's list yet
- Voice.notifyFailure(mActivity, mContext.getString(
- R.string.the_city_you_specified_is_not_available));
- break;
- }
-
- DataModel.getDataModel().setSelectedCities(selectedCities);
- final String reason = mContext.getString(R.string.city_deleted, city.getName());
- Voice.notifySuccess(mActivity, reason);
- LogUtils.i(reason);
- Events.sendClockEvent(R.string.action_delete, R.string.label_intent);
- break;
- }
- case ACTION_SHOW_CLOCK:
- final boolean fromWidget = mIntent.getBooleanExtra(EXTRA_FROM_WIDGET, false);
- final int label = fromWidget ? R.string.label_widget : R.string.label_intent;
- Events.sendClockEvent(R.string.action_show, label);
- break;
- }
- return null;
- }
-
- private void startSelectWorldClocksActivity() {
- mContext.startActivity(new Intent(mContext, CitySelectionActivity.class)
- .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
- }
- }
}
diff --git a/src/com/android/deskclock/Utils.java b/src/com/android/deskclock/Utils.java
index d142dd244..73b7a91cc 100644
--- a/src/com/android/deskclock/Utils.java
+++ b/src/com/android/deskclock/Utils.java
@@ -25,9 +25,6 @@ import android.app.AlarmManager;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
-import android.content.SharedPreferences;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Color;
@@ -35,7 +32,6 @@ import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.graphics.Typeface;
-import android.net.Uri;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
@@ -52,7 +48,6 @@ import android.text.style.AbsoluteSizeSpan;
import android.text.style.StyleSpan;
import android.text.style.TypefaceSpan;
import android.util.ArraySet;
-import android.view.MenuItem;
import android.view.View;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.DecelerateInterpolator;
@@ -63,7 +58,6 @@ import com.android.deskclock.data.DataModel;
import com.android.deskclock.provider.AlarmInstance;
import com.android.deskclock.provider.DaysOfWeek;
import com.android.deskclock.settings.SettingsActivity;
-import com.android.deskclock.stopwatch.Stopwatches;
import com.android.deskclock.timer.Timers;
import java.text.NumberFormat;
@@ -177,7 +171,7 @@ public class Utils {
}
/**
- * Calculate the amount by which the radius of a CircleTimerView should be offset by the any
+ * Calculate the amount by which the radius of a CircleTimerView should be offset by any
* of the extra painted objects.
*/
public static float calculateRadiusOffset(
@@ -201,23 +195,6 @@ public class Utils {
}
/**
- * Clears the persistent data of stopwatch (start time, state, laps, etc...).
- */
- public static void clearSwSharedPref(SharedPreferences prefs) {
- SharedPreferences.Editor editor = prefs.edit();
- editor.remove (Stopwatches.PREF_START_TIME);
- editor.remove (Stopwatches.PREF_ACCUM_TIME);
- editor.remove (Stopwatches.PREF_STATE);
- int lapNum = prefs.getInt(Stopwatches.PREF_LAP_NUM, Stopwatches.STOPWATCH_RESET);
- for (int i = 0; i < lapNum; i++) {
- String key = Stopwatches.PREF_LAP_TIME + Integer.toString(i);
- editor.remove(key);
- }
- editor.remove(Stopwatches.PREF_LAP_NUM);
- editor.apply();
- }
-
- /**
* Broadcast a message to show the in-use timers in the notifications
*/
public static void showInUseNotifications(Context context) {
@@ -699,7 +676,6 @@ public class Utils {
}
/**
- * @param context
* @param id Resource id of the plural
* @param quantity integer value
* @return string with properly localized numbers
diff --git a/src/com/android/deskclock/data/City.java b/src/com/android/deskclock/data/City.java
index 341f212a7..a5fd850ae 100644
--- a/src/com/android/deskclock/data/City.java
+++ b/src/com/android/deskclock/data/City.java
@@ -50,7 +50,7 @@ public final class City {
/** A cached upper case form of the {@link #mName} used in case-insensitive name comparisons. */
private String mNameUpperCase;
- public City(String id, int index, String indexString, String name, String phoneticName,
+ City(String id, int index, String indexString, String name, String phoneticName,
String timeZoneId) {
mId = id;
mIndex = index;
diff --git a/src/com/android/deskclock/data/CityDAO.java b/src/com/android/deskclock/data/CityDAO.java
index 39d78f2e1..bdebca4d3 100644
--- a/src/com/android/deskclock/data/CityDAO.java
+++ b/src/com/android/deskclock/data/CityDAO.java
@@ -47,7 +47,7 @@ final class CityDAO {
// Key to a preference that stores the number of selected cities.
private static final String NUMBER_OF_CITIES = "number_of_cities";
- // Prefix for a key to a preference entry indicating the id of a selected city.
+ // Prefix for a key to a preference that stores the id of a selected city.
private static final String CITY_ID = "city_id_";
// Lazily instantiated and cached for the life of the application.
@@ -156,4 +156,4 @@ final class CityDAO {
return sPrefs;
}
-}
+} \ No newline at end of file
diff --git a/src/com/android/deskclock/data/CityModel.java b/src/com/android/deskclock/data/CityModel.java
index 3cf47d1c4..eb1bd6a39 100644
--- a/src/com/android/deskclock/data/CityModel.java
+++ b/src/com/android/deskclock/data/CityModel.java
@@ -102,6 +102,7 @@ final class CityModel {
allCities.addAll(getUnselectedCities());
mAllCities = Collections.unmodifiableList(allCities);
}
+
return mAllCities;
}
@@ -168,6 +169,7 @@ final class CityModel {
Collections.sort(selectedCities, new City.UtcOffsetComparator());
mSelectedCities = Collections.unmodifiableList(selectedCities);
}
+
return mSelectedCities;
}
@@ -220,6 +222,7 @@ final class CityModel {
if (mCityMap == null) {
mCityMap = CityDAO.getCities(mContext);
}
+
return mCityMap;
}
diff --git a/src/com/android/deskclock/data/DataModel.java b/src/com/android/deskclock/data/DataModel.java
index f7e66c0e5..b51b67d41 100644
--- a/src/com/android/deskclock/data/DataModel.java
+++ b/src/com/android/deskclock/data/DataModel.java
@@ -55,6 +55,12 @@ public final class DataModel {
/** The model from which alarm data are fetched. */
private AlarmModel mAlarmModel;
+ /** The model from which stopwatch data are fetched. */
+ private StopwatchModel mStopwatchModel;
+
+ /** The model from which notification data are fetched. */
+ private NotificationModel mNotificationModel;
+
public static DataModel getDataModel() {
return sDataModel;
}
@@ -69,10 +75,38 @@ public final class DataModel {
throw new IllegalStateException("context has already been set");
}
mContext = context.getApplicationContext();
+
mSettingsModel = new SettingsModel(mContext);
+ mNotificationModel = new NotificationModel();
mCityModel = new CityModel(mContext, mSettingsModel);
mTimerModel = new TimerModel(mContext, mSettingsModel);
mAlarmModel = new AlarmModel(mContext, mSettingsModel);
+ mStopwatchModel = new StopwatchModel(mContext, mNotificationModel);
+ }
+
+ //
+ // Application
+ //
+
+ /**
+ * @param inForeground {@code true} to indicate the application is open in the foreground
+ */
+ public void setApplicationInForeground(boolean inForeground) {
+ enforceMainLooper();
+
+ if (mNotificationModel.isApplicationInForeground() != inForeground) {
+ mNotificationModel.setApplicationInForeground(inForeground);
+
+ // Refresh all notifications in response to a change in app open state.
+ mStopwatchModel.updateNotification();
+ }
+ }
+
+ /**
+ * @return {@code true} when the application is open in the foreground; {@code false} otherwise
+ */
+ public boolean isApplicationInForeground() {
+ return mNotificationModel.isApplicationInForeground();
}
//
@@ -218,6 +252,91 @@ public final class DataModel {
}
//
+ // Stopwatch
+ //
+
+ /**
+ * @return the current state of the stopwatch
+ */
+ public Stopwatch getStopwatch() {
+ enforceMainLooper();
+ return mStopwatchModel.getStopwatch();
+ }
+
+ /**
+ * @return the stopwatch after being started
+ */
+ public Stopwatch startStopwatch() {
+ enforceMainLooper();
+ return mStopwatchModel.setStopwatch(getStopwatch().start());
+ }
+
+ /**
+ * @return the stopwatch after being paused
+ */
+ public Stopwatch pauseStopwatch() {
+ enforceMainLooper();
+ return mStopwatchModel.setStopwatch(getStopwatch().pause());
+ }
+
+ /**
+ * @return the stopwatch after being reset
+ */
+ public Stopwatch resetStopwatch() {
+ enforceMainLooper();
+ return mStopwatchModel.setStopwatch(getStopwatch().reset());
+ }
+
+ /**
+ * @return the laps recorded for this stopwatch
+ */
+ public List<Lap> getLaps() {
+ enforceMainLooper();
+ return mStopwatchModel.getLaps();
+ }
+
+ /**
+ * @return a newly recorded lap completed now; {@code null} if no more laps can be added
+ */
+ public Lap addLap() {
+ enforceMainLooper();
+ return mStopwatchModel.addLap();
+ }
+
+ /**
+ * Clears the laps recorded for this stopwatch.
+ */
+ public void clearLaps() {
+ enforceMainLooper();
+ mStopwatchModel.clearLaps();
+ }
+
+ /**
+ * @return {@code true} iff more laps can be recorded
+ */
+ public boolean canAddMoreLaps() {
+ enforceMainLooper();
+ return mStopwatchModel.canAddMoreLaps();
+ }
+
+ /**
+ * @return the longest lap time of all recorded laps and the current lap
+ */
+ public long getLongestLapTime() {
+ enforceMainLooper();
+ return mStopwatchModel.getLongestLapTime();
+ }
+
+ /**
+ * @param time a point in time after the end of the last lap
+ * @return the elapsed time between the given {@code time} and the end of the previous lap
+ */
+ public long getCurrentLapTime(long time) {
+ enforceMainLooper();
+ return mStopwatchModel.getCurrentLapTime(time);
+ }
+
+ //
// Settings
//
@@ -245,4 +364,4 @@ public final class DataModel {
enforceMainLooper();
return mSettingsModel.getShowHomeClock();
}
-}
+} \ No newline at end of file
diff --git a/src/com/android/deskclock/data/Lap.java b/src/com/android/deskclock/data/Lap.java
new file mode 100644
index 000000000..1c42fd83d
--- /dev/null
+++ b/src/com/android/deskclock/data/Lap.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.data;
+
+/**
+ * A read-only domain object representing a stopwatch lap.
+ */
+public final class Lap {
+
+ /** The 1-based position of the lap. */
+ private final int mLapNumber;
+
+ /** Elapsed time in ms since the lap was last started. */
+ private final long mLapTime;
+
+ /** Elapsed time in ms accumulated for all laps up to and including this one. */
+ private final long mAccumulatedTime;
+
+ Lap(int lapNumber, long lapTime, long accumulatedTime) {
+ mLapNumber = lapNumber;
+ mLapTime = lapTime;
+ mAccumulatedTime = accumulatedTime;
+ }
+
+ public int getLapNumber() { return mLapNumber; }
+ public long getLapTime() { return mLapTime; }
+ public long getAccumulatedTime() { return mAccumulatedTime; }
+} \ No newline at end of file
diff --git a/src/com/android/deskclock/data/NotificationModel.java b/src/com/android/deskclock/data/NotificationModel.java
new file mode 100644
index 000000000..e772a5018
--- /dev/null
+++ b/src/com/android/deskclock/data/NotificationModel.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.data;
+
+/**
+ * Data that must be coordinated across all notifications is accessed via this model.
+ */
+final class NotificationModel {
+
+ private boolean mApplicationInForeground;
+
+ /**
+ * @param inForeground {@code true} to indicate the application is open in the foreground
+ */
+ void setApplicationInForeground(boolean inForeground) {
+ mApplicationInForeground = inForeground;
+ }
+
+ /**
+ * @return {@code true} while the application is open in the foreground
+ */
+ boolean isApplicationInForeground() {
+ return mApplicationInForeground;
+ }
+
+ /**
+ * @return a value that identifies the stopwatch notification
+ */
+ int getStopwatchNotificationId() {
+ return Integer.MAX_VALUE - 1;
+ }
+} \ No newline at end of file
diff --git a/src/com/android/deskclock/data/Stopwatch.java b/src/com/android/deskclock/data/Stopwatch.java
new file mode 100644
index 000000000..9eb2b9cf2
--- /dev/null
+++ b/src/com/android/deskclock/data/Stopwatch.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.data;
+
+import android.os.SystemClock;
+
+import static com.android.deskclock.data.Stopwatch.State.PAUSED;
+import static com.android.deskclock.data.Stopwatch.State.RESET;
+import static com.android.deskclock.data.Stopwatch.State.RUNNING;
+
+/**
+ * A read-only domain object representing a stopwatch.
+ */
+public final class Stopwatch {
+
+ public enum State { RESET, RUNNING, PAUSED }
+
+ /** The single, immutable instance of a reset stopwatch. */
+ private static final Stopwatch RESET_STOPWATCH = new Stopwatch(RESET, Long.MIN_VALUE, 0);
+
+ /** Current state of this stopwatch. */
+ private final State mState;
+
+ /** Elapsed time in ms the stopwatch was last started; {@link Long#MIN_VALUE} if not running. */
+ private final long mLastStartTime;
+
+ /** Elapsed time in ms this stopwatch has accumulated while running. */
+ private final long mAccumulatedTime;
+
+ Stopwatch(State state, long lastStartTime, long accumulatedTime) {
+ mState = state;
+ mLastStartTime = lastStartTime;
+ mAccumulatedTime = accumulatedTime;
+ }
+
+ public State getState() { return mState; }
+ public long getLastStartTime() { return mLastStartTime; }
+ public boolean isReset() { return mState == RESET; }
+ public boolean isPaused() { return mState == PAUSED; }
+ public boolean isRunning() { return mState == RUNNING; }
+
+ /**
+ * @return the total amount of time accumulated up to this moment
+ */
+ public long getTotalTime() {
+ if (mState != RUNNING) {
+ return mAccumulatedTime;
+ }
+
+ return mAccumulatedTime + (now() - mLastStartTime);
+ }
+
+ /**
+ * @return the amount of time accumulated up to the last time the stopwatch was started
+ */
+ long getAccumulatedTime() {
+ return mAccumulatedTime;
+ }
+
+ /**
+ * @return a copy of this stopwatch that is running
+ */
+ Stopwatch start() {
+ if (mState == RUNNING) {
+ return this;
+ }
+
+ return new Stopwatch(RUNNING, now(), mAccumulatedTime);
+ }
+
+ /**
+ * @return a copy of this stopwatch that is paused
+ */
+ Stopwatch pause() {
+ if (mState != RUNNING) {
+ return this;
+ }
+
+ final long accumulatedTime = mAccumulatedTime + (now() - mLastStartTime);
+ return new Stopwatch(PAUSED, Long.MIN_VALUE, accumulatedTime);
+ }
+
+ /**
+ * @return a copy of this stopwatch that is reset
+ */
+ Stopwatch reset() {
+ return RESET_STOPWATCH;
+ }
+
+ private static long now() {
+ return SystemClock.elapsedRealtime();
+ }
+} \ No newline at end of file
diff --git a/src/com/android/deskclock/data/StopwatchDAO.java b/src/com/android/deskclock/data/StopwatchDAO.java
new file mode 100644
index 000000000..a95cbe455
--- /dev/null
+++ b/src/com/android/deskclock/data/StopwatchDAO.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.data;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+
+import com.android.deskclock.data.Stopwatch.State;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import static com.android.deskclock.data.Stopwatch.State.RESET;
+
+/**
+ * This class encapsulates the transfer of data between {@link Stopwatch} and {@link Lap} domain
+ * objects and their permanent storage in {@link SharedPreferences}.
+ */
+final class StopwatchDAO {
+
+ // Key to a preference that stores the state of the stopwatch.
+ private static final String STATE = "sw_state";
+
+ // Key to a preference that stores the last start time of the stopwatch.
+ private static final String LAST_START_TIME = "sw_start_time";
+
+ // Key to a preference that stores the accumulated elapsed time of the stopwatch.
+ private static final String ACCUMULATED_TIME = "sw_accum_time";
+
+ // Key to a preference that stores the number of recorded laps.
+ private static final String LAP_COUNT = "sw_lap_num";
+
+ // Prefix for a key to a preference that stores accumulated time at the end of a particular lap.
+ private static final String LAP_ACCUMULATED_TIME = "sw_lap_time_";
+
+ // Lazily instantiated and cached for the life of the application.
+ private static SharedPreferences sPrefs;
+
+ private StopwatchDAO() {}
+
+ /**
+ * @return the stopwatch from permanent storage or a reset stopwatch if none exists
+ */
+ public static Stopwatch getStopwatch(Context context) {
+ final SharedPreferences prefs = getSharedPreferences(context);
+ final int stateIndex = prefs.getInt(STATE, RESET.ordinal());
+ final State state = State.values()[stateIndex];
+ final long lastStartTime = prefs.getLong(LAST_START_TIME, Long.MIN_VALUE);
+ final long accumulatedTime = prefs.getLong(ACCUMULATED_TIME, 0);
+ return new Stopwatch(state, lastStartTime, accumulatedTime);
+ }
+
+ /**
+ * @param stopwatch the last state of the stopwatch
+ */
+ public static void setStopwatch(Context context, Stopwatch stopwatch) {
+ final SharedPreferences prefs = getSharedPreferences(context);
+ final SharedPreferences.Editor editor = prefs.edit();
+
+ if (stopwatch.isReset()) {
+ editor.remove(STATE)
+ .remove(LAST_START_TIME)
+ .remove(ACCUMULATED_TIME);
+ } else {
+ editor.putInt(STATE, stopwatch.getState().ordinal())
+ .putLong(LAST_START_TIME, stopwatch.getLastStartTime())
+ .putLong(ACCUMULATED_TIME, stopwatch.getAccumulatedTime());
+ }
+
+ editor.apply();
+ }
+
+ /**
+ * @return a list of recorded laps for the stopwatch
+ */
+ public static List<Lap> getLaps(Context context) {
+ final SharedPreferences prefs = getSharedPreferences(context);
+
+ // Prepare the container to be filled with laps.
+ final int lapCount = prefs.getInt(LAP_COUNT, 0);
+ final List<Lap> laps = new ArrayList<>(lapCount);
+
+ long prevAccumulatedTime = 0;
+
+ // Lap numbers are 1-based and so the are corresponding shared preference keys.
+ for (int lapNumber = 1; lapNumber <= lapCount; lapNumber++) {
+ // Look up the accumulated time for the lap.
+ final String lapAccumulatedTimeKey = LAP_ACCUMULATED_TIME + lapNumber;
+ final long accumulatedTime = prefs.getLong(lapAccumulatedTimeKey, 0);
+
+ // Lap time is the delta between accumulated time of this lap and prior lap.
+ final long lapTime = accumulatedTime - prevAccumulatedTime;
+
+ // Create the lap instance from the data.
+ laps.add(new Lap(lapNumber, lapTime, accumulatedTime));
+
+ // Update the accumulated time of the previous lap.
+ prevAccumulatedTime = accumulatedTime;
+ }
+
+ // Laps are stored in the order they were recorded; display order is the reverse.
+ Collections.reverse(laps);
+
+ return laps;
+ }
+
+ /**
+ * @param newLapCount the number of laps including the new lap
+ * @param accumulatedTime the amount of time accumulate by the stopwatch at the end of the lap
+ */
+ public static void addLap(Context context, int newLapCount, long accumulatedTime) {
+ getSharedPreferences(context).edit()
+ .putInt(LAP_COUNT, newLapCount)
+ .putLong(LAP_ACCUMULATED_TIME + newLapCount, accumulatedTime)
+ .apply();
+ }
+
+ /**
+ * Remove the recorded laps for the stopwatch
+ */
+ public static void clearLaps(Context context) {
+ final SharedPreferences prefs = getSharedPreferences(context);
+ final SharedPreferences.Editor editor = prefs.edit();
+
+ final int lapCount = prefs.getInt(LAP_COUNT, 0);
+ for (int lapNumber = 1; lapNumber <= lapCount; lapNumber++) {
+ editor.remove(LAP_ACCUMULATED_TIME + lapNumber);
+ }
+ editor.remove(LAP_COUNT);
+
+ editor.apply();
+ }
+
+ private static SharedPreferences getSharedPreferences(Context context) {
+ if (sPrefs == null) {
+ sPrefs = PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext());
+ }
+
+ return sPrefs;
+ }
+} \ No newline at end of file
diff --git a/src/com/android/deskclock/data/StopwatchModel.java b/src/com/android/deskclock/data/StopwatchModel.java
new file mode 100644
index 000000000..fa7217286
--- /dev/null
+++ b/src/com/android/deskclock/data/StopwatchModel.java
@@ -0,0 +1,342 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.data;
+
+import android.app.Notification;
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.res.Resources;
+import android.os.SystemClock;
+import android.support.v4.app.NotificationCompat;
+import android.support.v4.app.NotificationManagerCompat;
+import android.widget.RemoteViews;
+
+import com.android.deskclock.DeskClock;
+import com.android.deskclock.HandleDeskClockApiCalls;
+import com.android.deskclock.R;
+import com.android.deskclock.stopwatch.StopwatchService;
+
+import java.util.Collections;
+import java.util.List;
+
+import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
+import static android.view.View.GONE;
+import static android.view.View.INVISIBLE;
+import static android.view.View.VISIBLE;
+
+/**
+ * All {@link Stopwatch} data is accessed via this model.
+ */
+final class StopwatchModel {
+
+ private final Context mContext;
+
+ /** The model from which notification data are fetched. */
+ private final NotificationModel mNotificationModel;
+
+ /** Used to create and destroy system notifications related to the stopwatch. */
+ private final NotificationManagerCompat mNotificationManager;
+
+ /** Update stopwatch notification when locale changes. */
+ private final BroadcastReceiver mLocaleChangedReceiver = new LocaleChangedReceiver();
+
+ /** The current state of the stopwatch. */
+ private Stopwatch mStopwatch;
+
+ /** A mutable copy of the recorded stopwatch laps. */
+ private List<Lap> mLaps;
+
+ StopwatchModel(Context context, NotificationModel notificationModel) {
+ mContext = context;
+ mNotificationModel = notificationModel;
+ mNotificationManager = NotificationManagerCompat.from(context);
+
+ // Update stopwatch notification when locale changes.
+ final IntentFilter localeBroadcastFilter = new IntentFilter(Intent.ACTION_LOCALE_CHANGED);
+ mContext.registerReceiver(mLocaleChangedReceiver, localeBroadcastFilter);
+ }
+
+ /**
+ * @return the current state of the stopwatch
+ */
+ Stopwatch getStopwatch() {
+ if (mStopwatch == null) {
+ mStopwatch = StopwatchDAO.getStopwatch(mContext);
+ }
+
+ return mStopwatch;
+ }
+
+ /**
+ * @param stopwatch the new state of the stopwatch
+ */
+ Stopwatch setStopwatch(Stopwatch stopwatch) {
+ if (mStopwatch != stopwatch) {
+ StopwatchDAO.setStopwatch(mContext, stopwatch);
+ mStopwatch = stopwatch;
+
+ // Refresh the stopwatch notification to reflect the latest stopwatch state.
+ if (!mNotificationModel.isApplicationInForeground()) {
+ updateNotification();
+ }
+ }
+
+ return stopwatch;
+ }
+
+ /**
+ * @return the laps recorded for this stopwatch
+ */
+ List<Lap> getLaps() {
+ return Collections.unmodifiableList(getMutableLaps());
+ }
+
+ /**
+ * @return a newly recorded lap completed now; {@code null} if no more laps can be added
+ */
+ Lap addLap() {
+ if (!canAddMoreLaps()) {
+ return null;
+ }
+
+ final long totalTime = getStopwatch().getTotalTime();
+ final List<Lap> laps = getMutableLaps();
+
+ final int lapNumber = laps.size() + 1;
+ StopwatchDAO.addLap(mContext, lapNumber, totalTime);
+
+ final long prevAccumulatedTime = laps.isEmpty() ? 0 : laps.get(0).getAccumulatedTime();
+ final long lapTime = totalTime - prevAccumulatedTime;
+
+ final Lap lap = new Lap(lapNumber, lapTime, totalTime);
+ laps.add(0, lap);
+
+ // Refresh the stopwatch notification to reflect the latest stopwatch state.
+ if (!mNotificationModel.isApplicationInForeground()) {
+ updateNotification();
+ }
+
+ return lap;
+ }
+
+ /**
+ * Clears the laps recorded for this stopwatch.
+ */
+ void clearLaps() {
+ StopwatchDAO.clearLaps(mContext);
+ getMutableLaps().clear();
+ }
+
+ /**
+ * @return {@code true} iff more laps can be recorded
+ */
+ boolean canAddMoreLaps() {
+ return getLaps().size() < 98;
+ }
+
+ /**
+ * @return the longest lap time of all recorded laps and the current lap
+ */
+ long getLongestLapTime() {
+ long maxLapTime = 0;
+
+ final List<Lap> laps = getLaps();
+ if (!laps.isEmpty()) {
+ // Compute the maximum lap time across all recorded laps.
+ for (Lap lap : getLaps()) {
+ maxLapTime = Math.max(maxLapTime, lap.getLapTime());
+ }
+
+ // Compare with the maximum lap time for the current lap.
+ final Stopwatch stopwatch = getStopwatch();
+ final long currentLapTime = stopwatch.getTotalTime() - laps.get(0).getAccumulatedTime();
+ maxLapTime = Math.max(maxLapTime, currentLapTime);
+ }
+
+ return maxLapTime;
+ }
+
+ /**
+ * @param time a point in time after the end of the last lap
+ * @return the elapsed time between the given {@code time} and the end of the previous lap
+ */
+ long getCurrentLapTime(long time) {
+ final Lap previousLap = getLaps().get(0);
+
+ final long last = previousLap.getAccumulatedTime();
+ final long lapTime = time - last;
+
+ if (lapTime < 0) {
+ final String message = String.format("time (%d) must exceed last lap (%d)", time, last);
+ throw new IllegalArgumentException(message);
+ }
+
+ return lapTime;
+ }
+
+ /**
+ * Updates the notification to reflect the latest state of the stopwatch and recorded laps.
+ */
+ void updateNotification() {
+ final Stopwatch stopwatch = getStopwatch();
+
+ // Notification should be hidden if the stopwatch has no time or the app is open.
+ if (stopwatch.isReset() || mNotificationModel.isApplicationInForeground()) {
+ mNotificationManager.cancel(mNotificationModel.getStopwatchNotificationId());
+ return;
+ }
+
+ // Intent to load the app when the notification is tapped.
+ final Intent showApp = new Intent(mContext, HandleDeskClockApiCalls.class)
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ .setAction(HandleDeskClockApiCalls.ACTION_SHOW_STOPWATCH)
+ .putExtra(HandleDeskClockApiCalls.EXTRA_FROM_NOTIFICATION, true)
+ .putExtra(DeskClock.SELECT_TAB_INTENT_EXTRA, DeskClock.STOPWATCH_TAB_INDEX);
+
+ final PendingIntent pendingShowApp = PendingIntent.getActivity(mContext, 0, showApp,
+ PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT);
+
+ // Compute some values required below.
+ final boolean running = stopwatch.isRunning();
+ final String pname = mContext.getPackageName();
+ final Resources res = mContext.getResources();
+ final long base = SystemClock.elapsedRealtime() - stopwatch.getTotalTime();
+
+ final RemoteViews collapsed = new RemoteViews(pname, R.layout.stopwatch_notif_collapsed);
+ collapsed.setChronometer(R.id.swn_collapsed_chronometer, base, null, running);
+ collapsed.setOnClickPendingIntent(R.id.swn_collapsed_hitspace, pendingShowApp);
+ collapsed.setImageViewResource(R.id.notification_icon, R.drawable.stat_notify_stopwatch);
+
+ final RemoteViews expanded = new RemoteViews(pname, R.layout.stopwatch_notif_expanded);
+ expanded.setChronometer(R.id.swn_expanded_chronometer, base, null, running);
+ expanded.setOnClickPendingIntent(R.id.swn_expanded_hitspace, pendingShowApp);
+ expanded.setImageViewResource(R.id.notification_icon, R.drawable.stat_notify_stopwatch);
+
+ final int leftButtonId = R.id.swn_left_button;
+ final int rightButtonId = R.id.swn_right_button;
+ if (running) {
+ // Left button: Pause
+ expanded.setTextViewText(leftButtonId, res.getText(R.string.sw_pause_button));
+ setTextViewDrawable(expanded, leftButtonId, R.drawable.ic_pause_24dp);
+ final Intent pause = new Intent(mContext, StopwatchService.class)
+ .setAction(HandleDeskClockApiCalls.ACTION_PAUSE_STOPWATCH)
+ .putExtra(HandleDeskClockApiCalls.EXTRA_FROM_NOTIFICATION, true);
+ expanded.setOnClickPendingIntent(leftButtonId, pendingServiceIntent(pause));
+
+ // Right button: Add Lap
+ if (canAddMoreLaps()) {
+ expanded.setTextViewText(rightButtonId, res.getText(R.string.sw_lap_button));
+ setTextViewDrawable(expanded, rightButtonId, R.drawable.ic_sw_lap_24dp);
+
+ final Intent lap = new Intent(mContext, StopwatchService.class)
+ .setAction(HandleDeskClockApiCalls.ACTION_LAP_STOPWATCH)
+ .putExtra(HandleDeskClockApiCalls.EXTRA_FROM_NOTIFICATION, true);
+ expanded.setOnClickPendingIntent(rightButtonId, pendingServiceIntent(lap));
+ expanded.setViewVisibility(rightButtonId, VISIBLE);
+ } else {
+ expanded.setViewVisibility(rightButtonId, INVISIBLE);
+ }
+
+ // Show the current lap number if any laps have been recorded.
+ final int lapCount = getLaps().size();
+ if (lapCount > 0) {
+ final int lapNumber = lapCount + 1;
+ final String lap = res.getString(R.string.sw_notification_lap_number, lapNumber);
+ collapsed.setTextViewText(R.id.swn_collapsed_laps, lap);
+ collapsed.setViewVisibility(R.id.swn_collapsed_laps, VISIBLE);
+ expanded.setTextViewText(R.id.swn_expanded_laps, lap);
+ expanded.setViewVisibility(R.id.swn_expanded_laps, VISIBLE);
+ } else {
+ collapsed.setViewVisibility(R.id.swn_collapsed_laps, GONE);
+ expanded.setViewVisibility(R.id.swn_expanded_laps, GONE);
+ }
+ } else {
+ // Left button: Start
+ expanded.setTextViewText(leftButtonId, res.getText(R.string.sw_start_button));
+ setTextViewDrawable(expanded, leftButtonId, R.drawable.ic_start_24dp);
+ final Intent start = new Intent(mContext, StopwatchService.class)
+ .setAction(HandleDeskClockApiCalls.ACTION_START_STOPWATCH)
+ .putExtra(HandleDeskClockApiCalls.EXTRA_FROM_NOTIFICATION, true);
+ expanded.setOnClickPendingIntent(leftButtonId, pendingServiceIntent(start));
+
+ // Right button: Reset (HandleDeskClockApiCalls will also bring forward the app)
+ expanded.setViewVisibility(rightButtonId, VISIBLE);
+ expanded.setTextViewText(rightButtonId, res.getText(R.string.sw_reset_button));
+ setTextViewDrawable(expanded, rightButtonId, R.drawable.ic_reset_24dp);
+ final Intent reset = new Intent(mContext, HandleDeskClockApiCalls.class)
+ .setAction(HandleDeskClockApiCalls.ACTION_RESET_STOPWATCH)
+ .putExtra(HandleDeskClockApiCalls.EXTRA_FROM_NOTIFICATION, true);
+ expanded.setOnClickPendingIntent(rightButtonId, pendingActivityIntent(reset));
+
+ // Indicate the stopwatch is paused.
+ collapsed.setTextViewText(R.id.swn_collapsed_laps, res.getString(R.string.swn_paused));
+ collapsed.setViewVisibility(R.id.swn_collapsed_laps, VISIBLE);
+ expanded.setTextViewText(R.id.swn_expanded_laps, res.getString(R.string.swn_paused));
+ expanded.setViewVisibility(R.id.swn_expanded_laps, VISIBLE);
+ }
+
+ // Swipe away will reset the stopwatch without bringing forward the app.
+ final Intent reset = new Intent(mContext, StopwatchService.class)
+ .setAction(HandleDeskClockApiCalls.ACTION_RESET_STOPWATCH)
+ .putExtra(HandleDeskClockApiCalls.EXTRA_FROM_NOTIFICATION, true);
+
+ final Notification notification = new NotificationCompat.Builder(mContext)
+ .setLocalOnly(true)
+ .setOngoing(running)
+ .setContent(collapsed)
+ .setAutoCancel(stopwatch.isPaused())
+ .setPriority(Notification.PRIORITY_MAX)
+ .setDeleteIntent(pendingServiceIntent(reset))
+ .setSmallIcon(R.drawable.ic_tab_stopwatch_activated)
+ .build();
+ notification.bigContentView = expanded;
+ mNotificationManager.notify(mNotificationModel.getStopwatchNotificationId(), notification);
+ }
+
+ private PendingIntent pendingServiceIntent(Intent intent) {
+ return PendingIntent.getService(mContext, 0, intent, FLAG_UPDATE_CURRENT);
+ }
+
+ private PendingIntent pendingActivityIntent(Intent intent) {
+ return PendingIntent.getActivity(mContext, 0, intent, FLAG_UPDATE_CURRENT);
+ }
+
+ private static void setTextViewDrawable(RemoteViews rv, int viewId, int drawableId) {
+ rv.setTextViewCompoundDrawablesRelative(viewId, drawableId, 0, 0, 0);
+ }
+
+ private List<Lap> getMutableLaps() {
+ if (mLaps == null) {
+ mLaps = StopwatchDAO.getLaps(mContext);
+ }
+
+ return mLaps;
+ }
+
+ /**
+ * Update the stopwatch notification in response to a locale change.
+ */
+ private final class LocaleChangedReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ updateNotification();
+ }
+ }
+} \ No newline at end of file
diff --git a/src/com/android/deskclock/stopwatch/LapsAdapter.java b/src/com/android/deskclock/stopwatch/LapsAdapter.java
new file mode 100644
index 000000000..c244f1754
--- /dev/null
+++ b/src/com/android/deskclock/stopwatch/LapsAdapter.java
@@ -0,0 +1,321 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.stopwatch;
+
+import android.content.Context;
+import android.support.annotation.VisibleForTesting;
+import android.text.format.DateUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.TextView;
+
+import com.android.deskclock.R;
+import com.android.deskclock.data.DataModel;
+import com.android.deskclock.data.Lap;
+import com.android.deskclock.data.Stopwatch;
+
+import java.text.DecimalFormatSymbols;
+import java.util.List;
+
+/**
+ * Displays a list of lap times in reverse order. That is, the newest lap is at the top, the oldest
+ * lap is at the bottom.
+ */
+class LapsAdapter extends BaseAdapter {
+
+ private final LayoutInflater mInflater;
+ private final Context mContext;
+
+ /** Used to determine when the time format for the lap time column has changed length. */
+ private int mLastFormattedLapTimeLength;
+
+ /** Used to determine when the time format for the total time column has changed length. */
+ private int mLastFormattedAccumulatedTimeLength;
+
+ public LapsAdapter(Context context) {
+ mContext = context;
+ mInflater = LayoutInflater.from(context);
+ }
+
+ /**
+ * After recording the first lap, there is always a "current lap" in progress.
+ *
+ * @return 0 if no laps are yet recorded; lap count + 1 if any laps exist
+ */
+ @Override
+ public int getCount() {
+ final int lapCount = getLaps().size();
+ final int currentLapCount = lapCount == 0 ? 0 : 1;
+ return currentLapCount + lapCount;
+ }
+
+ /**
+ * @return {@code null} for the current lap, the Lap object for all other laps
+ */
+ @Override
+ public Lap getItem(int position) {
+ return position == 0 ? null : getLaps().get(position - 1);
+ }
+
+ @Override
+ public boolean hasStableIds() {
+ return true;
+ }
+
+ @Override
+ public long getItemId(int position) {
+ // During transitions the adapter may be queried with invalid positions.
+ if (position >= getCount()) {
+ return -1;
+ }
+
+ final Lap lap = getItem(position);
+ return lap == null ? getLaps().size() + 1 : lap.getLapNumber();
+ }
+
+ @Override
+ public View getView(int position, View view, ViewGroup parent) {
+ if (getCount() == 0) {
+ return null;
+ }
+
+ // Inflate a new view if necessary.
+ if (view == null) {
+ view = mInflater.inflate(R.layout.lap_view, parent, false);
+ final TextView lapTime = (TextView) view.findViewById(R.id.lap_time);
+ final TextView lapNumber = (TextView) view.findViewById(R.id.lap_number);
+ final TextView accumulatedTime = (TextView) view.findViewById(R.id.lap_total);
+ view.setTag(new LapItemHolder(lapNumber, lapTime, accumulatedTime));
+ }
+
+ final long lapTime;
+ final int lapNumber;
+ final long totalTime;
+
+ // Lap will be null for the current lap.
+ final Lap lap = getItem(position);
+ if (lap != null) {
+ // For a recorded lap, merely extract the values to format.
+ lapTime = lap.getLapTime();
+ lapNumber = lap.getLapNumber();
+ totalTime = lap.getAccumulatedTime();
+ } else {
+ // For the current lap, compute times relative to the stopwatch.
+ totalTime = getStopwatch().getTotalTime();
+ lapTime = DataModel.getDataModel().getCurrentLapTime(totalTime);
+ lapNumber = getLaps().size() + 1;
+ }
+
+ // Bind data into the child views.
+ final LapItemHolder holder = (LapItemHolder) view.getTag();
+ holder.lapTime.setText(formatLapTime(lapTime));
+ holder.accumulatedTime.setText(formatAccumulatedTime(totalTime));
+ holder.lapNumber.setText(formatLapNumber(getLaps().size() + 1, lapNumber));
+
+ return view;
+ }
+
+ /**
+ * @return {@code false} to prevent the laps from responding to touch
+ */
+ @Override
+ public boolean isEnabled(int position) {
+ return false;
+ }
+
+ /**
+ * @param currentLapView the view that displays the current lap information
+ * @param lapTime time accumulated for the current lap
+ * @param accumulatedTime time accumulated for the current lap and all prior laps
+ */
+ void updateCurrentLap(View currentLapView, long lapTime, long accumulatedTime) {
+ final LapItemHolder holder = (LapItemHolder) currentLapView.getTag();
+ holder.lapTime.setText(formatLapTime(lapTime));
+ holder.accumulatedTime.setText(formatAccumulatedTime(accumulatedTime));
+ }
+
+ /**
+ * Record a new lap and update this adapter to include it.
+ *
+ * @return a newly cleared lap
+ */
+ Lap addLap() {
+ final Lap lap = DataModel.getDataModel().addLap();
+ notifyDataSetChanged();
+ return lap;
+ }
+
+ /**
+ * Remove all recorded laps and update this adapter.
+ */
+ void clearLaps() {
+ DataModel.getDataModel().clearLaps();
+
+ // Clear the computed time lengths related to the old recorded laps.
+ mLastFormattedLapTimeLength = 0;
+ mLastFormattedAccumulatedTimeLength = 0;
+
+ notifyDataSetChanged();
+ }
+
+ /**
+ * @return a formatted textual description of lap times and total time
+ */
+ String getShareText() {
+ final Stopwatch stopwatch = getStopwatch();
+ final long totalTime = stopwatch.getTotalTime();
+ final String stopwatchTime = formatTime(totalTime, totalTime, ":");
+
+ // Choose a size for the builder that is unlikely to be resized.
+ final StringBuilder builder = new StringBuilder(1000);
+
+ // Add the total elapsed time of the stopwatch.
+ builder.append(mContext.getString(R.string.sw_share_main, stopwatchTime));
+ builder.append("\n");
+
+ final List<Lap> laps = getLaps();
+ if (!laps.isEmpty()) {
+ // Add a header for lap times.
+ builder.append(mContext.getString(R.string.sw_share_laps));
+ builder.append("\n");
+
+ // Loop through the laps in the order they were recorded; reverse of display order.
+ for (int i = laps.size() - 1; i >= 0; i--) {
+ final Lap lap = laps.get(i);
+ builder.append(lap.getLapNumber());
+ builder.append(DecimalFormatSymbols.getInstance().getDecimalSeparator());
+ builder.append(' ');
+ builder.append(formatTime(lap.getLapTime(), lap.getLapTime(), " "));
+ builder.append("\n");
+ }
+ }
+
+ return builder.toString();
+ }
+
+ /**
+ * @param lapCount the total number of recorded laps
+ * @param lapNumber the number of the lap being formatted
+ * @return e.g. "# 7" if {@code lapCount} less than 10; "# 07" if {@code lapCount} is 10 or more
+ */
+ @VisibleForTesting
+ String formatLapNumber(int lapCount, int lapNumber) {
+ if (lapCount < 10) {
+ return String.format("# %d", lapNumber);
+ }
+
+ return String.format("# %02d", lapNumber);
+ }
+
+ /**
+ * @param maxTime the maximum amount of time; used to choose a time format
+ * @param time the time to format guaranteed not to exceed {@code maxTime}
+ * @param separator displayed between hours and minutes as well as minutes and seconds
+ * @return a formatted version of the time
+ */
+ @VisibleForTesting
+ String formatTime(long maxTime, long time, String separator) {
+ long hundredths, seconds, minutes, hours;
+ seconds = time / 1000;
+ hundredths = (time - seconds * 1000) / 10;
+ minutes = seconds / 60;
+ seconds = seconds - minutes * 60;
+ hours = minutes / 60;
+ minutes = minutes - hours * 60;
+
+ final char decimalSeparator = DecimalFormatSymbols.getInstance().getDecimalSeparator();
+
+ if (maxTime < 10 * DateUtils.MINUTE_IN_MILLIS) {
+ return String.format("%d%s%02d%s%02d",
+ minutes, separator, seconds, decimalSeparator, hundredths);
+ } else if (maxTime < 60 * DateUtils.MINUTE_IN_MILLIS) {
+ return String.format("%02d%s%02d%s%02d",
+ minutes, separator, seconds, decimalSeparator, hundredths);
+ } else if (maxTime < 10 * DateUtils.HOUR_IN_MILLIS) {
+ return String.format("%d%s%02d%s%02d%s%02d",
+ hours, separator, minutes, separator, seconds, decimalSeparator, hundredths);
+ } else if (maxTime < 100 * DateUtils.HOUR_IN_MILLIS) {
+ return String.format("%02d%s%02d%s%02d%s%02d",
+ hours, separator, minutes, separator, seconds, decimalSeparator, hundredths);
+ }
+
+ return String.format("%03d%s%02d%s%02d%s%02d",
+ hours, separator, minutes, separator, seconds, decimalSeparator, hundredths);
+ }
+
+ /**
+ * @param lapTime the lap time to be formatted
+ * @return a formatted version of the lap time
+ */
+ private String formatLapTime(long lapTime) {
+ // The longest lap dictates the way the given lapTime must be formatted.
+ final long longestLapTime = Math.max(DataModel.getDataModel().getLongestLapTime(), lapTime);
+ final String formattedLapTime = formatTime(longestLapTime, lapTime, " ");
+
+ // If the newly formatted lap time has altered the format, refresh all laps.
+ if (mLastFormattedLapTimeLength != formattedLapTime.length()) {
+ mLastFormattedLapTimeLength = formattedLapTime.length();
+ notifyDataSetChanged();
+ }
+
+ return formattedLapTime;
+ }
+
+ /**
+ * @param accumulatedTime the accumulated time to be formatted
+ * @return a formatted version of the accumulated time
+ */
+ private String formatAccumulatedTime(long accumulatedTime) {
+ final long totalTime = getStopwatch().getTotalTime();
+ final long longestAccumulatedTime = Math.max(totalTime, accumulatedTime);
+ final String formattedAccumulatedTime = formatTime(longestAccumulatedTime, accumulatedTime, " ");
+
+ // If the newly formatted accumulated time has altered the format, refresh all laps.
+ if (mLastFormattedAccumulatedTimeLength != formattedAccumulatedTime.length()) {
+ mLastFormattedAccumulatedTimeLength = formattedAccumulatedTime.length();
+ notifyDataSetChanged();
+ }
+
+ return formattedAccumulatedTime;
+ }
+
+ private Stopwatch getStopwatch() {
+ return DataModel.getDataModel().getStopwatch();
+ }
+
+ private List<Lap> getLaps() {
+ return DataModel.getDataModel().getLaps();
+ }
+
+ /**
+ * Cache the child views of each lap item view.
+ */
+ private static final class LapItemHolder {
+
+ private final TextView lapNumber;
+ private final TextView lapTime;
+ private final TextView accumulatedTime;
+
+ public LapItemHolder(TextView lapNumber, TextView lapTime, TextView accumulatedTime) {
+ this.lapNumber = lapNumber;
+ this.lapTime = lapTime;
+ this.accumulatedTime = accumulatedTime;
+ }
+ }
+} \ No newline at end of file
diff --git a/src/com/android/deskclock/stopwatch/StopwatchFragment.java b/src/com/android/deskclock/stopwatch/StopwatchFragment.java
index 8a373d7ed..efb620f9b 100644
--- a/src/com/android/deskclock/stopwatch/StopwatchFragment.java
+++ b/src/com/android/deskclock/stopwatch/StopwatchFragment.java
@@ -13,916 +13,537 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package com.android.deskclock.stopwatch;
-import android.animation.LayoutTransition;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
-import android.content.SharedPreferences;
-import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
-import android.content.res.Configuration;
import android.os.Bundle;
import android.os.PowerManager;
-import android.os.PowerManager.WakeLock;
-import android.preference.PreferenceManager;
-import android.text.format.DateUtils;
+import android.os.SystemClock;
+import android.transition.AutoTransition;
+import android.transition.Transition;
+import android.transition.TransitionManager;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityManager;
-import android.view.animation.Animation;
-import android.view.animation.TranslateAnimation;
-import android.widget.BaseAdapter;
import android.widget.ListView;
-import android.widget.TextView;
-import com.android.deskclock.CircleButtonsLayout;
-import com.android.deskclock.CircleTimerView;
import com.android.deskclock.DeskClock;
import com.android.deskclock.DeskClockFragment;
-import com.android.deskclock.HandleDeskClockApiCalls;
import com.android.deskclock.LogUtils;
import com.android.deskclock.R;
import com.android.deskclock.Utils;
+import com.android.deskclock.data.DataModel;
+import com.android.deskclock.data.Lap;
+import com.android.deskclock.data.Stopwatch;
import com.android.deskclock.events.Events;
import com.android.deskclock.timer.CountingTimerView;
-import java.util.ArrayList;
+import static android.content.Context.ACCESSIBILITY_SERVICE;
+import static android.content.Context.POWER_SERVICE;
+import static android.os.PowerManager.ON_AFTER_RELEASE;
+import static android.os.PowerManager.SCREEN_BRIGHT_WAKE_LOCK;
+import static android.view.View.GONE;
+import static android.view.View.INVISIBLE;
+import static android.view.View.VISIBLE;
-public class StopwatchFragment extends DeskClockFragment
- implements OnSharedPreferenceChangeListener {
- private static final boolean DEBUG = false;
+/**
+ * Fragment that shows the stopwatch and recorded laps.
+ */
+public final class StopwatchFragment extends DeskClockFragment {
private static final String TAG = "StopwatchFragment";
- private static final int STOPWATCH_REFRESH_INTERVAL_MILLIS = 25;
- // Lower the refresh rate in accessibility mode to give talkback time to catch up
- private static final int STOPWATCH_ACCESSIBILTY_REFRESH_INTERVAL_MILLIS = 500;
- int mState = Stopwatches.STOPWATCH_RESET;
+ /** Scheduled to update the stopwatch time and current lap time while stopwatch is running. */
+ private final Runnable mTimeUpdateRunnable = new TimeUpdateRunnable();
- // Stopwatch views that are accessed by the activity
- private CircleTimerView mTime;
- private CountingTimerView mTimeText;
- private ListView mLapsList;
- private WakeLock mWakeLock;
- private CircleButtonsLayout mCircleLayout;
+ /** Used to determine when talk back is on in order to lower the time update rate. */
+ private AccessibilityManager mAccessibilityManager;
- // Animation constants and objects
- private LayoutTransition mLayoutTransition;
- private LayoutTransition mCircleLayoutTransition;
- private View mStartSpace;
- private View mEndSpace;
- private View mBottomSpace;
- private boolean mSpacersUsed;
+ /** {@code true} while the {@link #mLapsList} is transitioning between shown and hidden. */
+ private boolean mLapsListIsTransitioning;
- private AccessibilityManager mAccessibilityManager;
+ /** The data source for {@link #mLapsList}. */
+ private LapsAdapter mLapsAdapter;
- // Used for calculating the time from the start taking into account the pause times
- long mStartTime = 0;
- long mAccumulatedTime = 0;
+ /** Draws the reference lap while the stopwatch is running. */
+ private StopwatchTimer mTime;
- // Lap information
- class Lap {
+ /** Displays the recorded lap times. */
+ private ListView mLapsList;
- Lap (long time, long total) {
- mLapTime = time;
- mTotalTime = total;
- }
- public long mLapTime;
- public long mTotalTime;
+ /** Displays the current stopwatch time. */
+ private CountingTimerView mTimeText;
- public void updateView() {
- View lapInfo = mLapsList.findViewWithTag(this);
- if (lapInfo != null) {
- mLapsAdapter.setTimeText(lapInfo, this);
- }
- }
- }
+ /** Held while the stopwatch is running and this fragment is forward to keep the screen on. */
+ private PowerManager.WakeLock mWakeLock;
- // Adapter for the ListView that shows the lap times.
- class LapsListAdapter extends BaseAdapter {
-
- private static final int VIEW_TYPE_LAP = 0;
- private static final int VIEW_TYPE_SPACE = 1;
- private static final int VIEW_TYPE_COUNT = 2;
-
- ArrayList<Lap> mLaps = new ArrayList<>();
- private final LayoutInflater mInflater;
- private final String[] mFormats;
- private final String[] mLapFormatSet;
- // Size of this array must match the size of formats
- private final long[] mThresholds = {
- 10 * DateUtils.MINUTE_IN_MILLIS, // < 10 minutes
- DateUtils.HOUR_IN_MILLIS, // < 1 hour
- 10 * DateUtils.HOUR_IN_MILLIS, // < 10 hours
- 100 * DateUtils.HOUR_IN_MILLIS, // < 100 hours
- 1000 * DateUtils.HOUR_IN_MILLIS // < 1000 hours
- };
- private int mLapIndex = 0;
- private int mTotalIndex = 0;
- private String mLapFormat;
-
- public LapsListAdapter(Context context) {
- mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
- mFormats = context.getResources().getStringArray(R.array.stopwatch_format_set);
- mLapFormatSet = context.getResources().getStringArray(R.array.sw_lap_number_set);
- updateLapFormat();
- }
+ /** The public no-arg constructor required by all fragments. */
+ public StopwatchFragment() {}
- @Override
- public boolean isEnabled(int position) {
- return false;
- }
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle state) {
+ mLapsAdapter = new LapsAdapter(getActivity());
- @Override
- public long getItemId(int position) {
- return position;
- }
+ final View v = inflater.inflate(R.layout.stopwatch_fragment, container, false);
+ mTime = (StopwatchTimer) v.findViewById(R.id.stopwatch_time);
+ mLapsList = (ListView) v.findViewById(R.id.laps_list);
+ mLapsList.setDividerHeight(0);
+ mLapsList.setAdapter(mLapsAdapter);
- @Override
- public int getItemViewType(int position) {
- return position < mLaps.size() ? VIEW_TYPE_LAP : VIEW_TYPE_SPACE;
- }
+ // Timer text serves as a virtual start/stop button.
+ mTimeText = (CountingTimerView) v.findViewById(R.id.stopwatch_time_text);
+ mTimeText.setVirtualButtonEnabled(true);
+ mTimeText.registerVirtualButtonAction(new ToggleStopwatchRunnable());
- @Override
- public int getViewTypeCount() {
- return VIEW_TYPE_COUNT;
- }
+ return v;
+ }
- @Override
- public View getView(int position, View convertView, ViewGroup parent) {
- if (getCount() == 0) {
- return null;
- }
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
- // Handle request for the Spacer at the end
- if (getItemViewType(position) == VIEW_TYPE_SPACE) {
- return convertView != null ? convertView
- : mInflater.inflate(R.layout.stopwatch_spacer, parent, false);
- }
+ mAccessibilityManager =
+ (AccessibilityManager) getActivity().getSystemService(ACCESSIBILITY_SERVICE);
+ }
- final View lapInfo = convertView != null ? convertView
- : mInflater.inflate(R.layout.lap_view, parent, false);
- Lap lap = getItem(position);
- lapInfo.setTag(lap);
+ @Override
+ public void onResume() {
+ super.onResume();
- TextView count = (TextView) lapInfo.findViewById(R.id.lap_number);
- count.setText(String.format(mLapFormat, mLaps.size() - position).toUpperCase());
- setTimeText(lapInfo, lap);
+ // Conservatively assume the data in the adapter has changed while the fragment was paused.
+ mLapsAdapter.notifyDataSetChanged();
- return lapInfo;
- }
+ // Update the state of the buttons.
+ setFabAppearance();
+ setLeftRightButtonAppearance();
- protected void setTimeText(View lapInfo, Lap lap) {
- TextView lapTime = (TextView)lapInfo.findViewById(R.id.lap_time);
- TextView totalTime = (TextView)lapInfo.findViewById(R.id.lap_total);
- lapTime.setText(Stopwatches.formatTimeText(lap.mLapTime, mFormats[mLapIndex]));
- totalTime.setText(Stopwatches.formatTimeText(lap.mTotalTime, mFormats[mTotalIndex]));
- }
+ // Draw the current stopwatch and lap times.
+ updateTime();
- @Override
- public int getCount() {
- // Add 1 for the spacer if list is not empty
- return mLaps.isEmpty() ? 0 : mLaps.size() + 1;
+ // Start updates if the stopwatch is running; blink text if it is paused.
+ switch (getStopwatch().getState()) {
+ case RUNNING:
+ acquireWakeLock();
+ mTime.startAnimation();
+ startUpdatingTime();
+ break;
+ case PAUSED:
+ mTimeText.blinkTimeStr(true);
+ break;
}
- @Override
- public Lap getItem(int position) {
- if (position >= mLaps.size()) {
- return null;
- }
- return mLaps.get(position);
- }
+ // Adjust the visibility of the list of laps.
+ showOrHideLaps(false);
- private void updateLapFormat() {
- // Note Stopwatches.MAX_LAPS < 100
- mLapFormat = mLapFormatSet[mLaps.size() < 10 ? 0 : 1];
- }
+ // Start watching for page changes away from this fragment.
+ getDeskClock().registerPageChangedListener(this);
- private void resetTimeFormats() {
- mLapIndex = mTotalIndex = 0;
+ // View is hidden in onPause, make sure it is visible now.
+ final View view = getView();
+ if (view != null) {
+ view.setVisibility(VISIBLE);
}
+ }
- /**
- * A lap is printed into two columns: the total time and the lap time. To make this print
- * as pretty as possible, multiple formats were created which minimize the width of the
- * print. As the total or lap time exceed the limit of that format, this code updates
- * the format used for the total and/or lap times.
- *
- * @param lap to measure
- * @return true if this lap exceeded either threshold and a format was updated.
- */
- public boolean updateTimeFormats(Lap lap) {
- boolean formatChanged = false;
- while (mLapIndex + 1 < mThresholds.length && lap.mLapTime >= mThresholds[mLapIndex]) {
- mLapIndex++;
- formatChanged = true;
- }
- while (mTotalIndex + 1 < mThresholds.length &&
- lap.mTotalTime >= mThresholds[mTotalIndex]) {
- mTotalIndex++;
- formatChanged = true;
- }
- return formatChanged;
- }
+ @Override
+ public void onPause() {
+ super.onPause();
- public void addLap(Lap l) {
- mLaps.add(0, l);
- // for efficiency caller also calls notifyDataSetChanged()
+ final View view = getView();
+ if (view != null) {
+ // Make the view invisible because when the lock screen is activated, the window stays
+ // active under it. Later, when unlocking the screen, we see the old stopwatch time for
+ // a fraction of a second.
+ getView().setVisibility(INVISIBLE);
}
- public void clearLaps() {
- mLaps.clear();
- updateLapFormat();
- resetTimeFormats();
- notifyDataSetChanged();
- }
+ // Stop all updates while the fragment is not visible.
+ mTime.stopAnimation();
+ stopUpdatingTime();
+ mTimeText.blinkTimeStr(false);
- // Helper function used to get the lap data to be stored in the activity's bundle
- public long [] getLapTimes() {
- int size = mLaps.size();
- if (size == 0) {
- return null;
- }
- long [] laps = new long[size];
- for (int i = 0; i < size; i ++) {
- laps[i] = mLaps.get(i).mTotalTime;
- }
- return laps;
- }
+ // Stop watching for page changes away from this fragment.
+ getDeskClock().unregisterPageChangedListener(this);
- // Helper function to restore adapter's data from the activity's bundle
- public void setLapTimes(long [] laps) {
- if (laps == null || laps.length == 0) {
- return;
- }
+ // Release the wake lock if it is currently held.
+ releaseWakeLock();
+ }
- int size = laps.length;
- mLaps.clear();
- for (long lap : laps) {
- mLaps.add(new Lap(lap, 0));
- }
- long totalTime = 0;
- for (int i = size -1; i >= 0; i --) {
- totalTime += laps[i];
- mLaps.get(i).mTotalTime = totalTime;
- updateTimeFormats(mLaps.get(i));
- }
- updateLapFormat();
- showLaps();
- notifyDataSetChanged();
+ @Override
+ public void onPageChanged(int page) {
+ if (page == DeskClock.STOPWATCH_TAB_INDEX && getStopwatch().isRunning()) {
+ acquireWakeLock();
+ } else {
+ releaseWakeLock();
}
}
- LapsListAdapter mLapsAdapter;
-
- public StopwatchFragment() {
+ @Override
+ public void onFabClick(View view) {
+ toggleStopwatchState();
}
- private void toggleStopwatchState() {
- long time = Utils.getTimeNow();
- Context context = getActivity().getApplicationContext();
- Intent intent = new Intent(context, StopwatchService.class);
- intent.putExtra(Stopwatches.MESSAGE_TIME, time);
- intent.putExtra(Stopwatches.SHOW_NOTIF, false);
- switch (mState) {
- case Stopwatches.STOPWATCH_RUNNING:
- // do stop
- long curTime = Utils.getTimeNow();
- mAccumulatedTime += (curTime - mStartTime);
- doStop();
- Events.sendStopwatchEvent(R.string.action_stop, R.string.label_deskclock);
-
- intent.setAction(HandleDeskClockApiCalls.ACTION_STOP_STOPWATCH);
- context.startService(intent);
- releaseWakeLock();
- break;
- case Stopwatches.STOPWATCH_RESET:
- case Stopwatches.STOPWATCH_STOPPED:
- // do start
- doStart(time);
- Events.sendStopwatchEvent(R.string.action_start, R.string.label_deskclock);
-
- intent.setAction(HandleDeskClockApiCalls.ACTION_START_STOPWATCH);
- context.startService(intent);
- acquireWakeLock();
+ @Override
+ public void onLeftButtonClick(View view) {
+ switch (getStopwatch().getState()) {
+ case RUNNING:
+ doAddLap();
break;
- default:
- LogUtils.wtf("Illegal state " + mState
- + " while pressing the right stopwatch button");
+ case PAUSED:
+ doReset();
break;
}
}
@Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container,
- Bundle savedInstanceState) {
- // Inflate the layout for this fragment
- ViewGroup v = (ViewGroup)inflater.inflate(R.layout.stopwatch_fragment, container, false);
-
- mTime = (CircleTimerView)v.findViewById(R.id.stopwatch_time);
- mTimeText = (CountingTimerView)v.findViewById(R.id.stopwatch_time_text);
- mLapsList = (ListView)v.findViewById(R.id.laps_list);
- mLapsList.setDividerHeight(0);
- mLapsAdapter = new LapsListAdapter(getActivity());
- mLapsList.setAdapter(mLapsAdapter);
-
- // Timer text serves as a virtual start/stop button.
- mTimeText.registerVirtualButtonAction(new Runnable() {
- @Override
- public void run() {
- toggleStopwatchState();
- }
- });
- mTimeText.setVirtualButtonEnabled(true);
-
- mCircleLayout = (CircleButtonsLayout)v.findViewById(R.id.stopwatch_circle);
- mCircleLayout.setCircleTimerViewIds(R.id.stopwatch_time, 0 /* stopwatchId */ ,
- 0 /* labelId */);
-
- // Animation setup
- mLayoutTransition = new LayoutTransition();
- mCircleLayoutTransition = new LayoutTransition();
-
- // The CircleButtonsLayout only needs to undertake location changes
- mCircleLayoutTransition.enableTransitionType(LayoutTransition.CHANGING);
- mCircleLayoutTransition.disableTransitionType(LayoutTransition.APPEARING);
- mCircleLayoutTransition.disableTransitionType(LayoutTransition.DISAPPEARING);
- mCircleLayoutTransition.disableTransitionType(LayoutTransition.CHANGE_APPEARING);
- mCircleLayoutTransition.disableTransitionType(LayoutTransition.CHANGE_DISAPPEARING);
- mCircleLayoutTransition.setAnimateParentHierarchy(false);
-
- // These spacers assist in keeping the size of CircleButtonsLayout constant
- mStartSpace = v.findViewById(R.id.start_space);
- mEndSpace = v.findViewById(R.id.end_space);
- mSpacersUsed = mStartSpace != null || mEndSpace != null;
-
- // Only applicable on portrait, only visible when there is no lap
- mBottomSpace = v.findViewById(R.id.bottom_space);
-
- // Listener to invoke extra animation within the laps-list
- mLayoutTransition.addTransitionListener(new LayoutTransition.TransitionListener() {
- @Override
- public void startTransition(LayoutTransition transition, ViewGroup container,
- View view, int transitionType) {
- if (view == mLapsList) {
- if (transitionType == LayoutTransition.DISAPPEARING) {
- if (DEBUG) LogUtils.v("StopwatchFragment.start laps-list disappearing");
- boolean shiftX = view.getResources().getConfiguration().orientation
- == Configuration.ORIENTATION_LANDSCAPE;
- int first = mLapsList.getFirstVisiblePosition();
- int last = mLapsList.getLastVisiblePosition();
- // Ensure index range will not cause a divide by zero
- if (last < first) {
- last = first;
- }
- long duration = transition.getDuration(LayoutTransition.DISAPPEARING);
- long offset = duration / (last - first + 1) / 5;
- for (int visibleIndex = first; visibleIndex <= last; visibleIndex++) {
- View lapView = mLapsList.getChildAt(visibleIndex - first);
- if (lapView != null) {
- float toXValue = shiftX ? 1.0f * (visibleIndex - first + 1) : 0;
- float toYValue = shiftX ? 0 : 4.0f * (visibleIndex - first + 1);
- TranslateAnimation animation = new TranslateAnimation(
- Animation.RELATIVE_TO_SELF, 0,
- Animation.RELATIVE_TO_SELF, toXValue,
- Animation.RELATIVE_TO_SELF, 0,
- Animation.RELATIVE_TO_SELF, toYValue);
- animation.setStartOffset((last - visibleIndex) * offset);
- animation.setDuration(duration);
- lapView.startAnimation(animation);
- }
- }
- }
- }
- }
+ public void onRightButtonClick(View view) {
+ doShare();
+ }
- @Override
- public void endTransition(LayoutTransition transition, ViewGroup container,
- View view, int transitionType) {
- if (transitionType == LayoutTransition.DISAPPEARING) {
- if (DEBUG) LogUtils.v("StopwatchFragment.end laps-list disappearing");
- int last = mLapsList.getLastVisiblePosition();
- for (int visibleIndex = mLapsList.getFirstVisiblePosition();
- visibleIndex <= last; visibleIndex++) {
- View lapView = mLapsList.getChildAt(visibleIndex);
- if (lapView != null) {
- Animation animation = lapView.getAnimation();
- if (animation != null) {
- animation.cancel();
- }
- }
- }
- }
- }
- });
+ @Override
+ public void setFabAppearance() {
+ if (mFab == null || getSelectedTab() != DeskClock.STOPWATCH_TAB_INDEX) {
+ return;
+ }
- return v;
+ if (getStopwatch().isRunning()) {
+ mFab.setImageResource(R.drawable.ic_pause_white_24dp);
+ mFab.setContentDescription(getString(R.string.sw_pause_button));
+ } else {
+ mFab.setImageResource(R.drawable.ic_start_white_24dp);
+ mFab.setContentDescription(getString(R.string.sw_start_button));
+ }
+ mFab.setVisibility(VISIBLE);
}
- /**
- * Make the final display setup.
- *
- * If the fragment is starting with an existing list of laps, shows the laps list and if the
- * spacers around the clock exist, hide them. If there are not laps at the start, hide the laps
- * list and show the clock spacers if they exist.
- */
@Override
- public void onStart() {
- super.onStart();
+ public void setLeftRightButtonAppearance() {
+ if (mLeftButton == null || mRightButton == null ||
+ getSelectedTab() != DeskClock.STOPWATCH_TAB_INDEX) {
+ return;
+ }
- boolean lapsVisible = mLapsAdapter.getCount() > 0;
+ mRightButton.setImageResource(R.drawable.ic_share);
+ mRightButton.setContentDescription(getString(R.string.sw_share_button));
- mLapsList.setVisibility(lapsVisible ? View.VISIBLE : View.GONE);
- if (mSpacersUsed) {
- showSpacerVisibility(lapsVisible);
+ switch (getStopwatch().getState()) {
+ case RESET:
+ mLeftButton.setEnabled(false);
+ mLeftButton.setVisibility(INVISIBLE);
+ mRightButton.setVisibility(INVISIBLE);
+ break;
+ case RUNNING:
+ mLeftButton.setImageResource(R.drawable.ic_lap);
+ mLeftButton.setContentDescription(getString(R.string.sw_lap_button));
+ mLeftButton.setEnabled(canRecordMoreLaps());
+ mLeftButton.setVisibility(canRecordMoreLaps() ? VISIBLE : INVISIBLE);
+ mRightButton.setVisibility(INVISIBLE);
+ break;
+ case PAUSED:
+ mLeftButton.setEnabled(true);
+ mLeftButton.setImageResource(R.drawable.ic_reset);
+ mLeftButton.setContentDescription(getString(R.string.sw_reset_button));
+ mLeftButton.setVisibility(VISIBLE);
+ mRightButton.setVisibility(VISIBLE);
+ break;
}
- showBottomSpacerVisibility(lapsVisible);
+ }
- ((ViewGroup)getView()).setLayoutTransition(mLayoutTransition);
- mCircleLayout.setLayoutTransition(mCircleLayoutTransition);
+ /**
+ * Start the stopwatch.
+ */
+ private void doStart() {
+ Events.sendStopwatchEvent(R.string.action_start, R.string.label_deskclock);
- mAccessibilityManager = (AccessibilityManager) getActivity().getSystemService(
- Context.ACCESSIBILITY_SERVICE);
- }
+ // Update the stopwatch state.
+ DataModel.getDataModel().startStopwatch();
- @Override
- public void onResume() {
- SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getActivity());
- prefs.registerOnSharedPreferenceChangeListener(this);
- readFromSharedPref(prefs);
- mTime.readFromSharedPref(prefs, "sw");
- mTime.postInvalidate();
+ // Start UI updates.
+ startUpdatingTime();
+ mTime.startAnimation();
+ mTimeText.blinkTimeStr(false);
+ // Update button states.
setFabAppearance();
setLeftRightButtonAppearance();
- mTimeText.setTime(mAccumulatedTime, true, true);
- if (mState == Stopwatches.STOPWATCH_RUNNING) {
- acquireWakeLock();
- startUpdateThread();
- } else if (mState == Stopwatches.STOPWATCH_STOPPED && mAccumulatedTime != 0) {
- mTimeText.blinkTimeStr(true);
- }
- showLaps();
- ((DeskClock)getActivity()).registerPageChangedListener(this);
- // View was hidden in onPause, make sure it is visible now.
- View v = getView();
- if (v != null) {
- v.setVisibility(View.VISIBLE);
- }
- super.onResume();
+
+ // Acquire the wake lock.
+ acquireWakeLock();
}
- @Override
- public void onPause() {
- if (mState == Stopwatches.STOPWATCH_RUNNING) {
- stopUpdateThread();
+ /**
+ * Pause the stopwatch.
+ */
+ private void doPause() {
+ Events.sendStopwatchEvent(R.string.action_pause, R.string.label_deskclock);
- // This is called because the lock screen was activated, the window stay
- // active under it and when we unlock the screen, we see the old time for
- // a fraction of a second.
- View v = getView();
- if (v != null) {
- v.setVisibility(View.INVISIBLE);
- }
- }
- // The stopwatch must keep running even if the user closes the app so save stopwatch state
- // in shared prefs
- SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getActivity());
- prefs.unregisterOnSharedPreferenceChangeListener(this);
- writeToSharedPref(prefs);
- mTime.writeToSharedPref(prefs, "sw");
- mTimeText.blinkTimeStr(false);
- ((DeskClock)getActivity()).unregisterPageChangedListener(this);
- releaseWakeLock();
- super.onPause();
- }
+ // Update the stopwatch state
+ DataModel.getDataModel().pauseStopwatch();
- @Override
- public void onPageChanged(int page) {
- if (page == DeskClock.STOPWATCH_TAB_INDEX && mState == Stopwatches.STOPWATCH_RUNNING) {
- acquireWakeLock();
- } else {
- releaseWakeLock();
- }
- }
+ // Redraw the paused stopwatch time.
+ updateTime();
- private void doStop() {
- if (DEBUG) LogUtils.v("StopwatchFragment.doStop");
- stopUpdateThread();
- mTime.pauseIntervalAnimation();
- mTimeText.setTime(mAccumulatedTime, true, true);
+ // Stop UI updates.
+ stopUpdatingTime();
+ mTime.stopAnimation();
mTimeText.blinkTimeStr(true);
- updateCurrentLap(mAccumulatedTime);
- mState = Stopwatches.STOPWATCH_STOPPED;
- setFabAppearance();
- setLeftRightButtonAppearance();
- }
- private void doStart(long time) {
- if (DEBUG) LogUtils.v("StopwatchFragment.doStart");
- mStartTime = time;
- startUpdateThread();
- mTimeText.blinkTimeStr(false);
- if (mTime.isAnimating()) {
- mTime.startIntervalAnimation();
- }
- mState = Stopwatches.STOPWATCH_RUNNING;
+ // Update button states.
setFabAppearance();
setLeftRightButtonAppearance();
- }
- private void doLap() {
- if (DEBUG) LogUtils.v("StopwatchFragment.doLap");
- showLaps();
- setFabAppearance();
- setLeftRightButtonAppearance();
+ // Release the wake lock.
+ releaseWakeLock();
}
+ /**
+ * Reset the stopwatch.
+ */
private void doReset() {
- if (DEBUG) LogUtils.v("StopwatchFragment.doReset");
- SharedPreferences prefs =
- PreferenceManager.getDefaultSharedPreferences(getActivity());
- Utils.clearSwSharedPref(prefs);
- mTime.clearSharedPref(prefs, "sw");
- mAccumulatedTime = 0;
- mLapsAdapter.clearLaps();
- showLaps();
- mTime.stopIntervalAnimation();
- mTime.reset();
- mTimeText.setTime(mAccumulatedTime, true, true);
+ Events.sendStopwatchEvent(R.string.action_reset, R.string.label_deskclock);
+
+ // Update the stopwatch state.
+ DataModel.getDataModel().resetStopwatch();
+
+ // Clear the laps.
+ showOrHideLaps(true);
+
+ // Clear the times.
+ mTime.postInvalidateOnAnimation();
+ mTimeText.setTime(0, true, true);
mTimeText.blinkTimeStr(false);
- mState = Stopwatches.STOPWATCH_RESET;
+
+ // Update button states.
setFabAppearance();
setLeftRightButtonAppearance();
+
+ // Release the wake lock.
+ releaseWakeLock();
}
- private void shareResults() {
+ /**
+ * Send stopwatch time and lap times to an external sharing application.
+ */
+ private void doShare() {
+ final String[] subjects = getResources().getStringArray(R.array.sw_share_strings);
+ final String subject = subjects[(int)(Math.random() * subjects.length)];
+ final String text = mLapsAdapter.getShareText();
+
+ final Intent shareIntent = new Intent(Intent.ACTION_SEND)
+ .addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET)
+ .putExtra(Intent.EXTRA_SUBJECT, subject)
+ .putExtra(Intent.EXTRA_TEXT, text)
+ .setType("text/plain");
+
final Context context = getActivity();
- final Intent shareIntent = new Intent(android.content.Intent.ACTION_SEND);
- shareIntent.setType("text/plain");
- shareIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
- shareIntent.putExtra(Intent.EXTRA_SUBJECT,
- Stopwatches.getShareTitle(context.getApplicationContext()));
- shareIntent.putExtra(Intent.EXTRA_TEXT, Stopwatches.buildShareResults(
- getActivity().getApplicationContext(), mTimeText.getTimeString(),
- getLapShareTimes(mLapsAdapter.getLapTimes())));
-
- final Intent launchIntent = Intent.createChooser(shareIntent,
- context.getString(R.string.sw_share_button));
+ final String title = context.getString(R.string.sw_share_button);
+ final Intent shareChooserIntent = Intent.createChooser(shareIntent, title);
try {
- context.startActivity(launchIntent);
- } catch (ActivityNotFoundException e) {
+ context.startActivity(shareChooserIntent);
+ } catch (ActivityNotFoundException anfe) {
LogUtils.e("No compatible receiver is found");
}
}
- /** Turn laps as they would be saved in prefs into format for sharing. **/
- private long[] getLapShareTimes(long[] input) {
- if (input == null) {
- return null;
- }
+ /**
+ * Record and add a new lap ending now.
+ */
+ private void doAddLap() {
+ Events.sendStopwatchEvent(R.string.action_lap, R.string.label_deskclock);
- int numLaps = input.length;
- long[] output = new long[numLaps];
- long prevLapElapsedTime = 0;
- for (int lap_i = numLaps - 1; lap_i >= 0; lap_i--) {
- long lap = input[lap_i];
- LogUtils.v("lap " + lap_i + ": " + lap);
- output[lap_i] = lap - prevLapElapsedTime;
- prevLapElapsedTime = lap;
+ // Record a new lap.
+ final Lap lap = mLapsAdapter.addLap();
+ if (lap == null) {
+ return;
}
- return output;
- }
- private boolean reachedMaxLaps() {
- return mLapsAdapter.getCount() >= Stopwatches.MAX_LAPS;
- }
+ // Update button states.
+ setLeftRightButtonAppearance();
- /***
- * Handle action when user presses the lap button
- * @param time - in hundredth of a second
- */
- private void addLapTime(long time) {
- // The total elapsed time
- final long curTime = time - mStartTime + mAccumulatedTime;
- int size = mLapsAdapter.getCount();
- if (size == 0) {
- // Create and add the first lap
- Lap firstLap = new Lap(curTime, curTime);
- mLapsAdapter.addLap(firstLap);
- // Create the first active lap
- mLapsAdapter.addLap(new Lap(0, curTime));
- // Update the interval on the clock and check the lap and total time formatting
- mTime.setIntervalTime(curTime);
- mLapsAdapter.updateTimeFormats(firstLap);
- } else {
- // Finish active lap
- final long lapTime = curTime - mLapsAdapter.getItem(1).mTotalTime;
- mLapsAdapter.getItem(0).mLapTime = lapTime;
- mLapsAdapter.getItem(0).mTotalTime = curTime;
- // Create a new active lap
- mLapsAdapter.addLap(new Lap(0, curTime));
- // Update marker on clock and check that formatting for the lap number
- mTime.setMarkerTime(lapTime);
- mLapsAdapter.updateLapFormat();
- }
- // Repaint the laps list
- mLapsAdapter.notifyDataSetChanged();
+ if (lap.getLapNumber() == 1) {
+ // Child views from prior lap sets hang around and blit to the screen when adding the
+ // first lap of the subsequent lap set. Remove those superfluous children here manually
+ // to ensure they aren't seen as the first lap is drawn.
+ mLapsList.removeAllViewsInLayout();
- // Start lap animation starting from the second lap
- mTime.stopIntervalAnimation();
- if (!reachedMaxLaps()) {
- mTime.startIntervalAnimation();
- }
- }
+ // Start animating the reference lap.
+ mTime.startAnimation();
+
+ // Recording the first lap transitions the UI to display the laps list.
+ showOrHideLaps(false);
- private void updateCurrentLap(long totalTime) {
- // There are either 0, 2 or more Laps in the list See {@link #addLapTime}
- if (mLapsAdapter.getCount() > 0) {
- Lap curLap = mLapsAdapter.getItem(0);
- curLap.mLapTime = totalTime - mLapsAdapter.getItem(1).mTotalTime;
- curLap.mTotalTime = totalTime;
- // If this lap has caused a change in the format for total and/or lap time, all of
- // the rows need a fresh print. The simplest way to refresh all of the rows is
- // calling notifyDataSetChanged.
- if (mLapsAdapter.updateTimeFormats(curLap)) {
- mLapsAdapter.notifyDataSetChanged();
+ } else {
+ if (mLapsList.getFirstVisiblePosition() > 0) {
+ // Ensure the newly added lap is visible on screen.
+ mLapsList.smoothScrollToPosition(0);
} else {
- curLap.updateView();
+ // Avoid nasty bugs in the Transition framework prior to L MR1.
+ // Without the transition, the new lap just appears on screen instantaneously.
+ if (Utils.isLMR1OrLater()) {
+ // Ignore updates to the current lap while adding the new recorded lap.
+ final Transition transition = new AutoTransition();
+ transition.excludeChildren(R.id.lap_view, true);
+
+ final ViewGroup sceneRoot = (ViewGroup) getView();
+ TransitionManager.beginDelayedTransition(sceneRoot, transition);
+ }
}
}
}
/**
- * Show or hide the laps-list
+ * Show or hide the list of laps.
*/
- private void showLaps() {
- if (DEBUG) LogUtils.v(String.format("StopwatchFragment.showLaps: count=%d",
- mLapsAdapter.getCount()));
+ private void showOrHideLaps(boolean clearLaps) {
+ final Transition transition = new AutoTransition()
+ .addListener(new Transition.TransitionListener() {
+ @Override
+ public void onTransitionStart(Transition transition) {
+ mLapsListIsTransitioning = true;
+ }
- boolean lapsVisible = mLapsAdapter.getCount() > 0;
+ @Override
+ public void onTransitionEnd(Transition transition) {
+ mLapsListIsTransitioning = false;
+ }
- // Layout change animations will start upon the first add/hide view. Temporarily disable
- // the layout transition animation for the spacers, make the changes, then re-enable
- // the animation for the add/hide laps-list
- if (mSpacersUsed) {
- ViewGroup rootView = (ViewGroup) getView();
- if (rootView != null) {
- rootView.setLayoutTransition(null);
+ @Override
+ public void onTransitionCancel(Transition transition) {
+ }
- showSpacerVisibility(lapsVisible);
+ @Override
+ public void onTransitionPause(Transition transition) {
+ }
- rootView.setLayoutTransition(mLayoutTransition);
- }
- }
+ @Override
+ public void onTransitionResume(Transition transition) {
+ }
+ });
- showBottomSpacerVisibility(lapsVisible);
+ final ViewGroup sceneRoot = (ViewGroup) getView();
+ TransitionManager.beginDelayedTransition(sceneRoot, transition);
- if (lapsVisible) {
- // There are laps - show the laps-list
- // No delay for the CircleButtonsLayout changes - start immediately so that the
- // circle has shifted before the laps-list starts appearing.
- mCircleLayoutTransition.setStartDelay(LayoutTransition.CHANGING, 0);
+ if (clearLaps) {
+ mLapsAdapter.clearLaps();
+ }
- mLapsList.setVisibility(View.VISIBLE);
- } else {
- // There are no laps - hide the laps list
+ final boolean lapsVisible = mLapsAdapter.getCount() > 0;
+ mLapsList.setVisibility(lapsVisible ? VISIBLE : GONE);
+ }
- // Delay the CircleButtonsLayout animation until after the laps-list disappears
- long startDelay = mLayoutTransition.getStartDelay(LayoutTransition.DISAPPEARING) +
- mLayoutTransition.getDuration(LayoutTransition.DISAPPEARING);
- mCircleLayoutTransition.setStartDelay(LayoutTransition.CHANGING, startDelay);
- mLapsList.setVisibility(View.GONE);
+ private void acquireWakeLock() {
+ if (mWakeLock == null) {
+ final PowerManager pm = (PowerManager) getActivity().getSystemService(POWER_SERVICE);
+ mWakeLock = pm.newWakeLock(SCREEN_BRIGHT_WAKE_LOCK | ON_AFTER_RELEASE, TAG);
+ mWakeLock.setReferenceCounted(false);
}
+ mWakeLock.acquire();
}
- private void showSpacerVisibility(boolean lapsVisible) {
- final int spacersVisibility = lapsVisible ? View.GONE : View.VISIBLE;
- if (mStartSpace != null) {
- mStartSpace.setVisibility(spacersVisibility);
- }
- if (mEndSpace != null) {
- mEndSpace.setVisibility(spacersVisibility);
+ private void releaseWakeLock() {
+ if (mWakeLock != null && mWakeLock.isHeld()) {
+ mWakeLock.release();
}
}
- private void showBottomSpacerVisibility(boolean lapsVisible) {
- if (mBottomSpace != null) {
- mBottomSpace.setVisibility(lapsVisible ? View.GONE : View.VISIBLE);
+ /**
+ * Either pause or start the stopwatch based on its current state.
+ */
+ private void toggleStopwatchState() {
+ if (getStopwatch().isRunning()) {
+ doPause();
+ } else {
+ doStart();
}
}
- private void startUpdateThread() {
- mTime.post(mTimeUpdateThread);
+ private Stopwatch getStopwatch() {
+ return DataModel.getDataModel().getStopwatch();
}
- private void stopUpdateThread() {
- mTime.removeCallbacks(mTimeUpdateThread);
+ private boolean canRecordMoreLaps() {
+ return DataModel.getDataModel().canAddMoreLaps();
}
- Runnable mTimeUpdateThread = new Runnable() {
- @Override
- public void run() {
- long curTime = Utils.getTimeNow();
- long totalTime = mAccumulatedTime + (curTime - mStartTime);
- if (mTime != null) {
- mTimeText.setTime(totalTime, true, true);
- }
- updateCurrentLap(totalTime);
- mTime.postDelayed(mTimeUpdateThread, mAccessibilityManager != null &&
- mAccessibilityManager.isTouchExplorationEnabled()
- ? STOPWATCH_ACCESSIBILTY_REFRESH_INTERVAL_MILLIS
- : STOPWATCH_REFRESH_INTERVAL_MILLIS);
- }
- };
-
- private void writeToSharedPref(SharedPreferences prefs) {
- SharedPreferences.Editor editor = prefs.edit();
- editor.putLong (Stopwatches.PREF_START_TIME, mStartTime);
- editor.putLong (Stopwatches.PREF_ACCUM_TIME, mAccumulatedTime);
- editor.putInt (Stopwatches.PREF_STATE, mState);
- if (mLapsAdapter != null) {
- long [] laps = mLapsAdapter.getLapTimes();
- if (laps != null) {
- editor.putInt (Stopwatches.PREF_LAP_NUM, laps.length);
- for (int i = 0; i < laps.length; i++) {
- String key = Stopwatches.PREF_LAP_TIME + Integer.toString(laps.length - i);
- editor.putLong (key, laps[i]);
- }
- }
- }
- if (mState == Stopwatches.STOPWATCH_RUNNING) {
- editor.putLong(Stopwatches.NOTIF_CLOCK_BASE, mStartTime-mAccumulatedTime);
- editor.putLong(Stopwatches.NOTIF_CLOCK_ELAPSED, -1);
- editor.putBoolean(Stopwatches.NOTIF_CLOCK_RUNNING, true);
- } else if (mState == Stopwatches.STOPWATCH_STOPPED) {
- editor.putLong(Stopwatches.NOTIF_CLOCK_ELAPSED, mAccumulatedTime);
- editor.putLong(Stopwatches.NOTIF_CLOCK_BASE, -1);
- editor.putBoolean(Stopwatches.NOTIF_CLOCK_RUNNING, false);
- } else if (mState == Stopwatches.STOPWATCH_RESET) {
- editor.remove(Stopwatches.NOTIF_CLOCK_BASE);
- editor.remove(Stopwatches.NOTIF_CLOCK_RUNNING);
- editor.remove(Stopwatches.NOTIF_CLOCK_ELAPSED);
- }
- editor.putBoolean(Stopwatches.PREF_UPDATE_CIRCLE, false);
- editor.apply();
+ /**
+ * Post the first runnable to update times within the UI. It will reschedule itself as needed.
+ */
+ private void startUpdatingTime() {
+ mTime.post(mTimeUpdateRunnable);
}
- private void readFromSharedPref(SharedPreferences prefs) {
- mStartTime = prefs.getLong(Stopwatches.PREF_START_TIME, 0);
- mAccumulatedTime = prefs.getLong(Stopwatches.PREF_ACCUM_TIME, 0);
- mState = prefs.getInt(Stopwatches.PREF_STATE, Stopwatches.STOPWATCH_RESET);
- int numLaps = prefs.getInt(Stopwatches.PREF_LAP_NUM, Stopwatches.STOPWATCH_RESET);
- if (mLapsAdapter != null) {
- long[] oldLaps = mLapsAdapter.getLapTimes();
- if (oldLaps == null || oldLaps.length < numLaps) {
- long[] laps = new long[numLaps];
- long prevLapElapsedTime = 0;
- for (int lap_i = 0; lap_i < numLaps; lap_i++) {
- String key = Stopwatches.PREF_LAP_TIME + Integer.toString(lap_i + 1);
- long lap = prefs.getLong(key, 0);
- laps[numLaps - lap_i - 1] = lap - prevLapElapsedTime;
- prevLapElapsedTime = lap;
- }
- mLapsAdapter.setLapTimes(laps);
- }
- }
- if (prefs.getBoolean(Stopwatches.PREF_UPDATE_CIRCLE, true)) {
- if (mState == Stopwatches.STOPWATCH_STOPPED) {
- doStop();
- } else if (mState == Stopwatches.STOPWATCH_RUNNING) {
- doStart(mStartTime);
- } else if (mState == Stopwatches.STOPWATCH_RESET) {
- doReset();
- }
- }
+ /**
+ * Remove the runnable that updates times within the UI.
+ */
+ private void stopUpdatingTime() {
+ mTime.removeCallbacks(mTimeUpdateRunnable);
}
- @Override
- public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
- if (prefs.equals(PreferenceManager.getDefaultSharedPreferences(getActivity()))) {
- if (! (key.equals(Stopwatches.PREF_LAP_NUM) ||
- key.startsWith(Stopwatches.PREF_LAP_TIME))) {
- readFromSharedPref(prefs);
- if (prefs.getBoolean(Stopwatches.PREF_UPDATE_CIRCLE, true)) {
- mTime.readFromSharedPref(prefs, "sw");
- }
+ /**
+ * Update all time displays based on a single snapshot of the stopwatch progress. This includes
+ * the stopwatch time drawn in the circle, the current lap time and the total elapsed time in
+ * the list of laps.
+ */
+ private void updateTime() {
+ // Compute the total time of the stopwatch.
+ final long totalTime = getStopwatch().getTotalTime();
+
+ // Update the total time display.
+ mTimeText.setTime(totalTime, true, true);
+
+ // Update the current lap if one exists and is visible on the screen.
+ final boolean lapsExist = mLapsAdapter.getCount() > 0;
+ final boolean currentLapIsVisible = mLapsList.getFirstVisiblePosition() == 0;
+ if (!mLapsListIsTransitioning && lapsExist && currentLapIsVisible) {
+ final View currentLapView = mLapsList.getChildAt(0);
+ if (currentLapView != null) {
+ // Compute the lap time using the total time.
+ final long lapTime = DataModel.getDataModel().getCurrentLapTime(totalTime);
+
+ // Update the current lap.
+ mLapsAdapter.updateCurrentLap(currentLapView, lapTime, totalTime);
}
}
}
- // Used to keeps screen on when stopwatch is running.
-
- private void acquireWakeLock() {
- if (mWakeLock == null) {
- final PowerManager pm =
- (PowerManager) getActivity().getSystemService(Context.POWER_SERVICE);
- mWakeLock = pm.newWakeLock(
- PowerManager.SCREEN_BRIGHT_WAKE_LOCK | PowerManager.ON_AFTER_RELEASE, TAG);
- mWakeLock.setReferenceCounted(false);
- }
- mWakeLock.acquire();
- }
-
- private void releaseWakeLock() {
- if (mWakeLock != null && mWakeLock.isHeld()) {
- mWakeLock.release();
- }
- }
+ /**
+ * This runnable periodically updates times throughout the UI. It stops these updates when the
+ * stopwatch is no longer running.
+ */
+ private final class TimeUpdateRunnable implements Runnable {
+ @Override
+ public void run() {
+ final long startTime = SystemClock.elapsedRealtime();
- @Override
- public void onFabClick(View view){
- toggleStopwatchState();
- }
+ updateTime();
- @Override
- public void onLeftButtonClick(View view) {
- final long time = Utils.getTimeNow();
- final Context context = getActivity().getApplicationContext();
- final Intent intent = new Intent(context, StopwatchService.class);
- intent.putExtra(Stopwatches.MESSAGE_TIME, time);
- intent.putExtra(Stopwatches.SHOW_NOTIF, false);
- switch (mState) {
- case Stopwatches.STOPWATCH_RUNNING:
- // Save lap time
- addLapTime(time);
- doLap();
- Events.sendStopwatchEvent(R.string.action_lap, R.string.label_deskclock);
-
- intent.setAction(HandleDeskClockApiCalls.ACTION_LAP_STOPWATCH);
- context.startService(intent);
- break;
- case Stopwatches.STOPWATCH_STOPPED:
- // do reset
- doReset();
- Events.sendStopwatchEvent(R.string.action_reset, R.string.label_deskclock);
+ if (getStopwatch().isRunning()) {
+ // The stopwatch is still running so execute this runnable again after a delay.
+ final boolean talkBackOn = mAccessibilityManager.isTouchExplorationEnabled();
- intent.setAction(HandleDeskClockApiCalls.ACTION_RESET_STOPWATCH);
- context.startService(intent);
- releaseWakeLock();
- break;
- default:
- // Happens in monkey tests
- LogUtils.i("Illegal state " + mState + " while pressing the left stopwatch button");
- break;
- }
- }
+ // Grant longer time between redraws when talk-back is on to let it catch up.
+ final int period = talkBackOn ? 500 : 25;
- @Override
- public void onRightButtonClick(View view) {
- shareResults();
- }
+ // Try to maintain a consistent period of time between redraws.
+ final long endTime = SystemClock.elapsedRealtime();
+ final long delay = Math.max(0, startTime + period - endTime);
- @Override
- public void setFabAppearance() {
- final DeskClock activity = (DeskClock) getActivity();
- if (mFab == null || activity.getSelectedTab() != DeskClock.STOPWATCH_TAB_INDEX) {
- return;
- }
- if (mState == Stopwatches.STOPWATCH_RUNNING) {
- mFab.setImageResource(R.drawable.ic_pause_white_24dp);
- mFab.setContentDescription(getString(R.string.sw_stop_button));
- } else {
- mFab.setImageResource(R.drawable.ic_start_white_24dp);
- mFab.setContentDescription(getString(R.string.sw_start_button));
+ mTime.postDelayed(this, delay);
+ }
}
- mFab.setVisibility(View.VISIBLE);
}
- @Override
- public void setLeftRightButtonAppearance() {
- final DeskClock activity = (DeskClock) getActivity();
- if (mLeftButton == null || mRightButton == null ||
- activity.getSelectedTab() != DeskClock.STOPWATCH_TAB_INDEX) {
- return;
- }
- mRightButton.setImageResource(R.drawable.ic_share);
- mRightButton.setContentDescription(getString(R.string.sw_share_button));
-
- switch (mState) {
- case Stopwatches.STOPWATCH_RESET:
- mLeftButton.setImageResource(R.drawable.ic_lap);
- mLeftButton.setContentDescription(getString(R.string.sw_lap_button));
- mLeftButton.setEnabled(false);
- mLeftButton.setVisibility(View.INVISIBLE);
- mRightButton.setVisibility(View.INVISIBLE);
- break;
- case Stopwatches.STOPWATCH_RUNNING:
- mLeftButton.setImageResource(R.drawable.ic_lap);
- mLeftButton.setContentDescription(getString(R.string.sw_lap_button));
- mLeftButton.setEnabled(!reachedMaxLaps());
- mLeftButton.setVisibility(View.VISIBLE);
- mRightButton.setVisibility(View.INVISIBLE);
- break;
- case Stopwatches.STOPWATCH_STOPPED:
- mLeftButton.setImageResource(R.drawable.ic_reset);
- mLeftButton.setContentDescription(getString(R.string.sw_reset_button));
- mLeftButton.setEnabled(true);
- mLeftButton.setVisibility(View.VISIBLE);
- mRightButton.setVisibility(View.VISIBLE);
- break;
+ /**
+ * Tapping the stopwatch text also toggles the stopwatch state, just like the fab.
+ */
+ private final class ToggleStopwatchRunnable implements Runnable {
+ @Override
+ public void run() {
+ toggleStopwatchState();
}
}
-}
+} \ No newline at end of file
diff --git a/src/com/android/deskclock/stopwatch/StopwatchService.java b/src/com/android/deskclock/stopwatch/StopwatchService.java
index 1ee387c90..a373e3a4d 100644
--- a/src/com/android/deskclock/stopwatch/StopwatchService.java
+++ b/src/com/android/deskclock/stopwatch/StopwatchService.java
@@ -1,39 +1,37 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
package com.android.deskclock.stopwatch;
-import android.app.Notification;
-import android.app.PendingIntent;
import android.app.Service;
-import android.content.Context;
import android.content.Intent;
-import android.content.SharedPreferences;
import android.os.IBinder;
-import android.preference.PreferenceManager;
-import android.support.v4.app.NotificationCompat;
-import android.support.v4.app.NotificationManagerCompat;
-import android.view.View;
-import android.widget.RemoteViews;
-import com.android.deskclock.CircleTimerView;
-import com.android.deskclock.DeskClock;
import com.android.deskclock.HandleDeskClockApiCalls;
import com.android.deskclock.R;
-import com.android.deskclock.Utils;
+import com.android.deskclock.data.DataModel;
+import com.android.deskclock.events.Events;
/**
- * TODO: Insert description here. (generated by sblitz)
+ * This service exists primarily to allow the stopwatch notification to alter the state of the
+ * stopwatch without disturbing the notification shade. If an activity were used instead (even one
+ * that is not displayed) the notification manager implicitly closes the notification shade which
+ * clashes with the use case of starting/stopping/lapping the stopwatch without interruption.
*/
-public class StopwatchService extends Service {
- // Member fields
- private int mNumLaps;
- private long mElapsedTime;
- private long mStartTime;
- private boolean mLoadApp;
- private NotificationManagerCompat mNotificationManager;
-
- // Constants for intent information
- // Make this a large number to avoid the alarm ID's which seem to be 1, 2, ...
- // Must also be different than TimerReceiver.IN_USE_NOTIFICATION_ID
- private static final int NOTIFICATION_ID = Integer.MAX_VALUE - 1;
+public final class StopwatchService extends Service {
@Override
public IBinder onBind(Intent intent) {
@@ -41,423 +39,42 @@ public class StopwatchService extends Service {
}
@Override
- public void onCreate() {
- mNumLaps = 0;
- mElapsedTime = 0;
- mStartTime = 0;
- mLoadApp = false;
- mNotificationManager = NotificationManagerCompat.from(this);
- }
-
- @Override
public int onStartCommand(Intent intent, int flags, int startId) {
- if (intent == null) {
- return Service.START_NOT_STICKY;
- }
-
- if (mStartTime == 0 || mElapsedTime == 0 || mNumLaps == 0) {
- // May not have the most recent values.
- readFromSharedPrefs();
- }
-
- String actionType = intent.getAction();
- long actionTime = intent.getLongExtra(Stopwatches.MESSAGE_TIME, Utils.getTimeNow());
- boolean showNotif = intent.getBooleanExtra(Stopwatches.SHOW_NOTIF, true);
- // Update the stopwatch circle when the app is open or is being opened.
- boolean updateCircle = !showNotif
- || intent.getAction().equals(Stopwatches.RESET_AND_LAUNCH_STOPWATCH);
- switch(actionType) {
- case HandleDeskClockApiCalls.ACTION_START_STOPWATCH:
- SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this) ;
- prefs.edit().putBoolean(Stopwatches.NOTIF_CLOCK_RUNNING, true).apply();
-
- mStartTime = actionTime;
- writeSharedPrefsStarted(mStartTime, updateCircle);
- if (showNotif) {
- setNotification(mStartTime - mElapsedTime, true, mNumLaps);
- } else {
- saveNotification(mStartTime - mElapsedTime, true, mNumLaps);
- }
- break;
- case HandleDeskClockApiCalls.ACTION_LAP_STOPWATCH:
- mNumLaps++;
- long lapTimeElapsed = actionTime - mStartTime + mElapsedTime;
- writeSharedPrefsLap(lapTimeElapsed, updateCircle);
- if (showNotif) {
- setNotification(mStartTime - mElapsedTime, true, mNumLaps);
- } else {
- saveNotification(mStartTime - mElapsedTime, true, mNumLaps);
+ final boolean fromNotification =
+ intent.getBooleanExtra(HandleDeskClockApiCalls.EXTRA_FROM_NOTIFICATION, false);
+
+ switch (intent.getAction()) {
+ case HandleDeskClockApiCalls.ACTION_START_STOPWATCH: {
+ DataModel.getDataModel().startStopwatch();
+ if (fromNotification) {
+ Events.sendStopwatchEvent(R.string.action_start, R.string.label_notification);
}
break;
- case HandleDeskClockApiCalls.ACTION_STOP_STOPWATCH:
- prefs = PreferenceManager.getDefaultSharedPreferences(this);
- prefs.edit().putBoolean(Stopwatches.NOTIF_CLOCK_RUNNING, false).apply();
-
- mElapsedTime = mElapsedTime + (actionTime - mStartTime);
- writeSharedPrefsStopped(mElapsedTime, updateCircle);
- if (showNotif) {
- setNotification(actionTime - mElapsedTime, false, mNumLaps);
- } else {
- saveNotification(mElapsedTime, false, mNumLaps);
+ }
+ case HandleDeskClockApiCalls.ACTION_PAUSE_STOPWATCH: {
+ DataModel.getDataModel().pauseStopwatch();
+ if (fromNotification) {
+ Events.sendStopwatchEvent(R.string.action_pause, R.string.label_notification);
}
break;
- case HandleDeskClockApiCalls.ACTION_RESET_STOPWATCH:
- mLoadApp = false;
- writeSharedPrefsReset(updateCircle);
- clearSavedNotification();
- stopSelf();
- break;
- case Stopwatches.RESET_AND_LAUNCH_STOPWATCH:
- mLoadApp = true;
- writeSharedPrefsReset(updateCircle);
- clearSavedNotification();
- closeNotificationShade();
- stopSelf();
- break;
- case Stopwatches.SHARE_STOPWATCH:
- if (mElapsedTime > 0) {
- closeNotificationShade();
- Intent shareIntent = new Intent(android.content.Intent.ACTION_SEND);
- shareIntent.setType("text/plain");
- shareIntent.putExtra(Intent.EXTRA_SUBJECT, Stopwatches.getShareTitle(
- getApplicationContext()));
- shareIntent.putExtra(Intent.EXTRA_TEXT, Stopwatches.buildShareResults(
- getApplicationContext(), mElapsedTime, readLapsFromPrefs()));
- Intent chooserIntent = Intent.createChooser(shareIntent, null);
- chooserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- getApplication().startActivity(chooserIntent);
+ }
+ case HandleDeskClockApiCalls.ACTION_LAP_STOPWATCH: {
+ DataModel.getDataModel().addLap();
+ if (fromNotification) {
+ Events.sendStopwatchEvent(R.string.action_lap, R.string.label_notification);
}
break;
- case Stopwatches.SHOW_NOTIF:
- // SHOW_NOTIF sent from the DeskClock.onPause
- // If a notification is not displayed, this service's work is over
- if (!showSavedNotification()) {
- stopSelf();
+ }
+ case HandleDeskClockApiCalls.ACTION_RESET_STOPWATCH: {
+ DataModel.getDataModel().clearLaps();
+ DataModel.getDataModel().resetStopwatch();
+ if (fromNotification) {
+ Events.sendStopwatchEvent(R.string.action_reset, R.string.label_notification);
}
break;
- case Stopwatches.KILL_NOTIF:
- mNotificationManager.cancel(NOTIFICATION_ID);
- break;
-
- }
-
- // We want this service to continue running until it is explicitly
- // stopped, so return sticky.
- return START_STICKY;
- }
-
- @Override
- public void onDestroy() {
- mNotificationManager.cancel(NOTIFICATION_ID);
- clearSavedNotification();
- mNumLaps = 0;
- mElapsedTime = 0;
- mStartTime = 0;
- if (mLoadApp) {
- Intent activityIntent = new Intent(getApplicationContext(), DeskClock.class);
- activityIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- activityIntent.putExtra(
- DeskClock.SELECT_TAB_INTENT_EXTRA, DeskClock.STOPWATCH_TAB_INDEX);
- startActivity(activityIntent);
- mLoadApp = false;
- }
- }
-
- private void setNotification(long clockBaseTime, boolean clockRunning, int numLaps) {
- Context context = getApplicationContext();
- // Intent to load the app for a non-button click.
- Intent intent = new Intent(context, DeskClock.class);
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- intent.putExtra(DeskClock.SELECT_TAB_INTENT_EXTRA, DeskClock.STOPWATCH_TAB_INDEX);
- // add category to distinguish between stopwatch intents and timer intents
- intent.addCategory("stopwatch");
- PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent,
- PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT);
-
- // Set up remoteviews for the notification.
- RemoteViews remoteViewsCollapsed = new RemoteViews(getPackageName(),
- R.layout.stopwatch_notif_collapsed);
- remoteViewsCollapsed.setOnClickPendingIntent(R.id.swn_collapsed_hitspace, pendingIntent);
- remoteViewsCollapsed.setChronometer(
- R.id.swn_collapsed_chronometer, clockBaseTime, null, clockRunning);
- remoteViewsCollapsed.
- setImageViewResource(R.id.notification_icon, R.drawable.stat_notify_stopwatch);
- RemoteViews remoteViewsExpanded = new RemoteViews(getPackageName(),
- R.layout.stopwatch_notif_expanded);
- remoteViewsExpanded.setOnClickPendingIntent(R.id.swn_expanded_hitspace, pendingIntent);
- remoteViewsExpanded.setChronometer(
- R.id.swn_expanded_chronometer, clockBaseTime, null, clockRunning);
- remoteViewsExpanded.
- setImageViewResource(R.id.notification_icon, R.drawable.stat_notify_stopwatch);
-
- if (clockRunning) {
- // Right button: lap
- remoteViewsExpanded.setTextViewText(
- R.id.swn_right_button, getResources().getText(R.string.sw_lap_button));
- Intent rightButtonIntent = new Intent(context, StopwatchService.class);
- rightButtonIntent.setAction(HandleDeskClockApiCalls.ACTION_LAP_STOPWATCH);
- remoteViewsExpanded.setOnClickPendingIntent(R.id.swn_right_button,
- PendingIntent.getService(context, 0, rightButtonIntent, 0));
- remoteViewsExpanded.
- setTextViewCompoundDrawablesRelative(R.id.swn_right_button,
- R.drawable.ic_lap_24dp, 0, 0, 0);
-
- // Left button: stop clock
- remoteViewsExpanded.setTextViewText(
- R.id.swn_left_button, getResources().getText(R.string.sw_stop_button));
- Intent leftButtonIntent = new Intent(context, StopwatchService.class);
- leftButtonIntent.setAction(HandleDeskClockApiCalls.ACTION_STOP_STOPWATCH);
- remoteViewsExpanded.setOnClickPendingIntent(R.id.swn_left_button,
- PendingIntent.getService(context, 0, leftButtonIntent, 0));
- remoteViewsExpanded.
- setTextViewCompoundDrawablesRelative(R.id.swn_left_button,
- R.drawable.ic_sw_stop_24dp, 0, 0, 0);
-
- // Show the laps if applicable.
- if (numLaps > 0) {
- String lapText = String.format(
- context.getString(R.string.sw_notification_lap_number), numLaps);
- remoteViewsCollapsed.setTextViewText(R.id.swn_collapsed_laps, lapText);
- remoteViewsCollapsed.setViewVisibility(R.id.swn_collapsed_laps, View.VISIBLE);
- remoteViewsExpanded.setTextViewText(R.id.swn_expanded_laps, lapText);
- remoteViewsExpanded.setViewVisibility(R.id.swn_expanded_laps, View.VISIBLE);
- } else {
- remoteViewsCollapsed.setViewVisibility(R.id.swn_collapsed_laps, View.GONE);
- remoteViewsExpanded.setViewVisibility(R.id.swn_expanded_laps, View.GONE);
}
- } else {
- // Right button: reset clock
- remoteViewsExpanded.setTextViewText(
- R.id.swn_right_button, getResources().getText(R.string.sw_reset_button));
- Intent rightButtonIntent = new Intent(context, StopwatchService.class);
- rightButtonIntent.setAction(Stopwatches.RESET_AND_LAUNCH_STOPWATCH);
- remoteViewsExpanded.setOnClickPendingIntent(R.id.swn_right_button,
- PendingIntent.getService(context, 0, rightButtonIntent, 0));
- remoteViewsExpanded.
- setTextViewCompoundDrawablesRelative(R.id.swn_right_button,
- R.drawable.ic_sw_reset_24dp, 0, 0, 0);
-
- // Left button: start clock
- remoteViewsExpanded.setTextViewText(
- R.id.swn_left_button, getResources().getText(R.string.sw_start_button));
- Intent leftButtonIntent = new Intent(context, StopwatchService.class);
- leftButtonIntent.setAction(HandleDeskClockApiCalls.ACTION_START_STOPWATCH);
- remoteViewsExpanded.setOnClickPendingIntent(R.id.swn_left_button,
- PendingIntent.getService(context, 0, leftButtonIntent, 0));
- remoteViewsExpanded.
- setTextViewCompoundDrawablesRelative(R.id.swn_left_button,
- R.drawable.ic_sw_start_24dp, 0, 0, 0);
-
- // Show stopped string.
- remoteViewsCollapsed.
- setTextViewText(R.id.swn_collapsed_laps, getString(R.string.swn_stopped));
- remoteViewsCollapsed.setViewVisibility(R.id.swn_collapsed_laps, View.VISIBLE);
- remoteViewsExpanded.
- setTextViewText(R.id.swn_expanded_laps, getString(R.string.swn_stopped));
- remoteViewsExpanded.setViewVisibility(R.id.swn_expanded_laps, View.VISIBLE);
}
- Intent dismissIntent = new Intent(context, StopwatchService.class);
- dismissIntent.setAction(HandleDeskClockApiCalls.ACTION_RESET_STOPWATCH);
-
- Notification notification = new NotificationCompat.Builder(context)
- .setAutoCancel(!clockRunning)
- .setContent(remoteViewsCollapsed)
- .setOngoing(clockRunning)
- .setDeleteIntent(PendingIntent.getService(context, 0, dismissIntent, 0))
- .setSmallIcon(R.drawable.ic_tab_stopwatch_activated)
- .setPriority(Notification.PRIORITY_MAX)
- .setLocalOnly(true)
- .build();
- notification.bigContentView = remoteViewsExpanded;
- mNotificationManager.notify(NOTIFICATION_ID, notification);
- }
-
- /** Save the notification to be shown when the app is closed. **/
- private void saveNotification(long clockTime, boolean clockRunning, int numLaps) {
- SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(
- getApplicationContext());
- SharedPreferences.Editor editor = prefs.edit();
- if (clockRunning) {
- editor.putLong(Stopwatches.NOTIF_CLOCK_BASE, clockTime);
- editor.putLong(Stopwatches.NOTIF_CLOCK_ELAPSED, -1);
- editor.putBoolean(Stopwatches.NOTIF_CLOCK_RUNNING, true);
- } else {
- editor.putLong(Stopwatches.NOTIF_CLOCK_ELAPSED, clockTime);
- editor.putLong(Stopwatches.NOTIF_CLOCK_BASE, -1);
- editor.putBoolean(Stopwatches.NOTIF_CLOCK_RUNNING, false);
- }
- editor.putBoolean(Stopwatches.PREF_UPDATE_CIRCLE, false);
- editor.apply();
- }
-
- /** Show the most recently saved notification. **/
- private boolean showSavedNotification() {
- SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(
- getApplicationContext());
- long clockBaseTime = prefs.getLong(Stopwatches.NOTIF_CLOCK_BASE, -1);
- long clockElapsedTime = prefs.getLong(Stopwatches.NOTIF_CLOCK_ELAPSED, -1);
- boolean clockRunning = prefs.getBoolean(Stopwatches.NOTIF_CLOCK_RUNNING, false);
- int numLaps = prefs.getInt(Stopwatches.PREF_LAP_NUM, -1);
- if (clockBaseTime == -1) {
- if (clockElapsedTime == -1) {
- return false;
- } else {
- // We don't have a clock base time, so the clock is stopped.
- // Use the elapsed time to figure out what time to show.
- mElapsedTime = clockElapsedTime;
- clockBaseTime = Utils.getTimeNow() - clockElapsedTime;
- }
- }
- setNotification(clockBaseTime, clockRunning, numLaps);
- return true;
- }
-
- private void clearSavedNotification() {
- SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(
- getApplicationContext());
- SharedPreferences.Editor editor = prefs.edit();
- editor.remove(Stopwatches.NOTIF_CLOCK_BASE);
- editor.remove(Stopwatches.NOTIF_CLOCK_RUNNING);
- editor.remove(Stopwatches.NOTIF_CLOCK_ELAPSED);
- editor.apply();
- }
-
- private void closeNotificationShade() {
- Intent intent = new Intent();
- intent.setAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
- sendBroadcast(intent);
- }
-
- private void readFromSharedPrefs() {
- SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(
- getApplicationContext());
- mStartTime = prefs.getLong(Stopwatches.PREF_START_TIME, 0);
- mElapsedTime = prefs.getLong(Stopwatches.PREF_ACCUM_TIME, 0);
- mNumLaps = prefs.getInt(Stopwatches.PREF_LAP_NUM, Stopwatches.STOPWATCH_RESET);
- }
-
- private long[] readLapsFromPrefs() {
- SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(
- getApplicationContext());
- int numLaps = prefs.getInt(Stopwatches.PREF_LAP_NUM, Stopwatches.STOPWATCH_RESET);
- long[] laps = new long[numLaps];
- long prevLapElapsedTime = 0;
- for (int lap_i = 0; lap_i < numLaps; lap_i++) {
- String key = Stopwatches.PREF_LAP_TIME + Integer.toString(lap_i + 1);
- long lap = prefs.getLong(key, 0);
- if (lap == prevLapElapsedTime && lap_i == numLaps - 1) {
- lap = mElapsedTime;
- }
- laps[numLaps - lap_i - 1] = lap - prevLapElapsedTime;
- prevLapElapsedTime = lap;
- }
- return laps;
- }
-
- private void writeToSharedPrefs(Long startTime, Long lapTimeElapsed, Long elapsedTime,
- Integer state, boolean updateCircle) {
- SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(
- getApplicationContext());
- SharedPreferences.Editor editor = prefs.edit();
- if (startTime != null) {
- editor.putLong(Stopwatches.PREF_START_TIME, startTime);
- mStartTime = startTime;
- }
- if (lapTimeElapsed != null) {
- int numLaps = prefs.getInt(Stopwatches.PREF_LAP_NUM, 0);
- if (numLaps == 0) {
- mNumLaps++;
- numLaps++;
- }
- editor.putLong(Stopwatches.PREF_LAP_TIME + Integer.toString(numLaps), lapTimeElapsed);
- numLaps++;
- editor.putLong(Stopwatches.PREF_LAP_TIME + Integer.toString(numLaps), lapTimeElapsed);
- editor.putInt(Stopwatches.PREF_LAP_NUM, numLaps);
- }
- if (elapsedTime != null) {
- editor.putLong(Stopwatches.PREF_ACCUM_TIME, elapsedTime);
- mElapsedTime = elapsedTime;
- }
- if (state != null) {
- if (state == Stopwatches.STOPWATCH_RESET) {
- editor.putInt(Stopwatches.PREF_STATE, Stopwatches.STOPWATCH_RESET);
- } else if (state == Stopwatches.STOPWATCH_RUNNING) {
- editor.putInt(Stopwatches.PREF_STATE, Stopwatches.STOPWATCH_RUNNING);
- } else if (state == Stopwatches.STOPWATCH_STOPPED) {
- editor.putInt(Stopwatches.PREF_STATE, Stopwatches.STOPWATCH_STOPPED);
- }
- }
- editor.putBoolean(Stopwatches.PREF_UPDATE_CIRCLE, updateCircle);
- editor.apply();
- }
-
- private void writeSharedPrefsStarted(long startTime, boolean updateCircle) {
- writeToSharedPrefs(startTime, null, null, Stopwatches.STOPWATCH_RUNNING, updateCircle);
- if (updateCircle) {
- long time = Utils.getTimeNow();
- SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(
- getApplicationContext());
- long intervalStartTime = prefs.getLong(
- Stopwatches.KEY + CircleTimerView.PREF_CTV_INTERVAL_START, -1);
- if (intervalStartTime != -1) {
- intervalStartTime = time;
- SharedPreferences.Editor editor = prefs.edit();
- editor.putLong(Stopwatches.KEY + CircleTimerView.PREF_CTV_INTERVAL_START,
- intervalStartTime);
- editor.putBoolean(Stopwatches.KEY + CircleTimerView.PREF_CTV_PAUSED, false);
- editor.apply();
- }
- }
- }
-
- private void writeSharedPrefsLap(long lapTimeElapsed, boolean updateCircle) {
- writeToSharedPrefs(null, lapTimeElapsed, null, null, updateCircle);
- if (updateCircle) {
- long time = Utils.getTimeNow();
- SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(
- getApplicationContext());
- SharedPreferences.Editor editor = prefs.edit();
- long laps[] = readLapsFromPrefs();
- int numLaps = laps.length;
- long lapTime = laps[1];
- if (numLaps == 2) { // Have only hit lap once.
- editor.putLong(Stopwatches.KEY + CircleTimerView.PREF_CTV_INTERVAL, lapTime);
- } else {
- editor.putLong(Stopwatches.KEY + CircleTimerView.PREF_CTV_MARKER_TIME, lapTime);
- }
- editor.putLong(Stopwatches.KEY + CircleTimerView.PREF_CTV_ACCUM_TIME, 0);
- if (numLaps < Stopwatches.MAX_LAPS) {
- editor.putLong(Stopwatches.KEY + CircleTimerView.PREF_CTV_INTERVAL_START, time);
- editor.putBoolean(Stopwatches.KEY + CircleTimerView.PREF_CTV_PAUSED, false);
- } else {
- editor.putLong(Stopwatches.KEY + CircleTimerView.PREF_CTV_INTERVAL_START, -1);
- }
- editor.apply();
- }
- }
-
- private void writeSharedPrefsStopped(long elapsedTime, boolean updateCircle) {
- writeToSharedPrefs(null, null, elapsedTime, Stopwatches.STOPWATCH_STOPPED, updateCircle);
- if (updateCircle) {
- long time = Utils.getTimeNow();
- SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(
- getApplicationContext());
- long accumulatedTime = prefs.getLong(
- Stopwatches.KEY + CircleTimerView.PREF_CTV_ACCUM_TIME, 0);
- long intervalStartTime = prefs.getLong(
- Stopwatches.KEY + CircleTimerView.PREF_CTV_INTERVAL_START, -1);
- accumulatedTime += time - intervalStartTime;
- SharedPreferences.Editor editor = prefs.edit();
- editor.putLong(Stopwatches.KEY + CircleTimerView.PREF_CTV_ACCUM_TIME, accumulatedTime);
- editor.putBoolean(Stopwatches.KEY + CircleTimerView.PREF_CTV_PAUSED, true);
- editor.putLong(
- Stopwatches.KEY + CircleTimerView.PREF_CTV_CURRENT_INTERVAL, accumulatedTime);
- editor.apply();
- }
- }
-
- private void writeSharedPrefsReset(boolean updateCircle) {
- writeToSharedPrefs(null, null, null, Stopwatches.STOPWATCH_RESET, updateCircle);
+ return Service.START_NOT_STICKY;
}
-}
+} \ No newline at end of file
diff --git a/src/com/android/deskclock/stopwatch/StopwatchTimer.java b/src/com/android/deskclock/stopwatch/StopwatchTimer.java
new file mode 100644
index 000000000..9a9d20eb8
--- /dev/null
+++ b/src/com/android/deskclock/stopwatch/StopwatchTimer.java
@@ -0,0 +1,188 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.deskclock.stopwatch;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.RectF;
+import android.util.AttributeSet;
+import android.view.View;
+
+import com.android.deskclock.R;
+import com.android.deskclock.Utils;
+import com.android.deskclock.data.DataModel;
+import com.android.deskclock.data.Lap;
+import com.android.deskclock.data.Stopwatch;
+
+import java.util.List;
+
+/**
+ * Custom view that draws a reference lap when one exists.
+ */
+public final class StopwatchTimer extends View {
+
+ /** The size of the dot indicating the user's position within the reference lap. */
+ private final float mDotRadius;
+
+ /** An amount to subtract from the true radius to account for drawing thicknesses. */
+ private final float mRadiusOffset;
+
+ /** Used to scale the width of the marker to make it similarly visible on all screens. */
+ private final float mScreenDensity;
+
+ /** The color indicating the remaining portion of the current lap. */
+ private final int mRemainderColor;
+
+ /** The color indicating the completed portion of the lap. */
+ private final int mCompletedColor;
+
+ /** The size of the stroke that paints the lap circle. */
+ private final float mStrokeSize;
+
+ /** The size of the stroke that paints the marker for the end of the prior lap. */
+ private final float mMarkerStrokeSize;
+
+ private final Paint mPaint = new Paint();
+ private final Paint mFill = new Paint();
+ private final RectF mArcRect = new RectF();
+
+ private boolean mAnimate;
+
+ @SuppressWarnings("unused")
+ public StopwatchTimer(Context context) {
+ this(context, null);
+ }
+
+ public StopwatchTimer(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ final Resources resources = context.getResources();
+ final float dotDiameter = resources.getDimension(R.dimen.circletimer_dot_size);
+
+ mDotRadius = dotDiameter / 2f;
+ mScreenDensity = resources.getDisplayMetrics().density;
+ mStrokeSize = resources.getDimension(R.dimen.circletimer_circle_size);
+ mMarkerStrokeSize = resources.getDimension(R.dimen.circletimer_marker_size);
+ mRadiusOffset = Utils.calculateRadiusOffset(mStrokeSize, dotDiameter, mMarkerStrokeSize);
+
+ mRemainderColor = resources.getColor(R.color.clock_white);
+ mCompletedColor = Utils.obtainStyledColor(context, R.attr.colorAccent, Color.RED);
+
+ mPaint.setAntiAlias(true);
+ mPaint.setStyle(Paint.Style.STROKE);
+
+ mFill.setAntiAlias(true);
+ mFill.setColor(mCompletedColor);
+ mFill.setStyle(Paint.Style.FILL);
+ }
+
+ /**
+ * Start the animation if it is not currently running.
+ */
+ void startAnimation() {
+ if (!mAnimate) {
+ mAnimate = true;
+ postInvalidateOnAnimation();
+ }
+ }
+
+ /**
+ * Stop the animation if it is currently running.
+ */
+ void stopAnimation() {
+ mAnimate = false;
+ }
+
+ @Override
+ public void onDraw(Canvas canvas) {
+ // Compute the size and location of the circle to be drawn.
+ final int xCenter = getWidth() / 2;
+ final int yCenter = getHeight() / 2;
+ final float radius = Math.min(xCenter, yCenter) - mRadiusOffset;
+
+ // Reset old painting state.
+ mPaint.setColor(mRemainderColor);
+ mPaint.setStrokeWidth(mStrokeSize);
+
+ final List<Lap> laps = getLaps();
+
+ // If a reference lap does not exist or should not be drawn, draw a simple white circle.
+ if (laps.isEmpty() || !DataModel.getDataModel().canAddMoreLaps()) {
+ // Draw a complete white circle; no red arc required.
+ canvas.drawCircle(xCenter, yCenter, radius, mPaint);
+
+ // No need to continue animating the plain white circle.
+ mAnimate = false;
+
+ return;
+ }
+
+ // The first lap is the reference lap to which all future laps are compared.
+ final int lapCount = laps.size();
+ final Lap firstLap = laps.get(lapCount - 1);
+ final Lap priorLap = laps.get(0);
+ final long firstLapTime = firstLap.getLapTime();
+ final long currentLapTime = getStopwatch().getTotalTime() - priorLap.getAccumulatedTime();
+
+ // Draw a combination of red and white arcs to create a circle.
+ mArcRect.top = yCenter - radius;
+ mArcRect.bottom = yCenter + radius;
+ mArcRect.left = xCenter - radius;
+ mArcRect.right = xCenter + radius;
+ final float redPercent = (float) currentLapTime / (float) firstLapTime;
+ final float whitePercent = 1 - (redPercent > 1 ? 1 : redPercent);
+
+ // Draw a white arc to indicate the amount of reference lap that remains.
+ canvas.drawArc(mArcRect, 270 + (1 - whitePercent) * 360, whitePercent * 360, false, mPaint);
+
+ // Draw a red arc to indicate the amount of reference lap completed.
+ mPaint.setColor(mCompletedColor);
+ canvas.drawArc(mArcRect, 270, redPercent * 360 , false, mPaint);
+
+ // Starting on lap 2, a marker can be drawn indicating where the prior lap ended.
+ if (lapCount > 1) {
+ mPaint.setColor(mRemainderColor);
+ mPaint.setStrokeWidth(mMarkerStrokeSize);
+ final float markerAngle = (float) priorLap.getLapTime() / (float) firstLapTime * 360;
+ final float startAngle = 270 + markerAngle;
+ final float sweepAngle = mScreenDensity * (float) (360 / (radius * Math.PI));
+ canvas.drawArc(mArcRect, startAngle, sweepAngle, false, mPaint);
+ }
+
+ // Draw a red dot to indicate current position relative to reference lap.
+ final float dotAngleDegrees = 270 + redPercent * 360;
+ final double dotAngleRadians = Math.toRadians(dotAngleDegrees);
+ final float dotX = xCenter + (float) (radius * Math.cos(dotAngleRadians));
+ final float dotY = yCenter + (float) (radius * Math.sin(dotAngleRadians));
+ canvas.drawCircle(dotX, dotY, mDotRadius, mFill);
+
+ if (mAnimate) {
+ postInvalidateOnAnimation();
+ }
+ }
+
+ private Stopwatch getStopwatch() {
+ return DataModel.getDataModel().getStopwatch();
+ }
+
+ private List<Lap> getLaps() {
+ return DataModel.getDataModel().getLaps();
+ }
+} \ No newline at end of file
diff --git a/src/com/android/deskclock/stopwatch/Stopwatches.java b/src/com/android/deskclock/stopwatch/Stopwatches.java
deleted file mode 100644
index 68b2c6956..000000000
--- a/src/com/android/deskclock/stopwatch/Stopwatches.java
+++ /dev/null
@@ -1,160 +0,0 @@
-/*
- * Copyright (C) 2012 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.deskclock.stopwatch;
-
-import android.content.Context;
-
-import com.android.deskclock.R;
-
-import java.text.DecimalFormatSymbols;
-
-/**
- * Stopwatch utility class providing access to stopwatch resources and data formatting strings of
- * stopwatch data.
- */
-public class Stopwatches {
- // Actions processed by stopwatch receiver
- public static final String SHARE_STOPWATCH = "share_stopwatch";
- public static final String RESET_AND_LAUNCH_STOPWATCH = "reset_and_launch_stopwatch";
- public static final String MESSAGE_TIME = "message_time";
- public static final String SHOW_NOTIF = "show_notification";
- public static final String KILL_NOTIF = "kill_notification";
- public static final String PREF_START_TIME = "sw_start_time";
- public static final String PREF_ACCUM_TIME = "sw_accum_time";
- public static final String PREF_STATE = "sw_state";
- public static final String PREF_LAP_NUM = "sw_lap_num";
- public static final String PREF_LAP_TIME = "sw_lap_time_";
- public static final String PREF_UPDATE_CIRCLE = "sw_update_circle";
- public static final String NOTIF_CLOCK_BASE = "notif_clock_base";
- public static final String NOTIF_CLOCK_ELAPSED = "notif_clock_elapsed";
- public static final String NOTIF_CLOCK_RUNNING = "notif_clock_running";
- public static final String KEY = "sw";
-
- public static final int STOPWATCH_RESET = 0;
- public static final int STOPWATCH_RUNNING = 1;
- public static final int STOPWATCH_STOPPED = 2;
-
- public static final int MAX_LAPS = 99;
- public static final int NO_LAP_NUMBER = -1;
-
- /**
- * Pull a random jocular title
- * @param context context with resources
- * @return the random title
- */
- public static String getShareTitle(Context context) {
- String [] mLabels = context.getResources().getStringArray(R.array.sw_share_strings);
- return mLabels[(int)(Math.random() * mLabels.length)];
- }
-
- /**
- * Create a multi-line text with the stopwatch lap data
- * @param context context with resources
- * @param time total elapsed time
- * @param laps array of times
- * @return formatted text
- */
- public static String buildShareResults(Context context, String time, long[] laps) {
- StringBuilder b = new StringBuilder (context.getString(R.string.sw_share_main, time));
- b.append("\n");
-
- int lapsNum = laps == null? 0 : laps.length;
- if (lapsNum == 0) {
- return b.toString();
- }
-
- b.append(context.getString(R.string.sw_share_laps));
- b.append("\n");
- for (int i = 1; i <= lapsNum; i ++) {
- b.append(getTimeText(context, laps[lapsNum-i], i));
- b.append("\n");
- }
- return b.toString();
- }
-
- /**
- * Create a multi-line text with the stopwatch lap data
- * @param context context with resources
- * @param time total elapsed time
- * @param laps array of times
- * @return formatted text
- */
- public static String buildShareResults(Context context, long time, long[] laps) {
- return buildShareResults(context, getTimeText(context, time, NO_LAP_NUMBER), laps);
- }
-
- /***
- * Format the string of the time running on the stopwatch up to hundred of a second accuracy
- * @param context context with resources
- * @param time - in hundreds of a second since the stopwatch started
- * @param lap lap number
- * @return formatted text
- */
- public static String getTimeText(Context context, long time, int lap) {
- if (time < 0) {
- time = 0;
- }
- String[] formats;
- if (lap != NO_LAP_NUMBER) {
- formats = context.getResources().getStringArray(R.array.shared_laps_format_set);
- } else {
- formats = context.getResources().getStringArray(R.array.stopwatch_format_set);
- }
- char decimalSeparator = DecimalFormatSymbols.getInstance().getDecimalSeparator();
- int formatIndex;
-
- long hundreds, seconds, minutes, hours;
- seconds = time / 1000;
- hundreds = (time - seconds * 1000) / 10;
- minutes = seconds / 60;
- seconds = seconds - minutes * 60;
- hours = minutes / 60;
- minutes = minutes - hours * 60;
- if (hours >= 100) {
- formatIndex = 4;
- } else if (hours >= 10) {
- formatIndex = 3;
- } else if (hours > 0) {
- formatIndex = 2;
- } else if (minutes >= 10) {
- formatIndex = 1;
- } else {
- formatIndex = 0;
- }
- return String.format(formats[formatIndex], hours, minutes,
- seconds, hundreds, decimalSeparator, lap);
- }
-
- /***
- * Sets the string of the time running on the stopwatch up to hundred of a second accuracy
- * @param time - in hundreds of a second since the stopwatch started
- */
- public static String formatTimeText(long time, final String format) {
- if (time < 0) {
- time = 0;
- }
- long hundreds, seconds, minutes, hours;
- seconds = time / 1000;
- hundreds = (time - seconds * 1000) / 10;
- minutes = seconds / 60;
- seconds = seconds - minutes * 60;
- hours = minutes / 60;
- minutes = minutes - hours * 60;
- char decimalSeparator = DecimalFormatSymbols.getInstance().getDecimalSeparator();
- return String.format(format, hours, minutes, seconds, hundreds, decimalSeparator);
- }
-}
diff --git a/src/com/android/deskclock/worldclock/CitySelectionActivity.java b/src/com/android/deskclock/worldclock/CitySelectionActivity.java
index 2af0ce04d..92a6bba69 100644
--- a/src/com/android/deskclock/worldclock/CitySelectionActivity.java
+++ b/src/com/android/deskclock/worldclock/CitySelectionActivity.java
@@ -268,7 +268,7 @@ public final class CitySelectionActivity extends BaseActivity {
}
@Override
- public Object getItem(int position) {
+ public City getItem(int position) {
if (hasHeader()) {
final int itemViewType = getItemViewType(position);
switch (itemViewType) {
@@ -299,7 +299,7 @@ public final class CitySelectionActivity extends BaseActivity {
return view;
case VIEW_TYPE_CITY:
- final City city = (City) getItem(position);
+ final City city = getItem(position);
final TimeZone timeZone = city.getTimeZone();
// Inflate a new view if necessary.
@@ -388,7 +388,7 @@ public final class CitySelectionActivity extends BaseActivity {
for (int position = 0; position < getCount(); position++) {
// Add a section if this position should show the section index.
if (getShowIndex(position)) {
- final City city = (City) getItem(position);
+ final City city = getItem(position);
switch (getCitySort()) {
case NAME:
sections.add(city.getIndexString());
@@ -532,8 +532,8 @@ public final class CitySelectionActivity extends BaseActivity {
}
// Otherwise compare the city with its predecessor to test if it is a header.
- final City priorCity = (City) getItem(position - 1);
- final City city = (City) getItem(position);
+ final City priorCity = getItem(position - 1);
+ final City city = getItem(position);
return getCitySortComparator().compare(priorCity, city) != 0;
}